This shader is for an upcoming game I am currently working on.
I have always wanted to make PSX shaders, so this was the perfect time to learn.
As I have knowledge and experience with Unity’s Scriptable Render Pipelines, I thought it best to make the game in URP so I don’t have to worry about continuing my HLSL learning, and I am able to quickly iterate on my shaders.
This was a choice that I thought was going to keep me from being able to make these shaders initially, but after many hours of research and development, I have ended up achieving every effect I set out to make.
These effects are as follows:
Vertex Jitter
![]() | ![]() |
|---|---|
| before | after |
Arguably one of the most defining features of the PlayStation 1 is the vertex jitter.
So I think I am justified in tackling this problem first.
After some research, I learnt that the vertices are snapped to positions in screen space. The main reason for the snapping is due to the PlayStation 1 using Fixed Point numbers instead of Floating Point.
There’s a fantastic video on floating and fixed point numbers by Spanning Tree.
The basic explanation is that fixed point numbers have low precision, with the same step size regardless of the number. This, along with the way it rasterizes, causes the vertices to snap to the nearest pixel on the screen.
IMPORTANTThis is very high level and abstracted, so I recommend doing some of your own research
I could not find an implementation of a proper screen space vertex jitter in URP with any explanation or image of how they achieved it, so I had to figure this one out myself.
After I achieved the effect, I did eventually find someone else who created the exact same result, but they’re using Amplify. So here is the shader graph!
The output goes into the Position node in Vertex.

It works by converting the vertex position to screen space, rounds to the nearest pixel (or arbitrary resolution), then converts to object space.
We need to use the world position so the vertex can be clamped to the correct pixel on the screen, and we need to convert the final position to object space as the Position node uses object space.
Affine Texture Mapping
![]() | ![]() |
|---|---|
| before | after |
I could not find a single resource of anyone getting this to work on URP with instructions, so I have decided to document it here!
This took a very long time to understand how to do. I learnt pretty quickly that I needed to somehow alter the interpolation of the UV, but I was initially told I had no way of doing this in URP.
This was a lie.
Turns out there is actually a way. Now technically you can’t adjust any of the pre-existing data, but there is a block node called CustomInterpolator in the vertex section, which you can pass the adjusted perspective UV through, along with the position’s w component.
The logic was extrapolated from this blog post by Daniel Ilett.
TIPTo add the
CustomInterpolator, right-click theVertexstack and selectCustomInterpolator

TIPThe
AffineStrengthinterpolation is used to be able to adjust the strength of the effect!
For the fragment, you retrieve them, readjust the UV with the w component, and use that as the texture’s UV.

After this is done, you will no longer have perspective correction and will need to subdivide surfaces to reduce the warping!
![]() | ![]() |
|---|---|
| Simple Quad | Subdivided Plane |
Interlacing
IMPORTANTI can’t show the full implementation, but if you follow the logic, you should be able to recreate the effect 🤍
What and Why?
This effect was the easiest to implement.
The original use of interlacing was to combine two frames into one to cut the framerate of TV shows in half to save on bandwidth, and then the TV would display the two halves as separate images to essentially double the framerate.
Consoles would instead only render half the vertical resolution at a time, not only saving on bandwidth but also in performance.
This is why old shows have a weird line motion blur, and why devices such as the RetroTINK are expensive as they have many ways of deinterlacing analog signals from devices such as consoles and VCRs.
We are going to instead fake the interlacing (as I cannot figure out how to only render every second line).
How?
There’s two ways I’ve thought of doing it. The second one is more of a fun experiment.
The first way is just to do the back and forth rendering directly to the render textures.
i.e. frame 1 goes to the odd scanlines, frame 2 goes to the even scanlines, then repeat.
This will make each new frame render back and forth between odd to even scanlines.
private void Update(){ // Alternate frames if (renderStep == 0) { camera.targetTexture = renderTextureA1; } else { camera.targetTexture = renderTextureA2; } renderStep++; renderStep %= 2; camera.Render();}The second option is to keep the most recent frame on the same line.
i.e. The current frame goes to the odd scanlines, and the previous frame goes to the even scanlines.
This will keep the current frame on the same odd scanline.
private void Start(){ camera.targetTexture = renderTextureA1;}
private void Update(){ // Move A1 to A2 as it is now the previous frame (we want the previous frame to be on the same lines to produce the desired effect) Graphics.CopyTexture(renderTextureA1, renderTextureA2); camera.Render();}Quantization and Dithering
Before anything, we need lighting. I am using these assets from Cyanilux in order to have custom lighting in my shader graphs, and it allows me to bypass all the extra techniques that the standard surface shader provides (we don’t want reflections, metallic, roughness, etc).

NOTEI couldn’t make this match PSX, but I think it’s close enough.
As always, I followed an Acerola video to get a simple algorithm for dithering.
Due to Unity’s ShaderGraph, you can’t easily index the Matrix node. So instead I use a texture I sourced from this GitHub repo. I used the 4x4 matrix.
This image is already normalized from zero to one, so we don’t have to do that step outlined in the video.
Once we’ve added the image and filter together, we need to get the closest colour. This is done using the following formula:

This is the final filter in the shader graph:

For the examples, I am using an n of 128.
UPDATEUnity has a dither node, and I finally decided to learn how to use it.
Below is the new code. I think it also looks more natural in its gradients, so I don’t know where I messed up.
All images still use the old version.

UPDATEAfter making a game with this shader and debugging the visuals, I missed a crucial step where the colours being dithered were in the wrong colourspace.
Make sure you convert the colourspace from RGB to Linear before the dither, and then Linear to RGB after.
Here it is, the full effect in all its glory!
This gif is using the Back and Forth interlacing

And here it is on mgs rat
![]() | ![]() |
|---|





