Diffused Posterization

Adding Noise to Posterization of 0-1 (Unity/HLSL)

In this tutorial we will look at posterization and a simple technique to add diffused noise to the effect.

The tutorial for this shader is going to be divided into 5 sections:

  • Posterization
  • Line Function
  • Adding Noise to Line Function
  • Colour Correction
  • Application

Posterization

The Posterization effect leverages the step function to sub-divide each whole-number interval of a linear function into k parts.


float posterize(float v, float k) {
return ceil(v*k)/k;
}

k=2, blue is linear increase, red is step increase

As we can see above, as x increases linearly, y increases in increments of 1/k everytime x crosses the 1/k threshold on its own axis.

When applied to a uv-texture, the step function looks like this:


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, 7);


o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = post;
o.Alpha = 1.0;
}

k=7

As we have seen before, a uv-texture ranges from 0 to 1 on each axis. In the code above, we have sub-divided the y-axis into 7 parts. This has the effect of giving us 7 distinct bands of colour that are all still in the range of 0 to 1.

Line Function

Next, we would like to draw lines at the edge of each colour band like so:

Let's start by drawing a single red line.

A single line drawn at 0.5, width 0.1


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);

fixed bar = step(IN.uv_MainTex.y, 0.5) - step(IN.uv_MainTex.y, 0.4);

fixed4 c = (1-bar) * post + bar * fixed4(1,0,0,1);


o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;

o.Alpha = c.a;
}

The above code checks whether the current value of the y-axis is less than 0.5 while also checking if it is less than 0.4. If the value is in the sweet spot where it is less than 0.5 but greater than 0.4, it assigns a value of 1 to the variable bar.

It then uses bar to blend with the posterized colour and red.

Now that we can draw a single line of a certain width anywhere on the uv-texture, we use the same technique to draw a line of width 0.1 at our posterized values on the y-axis:


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);

fixed bar = step(IN.uv_MainTex.y, post) - step(IN.uv_MainTex.y, post-0.1);

fixed4 c = (1-bar) * post + bar * float4(1,0,0,1);

o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
}

Success!

Adding Noise to Line Function

With the basics of posterization and posterized line drawing under our belt, we can move on to doing neat things with it. In the previous section we fixed our line widths to 0.1. In this section we will vary the line widths by a random variable.

Perlin Noise Texture


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);

fixed noise = tex2D(_NoiseTex, IN.uv_NoiseTex);

fixed bar = step(IN.uv_MainTex.y, post) - step(IN.uv_MainTex.y, post-noise/_K);

fixed4 c = (1-bar) * post + bar * float4(1,0,0,1);

o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
}

In the code above, we sample the Noise Texture which gives us a value between 0 and 1. We use this value to vary our line width. We also divide the noise by _K to scale the noise down such that if we have more colour bands due to our posterize function, the less we expand the width of each line due to noise.

k=3

Finally, to make the effect more interesting, we offset the noise texture coordinates by time.


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);

float2 textureCoordinate = IN.uv_NoiseTex + float2(_Time.y/10, _Time.y/20);

fixed noise = tex2D(_NoiseTex, textureCoordinate);


fixed bar = step(IN.uv_MainTex.y, post) - step(IN.uv_MainTex.y, post-noise/_K);
fixed4 c = (1-bar) * post + bar * float4(1,0,0,1);

o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
}

Colour Correction

Next, we want to replace the red colour with one that is appropriate to make it look like one band of colour from each posterization level is diffusing into the one below.


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);

float2 textureCoordinate = IN.uv_NoiseTex + float2(_Time.y/10, _Time.y/20);
fixed noise = tex2D(_NoiseTex, textureCoordinate);
float invK = 1.0/_K;


fixed bar = step(IN.uv_MainTex.y, post) - step(IN.uv_MainTex.y, post-noise/_K);
fixed4 c = (1-bar) * post + bar * (post+invK);


o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
}

We initialize a variable invK which is essentially the gap between each colour band. If k=2, the interval 0 to 1 is divided into 2 equal intervals, and so 1/2 is the difference between each colour band.

Each noisy, red line diffuses into a preceeding colour band, and we want to assign it the colour of the next colour band. We do this by adding invK (the gap value) to the current post value.

k=3

Application

Diffused Posterization is a pretty reusable image effect since it works on any range from 0 to 1; although this is pretty much the end of this tutorial we will continue to build the advertised effect.

We start by showing what posterization looks like on diffused lighting: diff (NDotL)


half4 LightingCustom (SurfaceOutputCustom s, half3 lightDir, half3 viewDir) {
float diff = max(0, dot(s.Normal, lightDir));
fixed post = posterize(diff, _K);

half4 col;
col = post;
col.a = s.Alpha;

return col;
}

We then proceed to take the posterization effect out of the surf function so that we can move it into our lighting function. Instead we assign the actual colour of the texture assigned to the Albedo.

We also setup a patterned, screenspace texture as we have seen in the previous tutorial, but this time we use it to sample and pass noise to the lighting function.


void surf (Input IN, inout SurfaceOutputCustom o) {
fixed post = posterize(IN.uv_MainTex.y, _K);


float2 textureCoordinate = IN.screenPos.xy / IN.screenPos.w; // perspective divide

float aspect = _ScreenParams.x / _ScreenParams.y;

textureCoordinate.x = textureCoordinate.x * aspect;

textureCoordinate += float2(_Time.y/10, _Time.y/20);


fixed noise = tex2D (_NoiseTex, frac(textureCoordinate * _NoiseScale));

fixed4 c = tex2D(_MainTex, IN.uv_MainTex);


float invK = 1.0/_K;

fixed bar = step(IN.uv_MainTex.y, post) - step(IN.uv_MainTex.y, post-noise/_K);

fixed4 c = (1-bar) * post + bar * (post + invK);


o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
o.Albedo = c.rgb;
o.Alpha = c.a;
o.noise = noise;

}

The screen space uv-textures that are used to sample noise look like this (code not shown):

We continue to diffuse the posterized lighting:


half4 LightingCustom (SurfaceOutputCustom s, half3 lightDir, half3 viewDir) {
float diff = max(0, dot(s.Normal, lightDir));
fixed post = posterize(diff, _K);

float invK = 1.0/_K;

fixed bar = step(diff, post) - step(diff, post-s.noise/_K);


fixed b = (1-bar) * post + bar * (post+invK);

half3 l = (s.Albedo * b + _AmbientColour * _AmbientStrength);


half4 col;
col.rgb = l;

col.a = s.Alpha;

return col;
}

Perlin Noise

Worley Noise

The full shader and sample scene can be found in this repository. Thanks for tuning in, until next time!