Toon Shaders
— Unity development, Shader Effects, Graphics — 2 min read
Toon shading (often called cel shading) is a rendering style designed to make 3D surfaces emulate 2D, flat surfaces. This is a kind of NPR(Non-Photorealistic Rendering).The core concept of this shading technique is to have sharp diffuse edge and sharp specular reflections and also an Outline.
1. How to write a simple toon shader with unlit shader
1.1 Diffuse
1//Diffuse light steps2 float difLight=dot(worldLightDir,i.worldNormal)*0.5+0.5;3 difLight = smoothstep(0,1,difLight);4 float toon=floor(difLight*_Steps)/_Steps;5 difLight=lerp(difLight,toon,_ToonEffect);6 fixed3 diffuse=_LightColor0.rgb*albedo*_DiffuseColor.rgb*difLight;
1.2 Specular
The specular calculation of Blinn-Phong light model is to to use the dot product of vertex normal and the middle angle of lightdir and viewdir (halfdir)
1//halfDir is the middle angle of lightdir and viewdir2//_Gloss is a factor to controll specular strength3float spec=pow(max(0,dot(normal,halfDir)),_Gloss)
But for toon shading, we skip the Gloss power and compare the dot product with a threshold using the step function. If the dot product is greater than the threshold the specular is 1, otherwise is 0
1float spec=dot(normal,halfDir);2spec = step(threshold,spec);
1.3 Rim
Rim is related to the angle of view dir and vertex normal dir.
Rim lighting is the addition of illumination to the edges of an object to simulate reflected light or backlighting. It is especially useful for toon shaders to help the object's silhouette stand out among the flat shaded surfaces.
The "rim" of an object will be defined as surfaces that are facing away from the camera. We will therefore calculate the rim by taking the dot product of the normal and the view direction, and inverting it.
1float rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimDot);2float4 rim = rimIntensity * _RimColor;
1.4 Outline
To render an outline, here we are using a simple method: to add an additional pass and add an outline offset and then draw the normal object on the top of this pass
1//1.object normal extrusion2v.vertex.xyz+=_Outline*v.normal;3o.vertex = UnityObjectToClipPos(v.vertex);4
5//2.view space normal extrusion6float4 pos = mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, v.vertex));7float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV,v.normal));8pos = pos + float4(normal,0) * _Outline;9o.vertex = mul(UNITY_MATRIX_P, pos);10
11//3.clip space normal extrusion12o.vertex = UnityObjectToClipPos(v.vertex);13float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));14float2 viewNormal = TransformViewToProjection(normal.xy);15o.vertex.xy += viewNormal * _Outline;
here is a full version of the toon shader based on vertex and fragment shader
1Shader "Toon/ToonShader"2{3 Properties4 {5 _MainTex ("Texture", 2D) = "white" {}6 _DiffuseColor("Diffuse",Color)=(1,1,1,1)7 _Outline("Outline Width",Range(0,1))=18 _OutlineColor("Outline Color",Color)=(0,0,0,1)9 _Steps("Steps",Range(1,30)) = 110 _ToonEffect("ToonEffect", Range(0,1)) = 0.511 _Specular("Specular Color",Color)=(1,1,1,1)12 _SpecularScale("Specular Scale",Range(0.0001,3))=113 _RimColor("Rim Light Color",Color)=(1,1,1,1)14 _RimPower("Rim Strength", Range(0.00000001,3))=115 _XRayColor("Oculusion Color",Color)=(1,1,1,1)16 _XRayPower("XRay Power",Range(0.0001,3))=117 }18 SubShader19 {20 Tags21 {22 "Queue"= "Geometry+1000" "RenderType"="Opaque"23 }24 LOD 10025 Pass26 {27 Name "Outline"28 Cull Front29
30 CGPROGRAM31 #pragma vertex vert32 #pragma fragment frag33
34 #include "UnityCG.cginc"35 #include "Lighting.cginc"36
37 struct appdata38 {39 float4 vertex : POSITION;40 float2 uv : TEXCOORD0;41 };42
43 struct v2f44 {45 float4 vertex : SV_POSITION;46 };47
48 float _Outline;49 fixed4 _OutlineColor;50 float _Steps;51 float _ToolEffect;52
53 v2f vert(appdata_base v)54 {55 v2f o;56 //物体法线外扩57 //v.vertex.xyz+=_Outline*v.normal;58 //o.vertex = UnityObjectToClipPos(v.vertex);59
60 //视角空间法线外拓61 //float4 pos = mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, v.vertex));62 //float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV,v.normal));63 //pos = pos + float4(normal,0) * _Outline;64 //o.vertex = mul(UNITY_MATRIX_P, pos);65
66 //裁剪空间法线外拓67 o.vertex = UnityObjectToClipPos(v.vertex);68 float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));69 float2 viewNormal = TransformViewToProjection(normal.xy);70 o.vertex.xy += viewNormal * _Outline;71 return o;72 }73
74 fixed4 frag(v2f i) : SV_Target75 {76 return _OutlineColor;77 }78 ENDCG79 }80
81 Pass82 {83 CGPROGRAM84 #pragma vertex vert85 #pragma fragment frag86
87 #include "UnityCG.cginc"88 #include "Lighting.cginc"89
90 struct v2f91 {92 float4 pos : SV_POSITION;93 float2 uv : TEXCOORD0;94 fixed3 worldNormal:TEXCOORD1;95 fixed3 worldPos:TEXCOORD2;96 };97
98 sampler2D _MainTex;99 float4 _MainTex_ST;100 float4 _DiffuseColor;101 float _Steps;102 float _ToonEffect;103 fixed4 _RimColor;104 float _RimPower;105 float _SpecularScale;106 fixed4 _Specular;107
108 v2f vert(appdata_base v)109 {110 v2f o;111 o.pos = UnityObjectToClipPos(v.vertex);112 o.worldNormal = UnityObjectToWorldNormal(v.normal);113 o.worldPos = mul(unity_ObjectToWorld, o.pos);114 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);115 return o;116 }117
118 fixed4 frag(v2f i) : SV_Target119 {120 // sample the texture121 fixed4 albedo = tex2D(_MainTex, i.uv);122 fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);123
124 //view Direction125 fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));126
127 //Diffuse light steps128 float difLight = dot(worldLightDir, i.worldNormal) * 0.5 + 0.5;129 difLight = smoothstep(0, 1, difLight);130 float toon = floor(difLight * _Steps) / _Steps;131 difLight = lerp(difLight, toon, _ToonEffect);132 fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * difLight;133
134 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;135
136 fixed3 halfDir = normalize(worldLightDir + viewDir);137 float spec = dot(i.worldNormal, halfDir);138 fixed w = fwidth(spec) * 2.0;139 fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(140 0.0001, _SpecularScale);141
142 /*float rim = 1 - dot(i.worldNormal, viewDir);143 fixed3 rimColor = _RimColor * pow(rim, 1 / _RimPower);*/144 float rimdot = 1 - dot(i.worldNormal, viewDir);145 float rimIntensity = smoothstep(_RimPower - 0.01, _RimPower + 0.01, rimdot);146 float4 rim = rimIntensity * _RimColor;147 return float4(ambient + diffuse + specular + rim, 1);148 }149 ENDCG150 }151 Pass152 {153 Tags154 {155 "LightMode"="ShadowCaster"156 }157
158 CGPROGRAM159 #pragma vertex vert160 #pragma fragment frag161 #pragma multi_compile_shadowcaster162 #include "UnityCG.cginc"163
164 struct v2f165 {166 V2F_SHADOW_CASTER;167 };168
169 v2f vert(appdata_base v)170 {171 v2f o;172 TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)173 return o;174 }175
176 float4 frag(v2f i) : SV_Target177 {178 SHADOW_CASTER_FRAGMENT(i)179 }180 ENDCG181 }182
183 }184}
2. Toon effect in Surface shader
we can also use Surface shader to implement the toon effect. The main difference is that we need to customize the LightMode in Surface shader to achieve this. And the base calculation are similar to the unlit shader. Surface shader enable easier and fast implementation of shadow casting and multiple lights handle
1//our lighting function. Will be called once per light2 float4 LightingStepped(SurfaceOutput s, float3 lightDir, half3 viewDir, float shadowAttenuation){3 //how much does the normal point towards the light?4 float towardsLight = dot(s.Normal, lightDir);5 // make the lighting a hard cut6 float towardsLightChange = fwidth(towardsLight);7 float lightIntensity = smoothstep(0, towardsLightChange, towardsLight);8
9 #ifdef USING_DIRECTIONAL_LIGHT10 //for directional lights, get a hard vut in the middle of the shadow attenuation11 float attenuationChange = fwidth(shadowAttenuation) * 0.5;12 float shadow = smoothstep(0.5 - attenuationChange, 0.5 + attenuationChange, shadowAttenuation);13 #else14 //for other light types (point, spot), put the cutoff near black, so the falloff doesn't affect the range15 float attenuationChange = fwidth(shadowAttenuation);16 float shadow = smoothstep(0, attenuationChange, shadowAttenuation);17 #endif18 lightIntensity = lightIntensity * shadow;19
20 //calculate shadow color and mix light and shadow based on the light. Then taint it based on the light color21 float3 shadowColor = s.Albedo * _ShadowTint;22 float4 color;23 color.rgb = lerp(shadowColor, s.Albedo, lightIntensity) * _LightColor0.rgb;24 color.a = s.Alpha;25 return color;26 }
Multiple lights and cast shadow
The full shader script is as follow
1Shader "Toon/ToonShaderSurfaceBigBro"2{3 Properties4 {5 _Color ("Color", Color) = (1,1,1,1)6 _MainTex ("Albedo (RGB)", 2D) = "white" {}7 _Outline("Outline Width",Range(0,1))=18 _OutlineColor("Outline Color",Color)=(0,0,0,1)9 _Steps("Steps",Range(1,30)) = 110 _ToonEffect("ToonEffect", Range(0,1)) = 0.511 _DiffuseColor("Diffuse",Color)=(1,1,1,1)12 _Specular("Specular Color",Color)=(1,1,1,1)13 _SpecularScale("Specular Scale",Range(0.0001,3))=114 }15 SubShader16 {17 Tags18 {19 "RenderType"="Opaque"20 }21 LOD 20022 Pass23 {24 Name "Outline"25 Cull Front26
27 CGPROGRAM28 #pragma vertex vert29 #pragma fragment frag30
31 #include "UnityCG.cginc"32 #include "Lighting.cginc"33
34 struct appdata35 {36 float4 vertex : POSITION;37 float2 uv : TEXCOORD0;38 };39
40 struct v2f41 {42 float4 vertex : SV_POSITION;43 };44
45 float _Outline;46 fixed4 _OutlineColor;47
48 v2f vert(appdata_base v)49 {50 v2f o;51 //裁剪空间法线外拓52 o.vertex = UnityObjectToClipPos(v.vertex);53 float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal));54 float2 viewNormal = TransformViewToProjection(normal.xy);55 o.vertex.xy += viewNormal * _Outline;56 return o;57 }58
59 fixed4 frag(v2f i) : SV_Target60 {61 return _OutlineColor;62 }63 ENDCG64 }65
66 CGPROGRAM67 #pragma surface surf Toon addshadow68
69 float4 _DiffuseColor;70 fixed4 _Color;71 float _Steps;72 float _ToonEffect;73 float _SpecularScale;74 fixed4 _Specular;75
76 half4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)77 {78 //Diffuse light steps79 float difLight = dot(lightDir, s.Normal) * 0.5 + 0.5;80 difLight = smoothstep(0, 1, difLight);81 float toon = floor(difLight * _Steps) / _Steps;82 difLight = lerp(difLight, toon, _ToonEffect);83 fixed3 diff = _LightColor0.rgb * _DiffuseColor.rgb * difLight;84
85 fixed3 halfDir = normalize(lightDir + viewDir);86 float spec = dot(s.Normal, halfDir);87 fixed w = fwidth(spec) * 2.0;88 fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(89 0.0001, _SpecularScale);90
91 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color;92
93 //custom shadow94 #ifdef USING_DIRECTIONAL_LIGHT95 float attenuationChange = fwidth(atten) * 0.5;96 float shadow = smoothstep(0.5 - attenuationChange, 0.5 + attenuationChange, atten);97 #else98 float attenuationChange = fwidth(atten);99 float shadow = smoothstep(0, attenuationChange, atten);100 #endif101 half4 c;102 c.rgb = (ambient + diff + specular) * atten * s.Albedo;103 c.a = s.Alpha;104 return c;105 }106
107 struct Input108 {109 float2 uv_MainTex;110 };111
112 sampler2D _MainTex;113
114 void surf(Input IN, inout SurfaceOutput o)115 {116 o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;117 }118 ENDCG119 }120 FallBack "Diffuse"121}
3. Other effects in Toon shader
3.1 Light turbulance
To enable a light turbulance for a torch, the trick is to add as offset for the lightDir
1float3 shakeOffset = float3(0, 0, 0);2shakeOffset.x = sin(_Time.z * 15);3shakeOffset.y = sin(_Time.z * 13 + 5);4shakeOffset.z = sin(_Time.z * 12 + 7);5
6#ifdef POINT7lightDir += shakeOffset * 0.1f;8#endif
3.2 Hair specular
Kajiya-Kay Shading Model
3.3 With multiple lights
There is another way to add multiple lights. By default, the Unity light system only supports four light sources. But we can add lights manually to make sure that every light has been calculate throw for shadow casting.
First there should be a LitTag to mark which light we want to take count.
1public class MultiLitsTag:MonoBehaviour2{3 public float shakeStrength = 0.5f;4 public int litSorting = 0;5}
Then generate a multilights container, and send the following message to shader:
(float4)_LitPosList: xyz is the world position, z can be another light perticular factor
(float4)_LitColList:xyz is the light color, w is the light range
1public class MultiLitsContainer : MonoBehaviour2{3 [SerializeField] private Transform[] pointLits;4
5 private Light[] _pointLightList;6
7 private MultiLitsTag[] _lightTagList;8 // Start is called before the first frame update9 void Awake()10 {11 _pointLightList = new Light[pointLits.Length];12 _lightTagList = new MultiLitsTag[pointLits.Length];13 for (int i = 0; i < pointLits.Length; i++)14 {15 _pointLightList[i] = pointLits[i].GetComponent<Light>();16 _lightTagList[i]=pointLits[i].GetComponent<MultiLitsTag>();17 }18 }19
20 // Update is called once per frame21 void Update()22 {23 Vector4[] litPosList = new Vector4[10];24 Vector4[] litColList = new Vector4[10];25
26 for (int i = 0; i < pointLits.Length; i++)27 {28 litPosList[i] = new Vector4(pointLits[i].position.x,pointLits[i].position.y,pointLits[i].position.z,_lightTagList[i].shakeStrength);29 litColList[i] = new Vector4(_pointLightList[i].color.r,_pointLightList[i].color.g,_pointLightList[i].color.b,_pointLightList[i].range);30 }31 Shader.SetGlobalFloat("_LitCount",pointLits.Length);32 Shader.SetGlobalVectorArray("_LitPosList",litPosList);33 Shader.SetGlobalVectorArray("_LitColList",litColList);34 }35}
And the shader would be as following
1Shader "Toon/ToonMultipleLightShader"2{3 Properties4 {5 _Tint("Tint",Color)=(1,1,1,1)6 _MainTex ("Texture", 2D) = "white" {}7 _UnlitColor("Shadow Color",Color)=(0.5,0.5,0.5,1)8 _MultiListFadeDistance("MultiList FadeDistance",Float)=209 10 _RimColor("Rim Color",Color)=(0.5,0.5,0.5,1)11 _RimLightSampler ("RimLight Sampler", 2D) = "white" {}12 _RimIntensity("Rim Intensity",Float)=1013 _UnlitThreshold("Shadow Range",Range(0,1))=0.114 }15 SubShader16 {17 Tags18 {19 "Queue"="Geometry" "RenderType"="Opaque"20 }21 LOD 10022
23 Pass24 {25 Tags26 {27 "LightMode"="ForwardBase"28 }29 CGPROGRAM30 #pragma vertex vert31 #pragma fragment frag32
33 #include "UnityCG.cginc"34
35 #pragma multi_compile_fwdbase36 #include "AutoLight.cginc"37 #include "UnityLightingCommon.cginc"38
39 struct appdata40 {41 float4 vertex : POSITION;42 float3 normal : NORMAL;43 float2 uv : TEXCOORD0;44 };45
46 struct v2f47 {48 float4 pos:SV_POSITION;49 float4 posWorld:TEXCOORD0;50 float3 normal:TEXCOORD1;51 float2 uv : TEXCOORD2;52 float3 camDir: TEXCOORD3;53 float3 lightDir: TEXCOORD4;54 LIGHTING_COORDS(5, 6)55 };56
57 sampler2D _MainTex;58 float4 _MainTex_ST;59 fixed4 _Tint;60 fixed4 _UnlitColor;61 62 float _UnlitThreshold;63 float _MultiListFadeDistance;64
65 fixed4 _RimColor;66 sampler2D _RimLightSampler;67 float _RimIntensity;68
69 float _LitCount;70 float4 _LitPosList[10];71 float4 _LitColList[10];72
73 v2f vert(appdata v)74 {75 v2f o;76 o.pos = UnityObjectToClipPos(v.vertex);77 o.posWorld = mul(unity_ObjectToWorld, v.vertex);78 o.normal = UnityObjectToWorldNormal(v.normal);79 o.camDir = normalize(_WorldSpaceCameraPos - o.posWorld);80 o.lightDir = WorldSpaceLightDir(v.vertex);81 o.uv = TRANSFORM_TEX(v.uv, _MainTex);82 return o;83 }84
85 fixed4 frag(v2f i) : SV_Target86 {87 // sample the texture88 fixed4 col = tex2D(_MainTex, i.uv) * _Tint;89
90 //add fire shake effect91 fixed4 pointLitCol = fixed4(0, 0, 0, 0);92 fixed pointLit = 0;93 float3 shakeOffset = float3(0, 0, 0);94 shakeOffset.x = sin(_Time.z * 15);95 shakeOffset.y = sin(_Time.z * 13+5);96 shakeOffset.z = sin(_Time.z * 12+7);97
98 for (int n = 0; n < _LitCount; n++)99 {100 float litDist = distance(_LitPosList[n].xyz, i.posWorld.xyz);101 float viewDist = distance(_LitPosList[n].xyz, _WorldSpaceCameraPos);102 float viewFade = 1 - saturate(viewDist / _MultiListFadeDistance);103 if (litDist < _MultiListFadeDistance)104 {105 float3 litDir = _LitPosList[n].xyz - i.posWorld.xyz;106 litDir += shakeOffset * 0.07 * _LitPosList[n].w;107 litDir = normalize(litDir);108 fixed newlitValue = max(0, dot(i.normal, litDir)) * (_LitPosList[n].w - litDist) * viewFade >109 0.3;110 fixed4 newlitCol = newlitValue * fixed4(_LitColList[n].xyz, 1);111 pointLitCol = lerp(pointLitCol, newlitCol, newlitValue);112 }113 }114 115 //light and shadow116 float3 normalDirection = normalize(i.normal);117 float attenuation = LIGHT_ATTENUATION(i);118 float3 lightDirection = normalize(_WorldSpaceLightPos0).xyz;119 fixed3 lightColor = _Tint.rgb * _UnlitColor.rgb * _LightColor0.rgb;120 if (attenuation * max(0.0, dot(normalDirection, lightDirection)) >= _UnlitThreshold)121 {122 lightColor = _LightColor0.rgb * _Tint.rgb;123 }124
125 //Rimlight126 float normalDotCam = dot(i.normal, i.camDir.xyz);127 float falloffU = clamp(1.0 - abs(normalDotCam), 0.02, 0.98);128
129 float rimlightDot = saturate(0.5 * (dot(i.normal, i.lightDir + float3(-1, 0, 0)) + 1.5));130 falloffU = saturate(rimlightDot * falloffU);131 falloffU = tex2D(_RimLightSampler, float2(falloffU, 0.25f)).r;132 float3 rimCol = falloffU * col * _RimColor * _RimIntensity;133
134 return float4(col.rgb * (lightColor.rgb + pointLitCol) + rimCol, 1.0);135 }136 ENDCG137 }138 }139 Fallback "VertexLit"140}
Reference
https://sorumi.xyz/posts/unity-toon-shader/
https://blog.csdn.net/QO_GQ/article/details/119616656
https://blog.csdn.net/csuyuanxing/article/details/123519895
https://www.cnblogs.com/littleperilla/p/15759993.html
https://roystan.net/articles/toon-shader/
https://www.ronja-tutorials.com/post/032-improved-toon/
http://walkingfat.com/《塞尔达-荒野之息》中角色受多个点光源影响做法/
https://docs.unity3d.com/Manual/SL-SurfaceShaderLightingExamples.html