Unity Tutorial — Toon Shade in HLSL

Fe Game Art
4 min readJan 22, 2022

--

Hey! In this tutorial I’m going to show a way to implement Toon Shade using textures to define the light influence.

But first, what is Toon Shade?

In a short explanation, Toon Shade, or Cel Shading, is a technique used to make 3D models get some cartoonish look, using hard transitions to shade instead of smooth transition.

Here’s a exemple.

The left model uses the Default Unlit Shader, the middle one uses a simple diffuse and the last one, the tutorial’s Toon Shade.

To implement it, we need some information:

  • Light direction;
  • 3D model’s normal;
  • Gray scale texture to map the light intensity.

In this tutorial, we will implement the following steps:

  • Calculate the dot product of the Light direction and the 3D model’s normal;
  • Use the result of the previous step to get a color from a gray scale texture;
  • Multiply by the base color of current pixel.

Normal Vector

Basically, a normal vector is a vector which is perpendicular to a point. In the example below, there’re many normals, represented by the blue lines.

Dot product

The dot product is calculated using 2 vectors and returns:

  • 1 if they are in exactly the same direction;
  • -1 if they are in opposite directions;
  • 0 if the vectors are perpendicular.

In our case, we’re going to use the Light direction and the model’s normal.

In this shader, we will use the dot product to get a point from a gray scale texture. To do it, is necessary to adjust the dot product to go from 0 to 1 by multipling it by 0.5 then adding 0.5.

Implementation

Let’s get started with some code.

First, we need to add the gray scale texture as a input of our shader.

Properties
{
_MainTex("Texture", 2D) = "white" {}
_GrayScaleTex("Gray Scale Texture", 2D) = "white" {}
}

...
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _GrayScaleTex;

Include the normal to the appdata struct. It will give us access to the model’s normal.

struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};

Then, create it in the v2f struct.

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
};

In the vertex shader, calculate the model’s world normals and pass it to the v2f struct. It will allow us to use this information in the fragment shader.

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal); return o;
}

To make things organized, create a function called toonShade. It will receive some information:

  • Color: The color of the pixel that will be affected
  • Normal: Model’s object normals
float4 toonShade(float4 color, float3 normal)
{
}

The first thing we need to add in our function is the light direction. We’ll need it to calculate the dot product. Unity already gives it to us in the built-in variable _WorldSpaceLightPos0. One more detail is that we need to normalize that variable. Doing that, we’ll get a vector with the same direction, but with the lenght of 1. It’s necessary to make the dot product work properly in our case.

fixed3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);

Then we need to calculate the dot product. To do it, we only need to call the function dot() passing the two vectors we want to use.

As mentioned before, dot product returns a value from -1 to 1. We need to adjust that value to a 0 to 1 range as we gonna use it to get a point based in the UV. To do it, just multiply the dot product by 0.5 then add 0.5.

fixed NdotL = dot(normal, lightDirection);
NdotL = (NdotL * 0.5) + 0.5;

Now we’ll get a color from the gray scale texture, using the result of the previous step, and multiply it by the current pixel color.

fixed toonColor = tex2D(_GrayScaleTex, float2(NdotL, 0));
color *= toonColor;

Alright! But we’re not done yet. We must call our toonShade function in our fragment shader. To do it we must pay attention to one more detail..

Before passing the normal to the toonShade function, it’s necessary to normalize it too. As we’ve done with the _WorldSpaceLightPos0 variable some steps before.

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
i.worldNormal = normalize(i.worldNormal);
col = toonShade(col, i.worldNormal);
return col;
}

Now create a material, add a gray scale texture and apply to a model!

Final result

There’re another ways to implement this effect, if you liked go on and try another approaches! I hope this tutorial helps.

You can check the full code here.

Follow me on Twitter to see more content!

--

--