Part 3: Texture Deformation

Erikkubiak
5 min readApr 17, 2023

--

In this serie of articles, we focus on how to reproduce a medieval drawing Post Processing in Unity using URP and Custom Render Feature, some C# code, some shader code and a lots of magic 👨‍🎨

In this part, we will continue what we did by just offsetting a bit the resulted outline thanks to a noise.

We will:

  • Use Mixture to generate a noise
  • Add a new step to offset the image and add some swirls to it

1. Generating our noise with Mixture

If you don’t know it, Mixture is an amazing tool that allows you o create procedural textures really easily and in Unity. In order to install it, you can easily follow their instructions.

When done, you can create Mixture assets really easily like creating a shader graph asset. You just need to Right Click on Project > Create > Static Mixture Graph. It is one of the Mixture you can create and it does really look like any nodal UI in Unity with the same controls. Mine for this look like this:

As you can see it is pretty simple, I am jst packing two noise in the R and G channel of a texture. Both are the same, I am just changing the seed of the second one. I am far from being an expert into Mixture so I will let you explore this.

Remark: What we did here is called Channel Packing, it is a widely used technic to improve performance. As you may know, reading into a texture can be quite heavy as it will need to process some stuff and in the worst case will need to load the memory in the Cache. Thus, to avoid a lot of texture read we can just pack alltogether. Like here, we need two noises, instead of having two textures we packed it into the R and G channel of the texture.

Now that we are done with our graph, you can just drag and drop it into any Texture field, it will work directly. Amazing, right?

2. Setting up our feature

Now, we can configure our code to sent the needed data to the GPU. First of all, we need to add settings for the texture and the intensity. Don’t remember how to do? No worries, I am here for you:

public class PassSettings
{
//...
public Texture NoiseTexture;
public float ObjectNoiseStrength;
}

Here we just modified our Settings class to add our Texture and a strength parameter to easily tweak the intensity. You can now drag’n’drop the texture and tweak the float. Now, we have tweakables settings but the GPU is not aware of it, so let’s change that:

public GravurePPPass(GravurePPRenderFeature.PassSettings settings,
GravurePPPass _clone)
{
//...
m_DrawMaterial = CoreUtils.CreateEngineMaterial("Hidden/EngravingPP");
m_DrawMaterial.SetFloat("_OutlineSize", settings.OutlineSize);
m_DrawMaterial.SetTexture("_NoiseTexture", m_Settings.NoiseTexture);
m_DrawMaterial.SetFloat("_NoiseStrength", m_Settings.ObjectNoiseStrength);
}

Now, we have it on our cpu and sent it to the GPU but we are still missing an important step. Our GPU is still not aware of it, so let’s add those to our Shader:

Shader "Hidden/GravurePP"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OutlineSize("Outline Size", Float) = 2
_NoiseTexture("Noise Texture", 2D) = "white" {}
_NoiseStrength("Noise Strength", Float) = 0.01
}
//...
}

Here we add:

  • A texture parameter, the type for 2D texture is … 🥁… 2D. The default is set to “white”, it just means hat Unity will use its fully white texture as default. There are others values such as “black”, “red”, etc. that you may find there in the Texture2D row.
  • Another float parameter.

3. Let’s move that pixel 💪

Finally, let’s move the pixels in the shader to add this hand drawn look.

First of all, let’s declare our variables in the hlsl part:

sampler2D _MainTex;
float4 _MainTex_TexelSize;

sampler2D _NoiseTexture;
float _NoiseStrength;

fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}

As you can see, we have again our Main Texture and its size information and then we reference our noise texture and the strength. Now, let’s do our deformation:

fixed4 frag (v2f i) : SV_Target
{
float2 noise = tex2D(_NoiseTexture, i.uv);
float2 shift = float2(
(noise.x * 2 - 1),
(noise.y * 2 - 1)
) * _NoiseStrength;
i.uv += shift;

return tex2D(_MainTex, i.uv);
}

As you can see, here we:

  1. Read our noise texture at the given screen space
  2. Remap the noise from [0, 1] to [-1, 1]. Then the pixels will be shifted in both direction. It is not mandatory but I find it better.
  3. We multiply it by the noise strength to make it configurable.
  4. Finally we add it to the uv and read our main texture before returning it.

Now, we should have a deformed texture. Why? Because we just took the UV that are used to read the main texture and we shifted it according to a noise, so the result will look noisy. I will let you tweak to see what it changes. Finally, we can the shift according to the texture inverse size to have uniform shifting compared to size. Thus, it will be the same deformation scale on X and Y even if it is wide angle screen. Here is the result:

You can really see how it is deformed on straight lines like it would have been drawn by an old monk 🖋

Remark: As suggested by Alexander Ameye, we may use world space UV instead of Screen Space UV to sample the noise. Thus the same geometry will have the same noise applied.

Conclusion

Your outlines looks even more amazing now😄 we will see in next part how to achieve that cool looking cross hatching you can see in the video 😉

You can follow me for more 😍

Thanks

--

--