本文為翻譯,附上原文鏈接。
轉載請注明出處——polobymulberry-博客園。
暗黑系
動機
如果你滿足以下條件,我建議你閱讀這篇教程:
准備工作
我們想實現一個toon shader - 一種能讓模型看起來具有卡通效果的shader,在圖形學領域,這被稱作非真實感圖形學(Non Photorealistic Rendering)。
為了實現這種卡通效果,我們得做以下幾件事:
表面着色器的工作流程
在教程的第一部分我使用了一個簡化版的表面着色器流程圖,事實上關於表面着色器的工作流程還有很多內容。首先我們看一個相對完整的工作流程圖:
(譯者注:上面很多部分都是可選的,如果我們不自定義這些部分,那將使用Unity自己提供的處理函數。這里尤其注意可選部分的使用格式。
#pragma surface surfaceFunction lightModel [optionalparams]
1.使用Unity自帶的光照模型Lambert,此時不需要自定義對應函數
#pragma surface surf Lambert
2.使用自定義光照模型,此時需要自定義函數,函數名稱為Lighting+lightModel,如下:
#pragma surface surf SimpleLambert half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, half atten) { ... }
#pragma surface surf Lambert finalcolor:mycolor vertex:myvert void myvert (inout appdata_full v, out Input data) {...} void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {...}
)
你可以從此流程圖看出表面着色器中的自定義部分有很多。比如我們可以首先使用自定義的光照模型來處理我們的光照數據,並將數據傳遞給表面着色器的輸出結構體,從而使表面着色器產生對應像素的顏色值。最后我們可以使用finalcolor來擾亂顏色值,並將最終結果輸出到屏幕上。
步驟1 — 減少紋理上的顏色種類
我們使用之前的含有bump紋理的shader,並往該shader中添加一些代碼。
一開始,我們將介紹如何使用finalcolor函數以減少紋理上的顏色種類。
#pragma surface surf Lambert finalcolor:final
首先我們添加finalcolor到#pragma語句尾部,來告訴程序我將使用finalcolor功能。注意此處我們命名該功能函數名稱為final。
_Tooniness ("Tooniness", Range(0.1,20)) = 4
我們添加_Tooniness屬性值(Range類型,范圍0.1~20,默認值為4)— 我們將使用
該值來限制紋理使用的顏色種類數量。當然我們在shader中還需定義同樣名稱的變量作為引用。
float _Tooniness;
我們簡單寫一個減少顏色種類的函數:
void final(Input IN, SurfaceOutput o, inout fixed4 color) { color = floor(color * _Tooniness)/_Tooniness; }
在上面函數中,我們先將color(rgba類型)乘上_Tooniess,再用floor(x)函數(取不大於x的最大整數)來將舍去結果中的浮點數部分,最后除以_Tooniness(比如說原始color.rgba=(0.3,0.4,0.5,1.0),_Tooniness=4,經過處理后,最后color.rgba=(0.25,0.25,0.5.1.0),我們發現顏色值的分量只能是0,1/4,2/4,3/4,1中的一個,這也就是_Tooniness的作用,將顏色從0~1的范圍變成了只有五個值,從而降低了顏色種類)。
下面是shader的源碼,完整的資源請點擊這里:
Shader "Custom/ToonShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {}
// 暫時用上該變量
_MainBump ("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; // 添加_Tooniness的引用 float _Tooniness; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } // 增加的final函數,修改像素的顏色 void final(Input IN, SurfaceOutput o, inout fixed4 color) { color = floor(color * _Tooniness)/_Tooniness; } ENDCG } FallBack "Diffuse" }
使用該shader的效果對比:
使用前:
使用后:
調整_Tooniness可以改變顏色種類數量(或者稱為顏色的精度)。一般卡通效果上的人物顏色種類也是很少的,所以此shader也比較符合卡通繪制的習慣。但是此shader不能稱作是一個真正的卡通shader。
步驟2 — Toon光照模型
接下來我們實現一個Toon光照模型,此光照模型使光照在模型上有銳利的顏色邊緣效果,而不是原模型中的過渡效果。
為了彌補上一節shader中的不足(實現的並不是真正卡通效果),我們在此基礎上添加一個變量_ColorMerge,注意此處的_ColorMerge做的其實和上一節的shader中的_Tooniness作用類似,也是為了減少顏色種類。另外在新shader中我們將_Tooniness用於Toon光照函數中 — 這樣做更合理!
_ColorMerge (“Color Merge, Range(0.1, 20)) = 0
對應的變量為
float _ColorMerge;
現在我們將在shader中添加一個自定義的光照模型Toon。我們將使用Toon光照計算函數來代替我們一直使用的Lambert光照模型。
我們在代碼中修改#pragma語句為
#pragma surface surf Toon
注意我們移除了finalcolor函數 — 我已經將對應的功能移到surf函數中實現了,這是為了在光照之前就計算好顏色(我們將得到更多變、更卡通的效果)。
上面已經說過我們想使用Toon光照模型就必須實現LightingToon函數 — LightingToon這個名字是將Lighting和#pragma語句中的Toon合並在一起組成的名字。
half4 LightingToon(SurfaceOutput s, half3 lightDir, half atten) { half4 c; half NdotL = dot(s.Normal, lightDir); NdotL = floor(NdotL * _Tooniness)/_Tooniness; c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; }
光照函數總是接受三個參數 — 表面着色器的輸出結構體(SurfaceOutput s),光照方向(half3 lightDir)和光衰減因子(half atten)(光衰減因子attenuation主要是計算點光源、探照燈的衰減以模擬出明暗效果。)
該LightingToon函數總是返回光照后像素的顏色(half4類型)。
我們簡單介紹下Toon光照模型是如何工作的 — 首先我們計算出光照方向和該像素處法向的點乘結果。記住如果點乘結果(即兩向量的余弦值)為1,表示兩向量方向一致,如果為-1,則兩向量反向,如果為0,則兩向量正好垂直。這個知識點對光照計算很有用途 — 比如一個像素直接對着光照方向,那么此像素顯示的就是它本身顏色。如果兩向量夾角大於90,即像素背向光照方向,對應顏色將變成黑色,如果夾角在0~90之間,我們根據角度的來插值出該像素的顏色。
Unity會對場景中的所有光照進行處理,來計算模型在所有光照下所產生的效果。並且在我們從surface函數中返回像素值后,Unity也會自動為該像素添加漫射光(ambient light)和光強度(intensity)的作用效果 — 此處我們想表達的意思就是,如果該像素不受當前光照影響,那么它將顯示為黑色。
對於我們的Toon Shading
surf函數中僅僅使用了_ColorMerge對顏色種類進行簡化。
void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_MainBump, IN.uv_MainBump)); o.Albedo = floor(c.rgb*_ColorMerge)/_ColorMerge; o.Alpha = c.a; }
下面是shader的源碼,完整的資源請點擊這里:
Shader "Custom/ToonLightShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _MainBump ("Bump", 2D) = "bump" {} // 該變量主要使用來降低顏色種類的 _Tooniness ("Tooniness", float) = 4 _ColorMerge ("ColorMerge", float) = 4 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Toon sampler2D _MainTex; sampler2D _MainBump; // 添加_Tooniness的引用 float _Tooniness; float _ColorMerge; struct Input { float2 uv_MainTex; float2 uv_MainBump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_MainBump, IN.uv_MainBump)); o.Albedo = floor(c.rgb*_ColorMerge)/_ColorMerge; o.Alpha = c.a; } half4 LightingToon(SurfaceOutput s, half3 lightDir, half atten) { half4 c; half NdotL = dot(s.Normal, lightDir); NdotL = floor(NdotL * _Tooniness)/_Tooniness; c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; } ENDCG } FallBack "Diffuse" }
步驟3 — 使用漸變紋理
有時候(尤其是動畫中),像素值的顏色會突然地變暗甚至是變黑。我們其實是想將這種顏色變化變得平滑一些。要做出平滑的效果,我們可以寫一個平滑效果的函數,但是最簡單的方法是使用一個ramp texture來產生平滑過渡效果,但又不失Toon效果中顏色種類少的要求。
之前我們求取NdotL,會有一個問題,比如當我們_Tooniness=4時,我們發現如果NdotL=0.249和NdotL=0.251之間的顏色會出現突然得變化,我們使用ramp紋理的目的也在於此,我們可以發現上面這張紋理在顏色邊緣的變化是使用平滑過渡的方式進行的,這也就解決了上一段中提出的問題。
我們之所以能使用該方法,是因為紋理的uv坐標范圍正好是0~1,所以我們可以將NdotL的值作為u值,而將v值設為0.5,這樣我們只需要通過這樣約定好的uv值在ramp紋理上采樣,得到對應的顏色值。
顯然我們首先要添加ramp紋理屬性值。
_Ramp ("Ramp Texture", 2D) = "white" {}
然后再添加一個sampler2D類型的_Ramp引用。
sampler2D _Ramp;
最后我們更新LightingToon光照函數。
half4 LightingToon(SurfaceOutput s, half3 lightDir, half atten) { half4 c; half NdotL = dot(s.Normal, lightDir); NdotL = saturate(tex2D(_Ramp, float2(NdotL,0.5))); c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; }
不加bump texture效果:
下面是shader的源碼,完整的資源請點擊這里:
Shader "Custom/RampTextureShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _MainBump ("Bump", 2D) = "bump" {} // 該變量主要使用來降低顏色種類的 _Tooniness ("Tooniness", Range(0.1,20)) = 4 _ColorMerge ("ColorMerge", Range(0.1,20)) = 8 // 使用ramp texture _Ramp ("Ramp Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Toon sampler2D _MainTex; sampler2D _MainBump; // 添加_Tooniness的引用 float _Tooniness; // 添加_ColorMerge的引用 float _ColorMerge; // 添加_Ramp的引用 sampler2D _Ramp; struct Input { float2 uv_MainTex; float2 uv_MainBump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_MainBump, IN.uv_MainBump)); o.Albedo = floor(c.rgb*_ColorMerge)/_ColorMerge; o.Alpha = c.a; } half4 LightingToon(SurfaceOutput s, half3 lightDir, half atten) { half4 c; half NdotL = dot(s.Normal, lightDir); NdotL = saturate(tex2D(_Ramp, float2(NdotL,0.5))); c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; } ENDCG } FallBack "Diffuse" }
添加邊緣效果
對於一個卡通效果的模型,我們還想添加黑色的邊緣。在表面着色器中想實現該效果得使用邊緣光照(rim lighting)(光照的顏色是黑色!)
如果當前像素方向和視線方向成90度,我們可以認為此像素為模型邊緣,然后我們即將該像素置為黑色,這就是我們所說的邊緣光照。
此時我們又將使用點乘這個強大的工具 — 但是這次點乘的參數不是光照方向,而是相機的的朝向,因為我們的黑色邊緣是根據視線方向(即相機的朝向)計算出來的。
當然我們在shader程序中得使用一個屬性值和一個變量來控制我們的邊緣線。
_Outline ("Outline", Range(0,1)) = 0.4
以及
float _Outline;
我們需要得到視線的方向 — 幸運的是如果我們在Input結構體中直接包含viewDir參數就可以得到視線方向 — 就像下面這樣:
struct Input { float2 uv_MainTex; float2 uv_MainBump; float3 viewDir; };
現在我們所要做的就是在surf函數中檢測出模型的邊緣。
// 檢測出模型的邊緣 half edge = saturate(dot(o.Normal, normalize(IN.viewDir))); edge = edge < _Outline ? edge/4 : 1; // 將邊緣塗黑 o.Albedo = floor(c.rgb*_ColorMerge)/_ColorMerge * edge;
首先我們計算像素法向和視線方向的點乘結果edge。然后如果edge小於_Outline的值,怎說明該像素屬於邊緣像素,於是重置edge為edge/4(一位置減小edge的值,注意此處4是一個經驗值),否則edge=1。最后我們將edge乘上像素的顏色值。
下面是shader的源碼,完整的資源請點擊這里:
Shader "Custom/RimLightingShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _MainBump ("Bump", 2D) = "bump" {} // 該變量主要使用來降低顏色種類的 _Tooniness ("Tooniness", Range(0.1,20)) = 4 _ColorMerge ("ColorMerge", Range(0.1,20)) = 8 // 使用ramp texture _Ramp ("Ramp Texture", 2D) = "white" {} _Outline ("Outline", Range(0,1)) = 0.4 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Toon sampler2D _MainTex; sampler2D _MainBump; // 添加_Tooniness的引用 float _Tooniness; // 添加_ColorMerge的引用 float _ColorMerge; // 添加_Ramp的引用 sampler2D _Ramp; // 添加_Outline的引用 float _Outline; struct Input { float2 uv_MainTex; float2 uv_MainBump; float3 viewDir; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_MainBump, IN.uv_MainBump)); // 檢測出模型的邊緣 half edge = saturate(dot(o.Normal, normalize(IN.viewDir))); edge = edge < _Outline ? edge/4 : 1; // 將邊緣塗黑 o.Albedo = floor(c.rgb*_ColorMerge)/_ColorMerge * edge; o.Alpha = c.a; } half4 LightingToon(SurfaceOutput s, half3 lightDir, half atten) { half4 c; half NdotL = dot(s.Normal, lightDir); NdotL = saturate(tex2D(_Ramp, float2(NdotL,0.5))); c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; } ENDCG } FallBack "Diffuse" }
總結
我們盡可能多的結合表面着色器的各種特性來實現我們的Toon Shader。事實上創建邊緣輪廓最好的方法(緩解顏色突變的情問題)是在shader代碼中創建兩個pass — 但是要使用這種方法得學會寫fragment shader並且要自己去寫光照。額,下次再說吧…..