VFX Breakdown: Let's Go Retro!





Since this project is heavily-inspired by Metal Gear Solid, I wanted to try my hand recreating the PS1/PSX visual style using Unity's URP Shader Graph (with some help from HLSL). Nowadays, retro PSX-style visuals are extremely popular (especially among indie titles), and you can find tons of videos and tutorials online to recreate this effect. However, in my research, I found many of these tutorials to be lacking. Though they looked great, the methods used felt more like workarounds than true emulation. Wanting to adhere as close as possible to the original rendering techniques, I researched the defining factors of the original PSX's visual style: Vertex Snapping, Affine Texture Mapping, and Screen-Space Dithering.  In this breakdown, I'll detail how I approached recreating these classic retro visuals using two shaders: a full-screen dither shader, and an unlit object shader.

Screen-Space Dithering:
If you're unfamiliar with the process of dithering, it's a process used to "hide" color banding on posterized images. Despite the PSX's ability to render images in 24-bit true color, it was extremely limited use compared to the 15-bit color mode.  Since I had already created a 2-bit dither shader that utilized a similar full-screen dither, I used that shader graph as a reference I could rework/improve.  While I couldn't find the exact dithering pattern the PSX used, I used an ordered dithering approach using a pre-defined Bayer threshold map.  First, the screen is downsampled by a globally defined value to find the new screen pixel position (rounded to the nearest integer).  Then, the pixel position is compared to the corresponding threshold map value. Once we have the threshold map value, next comes the process of quantizing the screen image from 24-bit to 15-bit.  Before calculating the new colors, it's important to know how many colors each RGB channel can hold at a specified bit-depth. This value gives us the amount of spread in color space. When calculated out, each channel at 15-bit color depth can contain for a max channel value of 32 (32768 colors in total). Next, the threshold and spread values are multiplied together and added to the pixel's color value, offsetting the color slightly. Finally, the offset color value is multiplied by the colors per channel, rounded down, then divided by the colors per channel again, resulting in the final dithered image.

Vertex Snapping/Vertex Wobble:
The PSX's wobbly models rivals the dithering as the arguably most defining characteristic of the console's look, and is another product of the hardware's limitations. The PSX was unable to calculate floating point (non-integer) numbers, leading to a number of interesting rounding calculations. In this instance, the PSX rounds or "snaps" the vertex positions of an object to their nearest screen-space pixel coordinates. To achieve this in Shader Graph, I used the same downsampling method I used in the previous stage to find the vertex's clip space position (rounded to the nearest integer), convert it back to object space and set the new vertex position. After this stage, the object shader proceeds to the texture mapping phase.

Affine Texture Mapping:
The final piece of the classic PSX look is a product of the console's affine texture mapping. Affine texture mapping is a method of texture mapping that does not take depth into account, perfect for the hardware's lack of Z-buffer. The lack of depth calculation leads to the fluid-like appearance of the textures, working in tandem with the vertex wobble beautifully. Unfortunately, modern renderers prefer a more accurate method of texture mapping, so I had to force this imperfection back into Unity.  


Files

PSX Object shader.png 168 kB
Dec 13, 2024
noDownsamp.png 43 kB
Dec 13, 2024
Down2.png 100 kB
Dec 13, 2024

Get Prototype: Odysseus

Leave a comment

Log in with itch.io to leave a comment.