卡通渲染的主要原理包含两个方面:
1.轮廓线的描边效果
2.模型漫反射离散和纯色高光区域的模拟
描边:
描边的实现方法采用将模型的轮廓线顶点向法线(或顶点)的方向扩展一定的像素得到。也可通过边缘检测(基于法线和深度)来实现。
漫反射离散:
利用离散的Ramp纹理对漫反射光照效果进行采样,可以实现不同效果梯度的卡通渲染效果,例如:
注意此纹理的灰度变化并非均匀变化,而是类似于一种突变,仅在灰度变化的交界处进行了平滑过渡。这样的Ramp纹理正是卡通渲染所需要的颜色过渡模式,也是卡通渲染实现的核心内容。
也可增加阶度的个数实现更多层次的卡通渲染效果。
纯色高光区域:
不同于真实渲染,卡通渲染的高光部分通常就是一个色块,这里主要的问题是处理高光边缘的锯齿问题。
这里可以利用smoothstep(-w,w,spec-threshold);在边缘范围[-w,w]进行平滑插值处理,其中w可以通过fwidth(spec);得到。
fwidth(spec);用于得到邻域像素的近似导数值。
Shader脚本如下,光照模型采用半兰伯特:
1 Shader "MyUnlit/CartoonShading" 2 { 3 Properties 4 { 5 _Color("Color Tint",Color)=(1,1,1,1) 6 _MainTex ("Texture", 2D) = "white" {} 7 _Ramp("Ramp Texture",2D)="white"{} 8 _Outline("Outline",Range(0,0.1))=0.02 9 _Factor("Factor of Outline",Range(0,1))=0.5 10 _OutlineColor("Outline Color",Color)=(0,0,0,1) 11 _Specular("Specular",Color)=(1,1,1,1) 12 _SpecularScale("Specular Scale",Range(0,0.1))=0.01 13 } 14 SubShader 15 { 16 Tags { "RenderType"="Opaque" } 17 //此Pass渲染描边 18 Pass 19 { 20 //命名用于之后可重复调用 21 NAME "OUTLINE" 22 //描边只用渲染背面,挤出轮廓线,所以剔除正面 23 Cull Front 24 //开启深度写入,防止物体交叠处的描边被后渲染的物体盖住 25 ZWrite On 26 CGPROGRAM 27 #pragma vertex vert 28 #pragma fragment frag 29 30 #include "UnityCG.cginc" 31 32 float _Outline; 33 float _Factor; 34 fixed4 _OutlineColor; 35 36 struct appdata 37 { 38 float4 vertex : POSITION; 39 float3 normal:NORMAL; 40 }; 41 42 struct v2f 43 { 44 float4 vertex : SV_POSITION; 45 }; 46 47 v2f vert (appdata v) 48 { 49 v2f o; 50 float3 pos=normalize(v.vertex.xyz); 51 float3 normal=normalize(v.normal); 52 53 //点积为了确定顶点对于几何中心的指向,判断此处的顶点是位于模型的凹处还是凸处 54 float D=dot(pos,normal); 55 //校正顶点的方向值,判断是否为轮廓线 56 pos*=sign(D); 57 //描边的朝向插值,偏向于法线方向还是顶点方向 58 pos=lerp(normal,pos,_Factor); 59 //将顶点向指定的方向挤出 60 v.vertex.xyz+=pos*_Outline; 61 o.vertex=UnityObjectToClipPos(v.vertex); 62 return o; 63 } 64 65 fixed4 frag (v2f i) : SV_Target 66 { 67 return fixed4(_OutlineColor.rgb,1); 68 } 69 ENDCG 70 } 71 //此Pass渲染卡通着色效果,主要运用半兰伯特光照模型配合渐变纹理 72 Pass 73 { 74 Tags{"LightMode"="ForwardBase"} 75 Cull Back 76 CGPROGRAM 77 78 #pragma vertex vert 79 #pragma fragment frag 80 #pragma multi_compile_fwdbase 81 82 #include "UnityCG.cginc" 83 //引入阴影相关的宏 84 #include "AutoLight.cginc" 85 //引入预设的光照变量,如_LightColor0 86 #include "Lighting.cginc" 87 88 fixed4 _Color; 89 sampler2D _MainTex; 90 sampler2D _Ramp; 91 fixed4 _Specular; 92 fixed _SpecularScale; 93 float4 _MainTex_ST; 94 95 struct appdata 96 { 97 float4 vertex:POSITION; 98 float2 uv:TEXCOORD0; 99 float3 normal:NORMAL; 100 float4 tangent:TANGENT; 101 }; 102 103 struct v2f 104 { 105 float4 pos:SV_POSITION; 106 float2 uv:TEXCOORD0; 107 float3 worldNormal:TEXCOORD1; 108 float3 worldPos:TEXCOORD2; 109 SHADOW_COORDS(3) 110 }; 111 112 v2f vert(appdata v) 113 { 114 v2f o; 115 o.pos=UnityObjectToClipPos(v.vertex); 116 o.uv=TRANSFORM_TEX(v.uv,_MainTex); 117 o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject); 118 o.worldPos=mul(unity_ObjectToWorld,v.vertex); 119 TRANSFER_SHADOW(o); 120 121 return o; 122 } 123 124 fixed4 frag(v2f i):SV_Target 125 { 126 fixed3 worldNormal=normalize(i.worldNormal); 127 fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos)); 128 fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos)); 129 fixed3 worldHalfDir=normalize(worldLightDir+worldViewDir); 130 131 //计算材质反射率 132 fixed4 c=tex2D(_MainTex,i.uv); 133 fixed3 albedo=c.rgb*_Color.rgb; 134 135 //计算环境光 136 fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo; 137 138 //处理阴影 139 UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); 140 141 //计算半兰伯特漫反射系数,亮化处理,将结果从[-1,1]映射到[0,1],以便作为渐变纹理的采样uv 142 fixed diff=dot(worldNormal,worldLightDir); 143 diff=(diff*0.5+0.5)*atten; 144 145 //卡通渲染的核心内容,对漫反射进行区域色阶的离散变化 146 fixed3 diffuse=_LightColor0.rgb*albedo*tex2D(_Ramp,float2(diff,diff)).rgb; 147 148 //计算半兰伯特高光系数,并将高光边缘的过渡进行抗锯齿处理,系数越大,过渡越明显 149 fixed spec=dot(worldNormal,worldHalfDir); 150 fixed w=fwidth(spec)*3.0; 151 152 //计算高光,在[-w,w]范围内平滑插值 153 fixed3 specular=_Specular.rgb*smoothstep(-w,w,spec-(1-_SpecularScale))*step(0.0001,_SpecularScale); 154 155 return fixed4(ambient+diffuse+specular,1.0); 156 } 157 ENDCG 158 } 159 } 160 FallBack "Diffuse" 161 }
效果如下: