卡通渲染的主要原理包含兩個方面:
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 }
效果如下: