We will be making this effect :

This effect can handle both color depth reduction ( reducing number of colors gets displayed ) as well as the dithering ( our main objective )
As this is an image effect go ahead and create a new image effect in Unity.
There is no vertex shader code as this is an image effect.
So now let's look at the properties that we have for the shader:
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
That's all? well.. we have more but these are not exposed as public. So we do not declare them in the properties section.
These are the properties declared in the CG PROGRAM
sampler2D _MainTex;
int _ColourDepth;
float _DitherStrength;
We will come to see what _ColourDepth
and _DitherStrength
are later on.
Now we will look at the Dither Table that we will be using:
static const float4x4 ditherTable = float4x4
(
-4.0, 0.0, -3.0, 1.0,
2.0, -2.0, 3.0, -1.0,
-3.0, 1.0, -4.0, 0.0,
3.0, -1.0, 2.0, -2.0
);
Declare this table along with the rest of the properties ( or uniforms or whatever you wanna call it ).
We need this matrix as it describes the pattern that will be put on screen for dithering.
Dithering comes in handy in a lot of places, In future tutorials we will look into using dithering to make high quality gradients.
Now let's look at the magical fragment shader:
fixed4 frag (v2f i) : SV_TARGET
{
/*(1)*/fixed4 col = tex2D(_MainTex, i.uv);
/*(2)*/uint2 pixelCoord = i.uv*_ScreenParams.xy;
/*(3)*/col += ditherTable[pixelCoord.x % 4][pixelCoord.y % 4] * _DitherStrength;
/*(4)*/return round(col * _ColourDepth) / _ColourDepth;
}
We will go through each line and understand what it does.
- Just accessing the texture and retrieving the color of that texel at the uv co-ordinate.
pixelCoord
is the actual pixel on the screen as it gets converted from ( 0 to 1, 0 to 1 ) space to (0 to width, 0 to height) space as it gets multiplied with_ScreenParams.xy
*Note:pixelCoord
is a unit2, as performing modulus with an int is slow.- Here we are accessing the
ditherTable
and according to our pixel coord we take a value and then make it smaller by multiplying with the_DitherStrength
value ( 0 to 1) then add it tocol
. - In order to artificially reduce color depth we use the round operation to reduce the range of colors that get displayed. We use the
_ColourDepth
(int) value to determine how many colors show up.
Let's move on to making the C# file that goes along with this.
1using UnityEngine;
2[ExecuteInEditMode, ImageEffectAllowedInSceneView, RequireComponent(typeof(Camera))]
3public class DitherEffect : MonoBehaviour
4{
5 public Material ditherMat;
6 [Range(0.0f, 1.0f)]
7 public float ditherStrength = 0.1f;
8 [Range(1, 32)]
9 public int colourDepth = 4;
10
11 private void OnRenderImage(RenderTexture src, RenderTexture dest)
12 {
13 ditherMat.SetFloat("_DitherStrength", ditherStrength);
14 ditherMat.SetInt("_ColourDepth", colourDepth);
15 Graphics.Blit(src, dest, ditherMat);
16 }
17}
Nothing really complicated here...
We just pass in the required data for dither strength and color depth to the material.
Here is the entire shader:
1Shader "Hidden/Dither"
2{
3 Properties
4 {
5 _MainTex ("Texture", 2D) = "white" {}
6 }
7 SubShader
8 {
9 Cull Off ZWrite Off ZTest Always
10
11 Pass
12 {
13 CGPROGRAM
14 #pragma vertex vert
15 #pragma fragment frag
16 #include "UnityCG.cginc"
17
18 struct appdata
19 {
20 float4 vertex : POSITION;
21 float2 uv : TEXCOORD0;
22 };
23
24 struct v2f
25 {
26 float2 uv : TEXCOORD0;
27 float4 vertex : SV_POSITION;
28 };
29
30 v2f vert (appdata v)
31 {
32 v2f o;
33 o.vertex = UnityObjectToClipPos(v.vertex);
34 o.uv = v.uv;
35 return o;
36 }
37
38 sampler2D _MainTex;
39 int _ColourDepth;
40 float _DitherStrength;
41
42 static const float4x4 ditherTable = float4x4
43 (
44 -4.0, 0.0, -3.0, 1.0,
45 2.0, -2.0, 3.0, -1.0,
46 -3.0, 1.0, -4.0, 0.0,
47 3.0, -1.0, 2.0, -2.0
48 );
49
50 fixed4 frag (v2f i) : SV_TARGET
51 {
52 fixed4 col = tex2D(_MainTex,i.uv);
53 uint2 pixelCoord = i.uv*_ScreenParams.xy; //warning that modulus is slow on integers, so use uint
54 col += ditherTable[pixelCoord.x % 4][pixelCoord.y % 4] * _DitherStrength;
55 return round(col * _ColourDepth) / _ColourDepth;
56 }
57 ENDCG
58 }
59 }
60}