Dithering Image Effect - Unity Shader

May 14, 2018
5m
Unity 3DShaders

We will be making this effect :

Dithering 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.

  1. Just accessing the texture and retrieving the color of that texel at the uv co-ordinate.
  2. 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. 
  3. 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 to col.
  4. 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.

C#

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:

Shader Lab

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}