Preface
Well, we have seen this type of shader for a long time. There are many implementation methods and the effects are somewhat different. Starting from this article, I learned how to write the cartoon type shader.
The final effect of this article is as follows (only the monsters and the Apple part are involved ):
The cartoon effect described in this article has the following characteristics:
- Simplified the color used in the model
- Simplify lighting and make the model have a clear area of light and shade
- Draw a contour (that is, stroke) at the edge of the model)
Let's review the pipeline of Unity surface shader. (Source: Unity gems)
We can see that there are four opportunities for modifying the rendering results (the code in the green box ). On the basis of understanding this, we will truly learn how to achieve the above results.
Simplified color
In the first step, we only implement one of the most common bump diffuse shader, and add some other techniques to simplify the color. The built-in shader of unity also includes bump diffuse shader. Its function is very simple, that is, to use a texture (also called a line texture) to record the concave and convex situations on the model, as the normal information of the vertex, the rendered model also feels uneven (details can be found on the official unity website ).
The basic bump diffuse shader code is as follows:
Shader "Example/Diffuse Bump" { Properties { _MainTex ("Texture", 2D) = "white" {} _BumpMap ("Bumpmap", 2D) = "bump" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; sampler2D _MainTex; sampler2D _BumpMap; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); } ENDCG } Fallback "Diffuse" }
The effect is as follows:
Next, perform the following steps:
- Add the following new properties to the properties block:
_Tooniness ("Tooniness", Range(0.1,20)) = 4
- Add a reference to the subshader block:
float _Tooniness;
- Add a new command final to # pragma:
#pragma surface surf Lambert finalcolor:final
Explanation: we can see from the previous pipeline diagram that the last chance to modify pixels is to use finalcolor: Your function. Finalcolor is followed by our function name, and unity will call this function for the final modification. Other optional guidelines can be found on the official website.
- Implement final functions:
void final(Input IN, SurfaceOutput o, inout fixed4 color) { color = floor(color * _Tooniness)/_Tooniness; }
Explanation: multiply the color value by _ tooniness, and then divide it by _ tooniness. Because the color ranges from 0 to 1, multiplying _ tooniness and then taking an integer in a certain range will get a specific integer, so that all colors will be included in a known set, to simplify the color. _ Tooniness: the smaller the value, the fewer color types of the output.
The complete code is as follows:
Shader "Custom/Toon" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Tooniness ("Tooniness", Range(0.1,20)) = 4 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert finalcolor:final sampler2D _MainTex; sampler2D _Bump; float _Tooniness; struct Input { float2 uv_MainTex; float2 uv_Bump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); o.Albedo = c.rgb; o.Alpha = c.a; } void final(Input IN, SurfaceOutput o, inout fixed4 color) { color = floor(color * _Tooniness)/_Tooniness; } ENDCG } FallBack "Diffuse"}
The effect is as follows:
Cartoon Lighting
In addition to simplifying the color by using the above method, it is more common to use a gradient texture (Ramp texture) to simulate cartoon light for the purpose. Is the gradient map we use for monsters (drawn in PS ):
This graph has obvious boundaries, and is not as slow as other gradient graphs. Just as the cartoon style often has obvious differences in light and shade.
To add a lighting function, follow these steps:
- Add the gradient chart attribute in the properties block:
_Ramp ("Ramp Texture", 2D) = "white" {}
- Add a reference to the subshader block:
sampler2D _Ramp;
- Add a new command to # pragma:
#pragma surface surf Toon
Explanation: we removed the final function and moved it to the surf function later. This allows us to have more variability. The above statement indicates that we will use the illumination function named toon.
- Modify the surf function:
void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness); o.Alpha = c.a; }
- Implement the toon function:
half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) { float difLight = max(0, dot (s.Normal, lightDir)); float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir)); float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp; c.a = s.Alpha; return c; }
Explanation: the most important part of the above is how to sample in ramp. We use two values: the diffuse illumination direction and the edge illumination direction. Max is used to prevent the occurrence of strange phenomena in areas with sudden changes in light and shade. The 0.5 operation is used to change the illumination interval and further increase the overall brightness. For more information, see the previous article.
The complete code is as follows:
Shader "Custom/Toon" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Ramp ("Ramp Texture", 2D) = "white" {} _Tooniness ("Tooniness", Range(0.1,20)) = 4 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Toon sampler2D _MainTex; sampler2D _Bump; sampler2D _Ramp; float _Tooniness; float _Outline; struct Input { float2 uv_MainTex; float2 uv_Bump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness); o.Alpha = c.a; } half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) { float difLight = max(0, dot (s.Normal, lightDir)); float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir)); float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp; c.a = s.Alpha; return c; } ENDCG } FallBack "Diffuse"}
The effect is as follows:
Add stroke
Finally, we add the stroke effect to the model. This is achieved through rim lighting. In this example, we render the edge into black to achieve stroke. Edge lighting finds those pixels that are close to 90 ° with the observed direction and turns them black. You probably thought about how to use edge lighting: dot multiplication.
Follow these steps:
- First, add attributes to the properties block for the stroke width:
_Outline ("Outline", Range(0,1)) = 0.4
- Add a reference to the subshader block:
float _Outline;
- As mentioned above, the edge illumination requires the observation direction, so we modify the input structure:
struct Input { float2 uv_MainTex; float2 uv_Bump; float3 viewDir; };
Explanation: viewdir is also a built-in parameter of Unity. Other built-in parameters can be found on the official website.
- We use the following method in the surf function to detect those edges:
half edge = saturate(dot (o.Normal, normalize(IN.viewDir))); edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;
Explanation: First, we get the point multiplication result of the normal direction and observation direction of the pixel. If the result is smaller than our threshold, we think this is the edge point we are looking for and divide it by 4 (an experimental value) to reduce its deserve to black; otherwise, make it equal to 1, that is, there is no effect.
The overall code is as follows:
Shader "Custom/Toon" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Ramp ("Ramp Texture", 2D) = "white" {} _Tooniness ("Tooniness", Range(0.1,20)) = 4 _Outline ("Outline", Range(0,1)) = 0.4 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Toon sampler2D _MainTex; sampler2D _Bump; sampler2D _Ramp; float _Tooniness; float _Outline; struct Input { float2 uv_MainTex; float2 uv_Bump; float3 viewDir; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump)); half edge = saturate(dot (o.Normal, normalize(IN.viewDir))); edge = edge < _Outline ? edge/4 : 1; o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge; o.Alpha = c.a; } half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) { float difLight = max(0, dot (s.Normal, lightDir)); float dif_hLambert = difLight * 0.5 + 0.5; float rimLight = max(0, dot (s.Normal, viewDir)); float rim_hLambert = rimLight * 0.5 + 0.5; float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb; float4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp; c.a = s.Alpha; return c; } ENDCG } FallBack "Diffuse"}
The final effect is as follows:
Conclusion
At the beginning of this article, I was referring to an article in Unity gems, but I found some errors and improvements in the learning process. For example, I have explained the lighting functions, and the implementation of gradient pasters. In the future, you still need to think more and get the best from its dregs.
In the subsequent cartoon shader series, I will first learn how to implement it using fragment shader in Unity gems. Finally, I will learn how to implement the cartoon effect in a unity resource package.
Thank you for your comments and suggestions!