Shader學習分享:Unity製作卡通風格化Shader
之前偶然在80level看到一篇用表面著色器寫的卡通風格化Shader的教程部落格覺得挺有趣:
教程中的shader是用表面著色器(
surface shader
)寫的,我用了頂點片元著色器(
vertex-fragment shader
)復現一遍教程的思路,如有錯誤請大佬指正:
注:表面著色器是unity為了簡化shader製作而設計的一種封裝層級更高的著色器,其本質還是頂點片元著色器,表面著色器會自動處理座標空間轉換和光照計算這些相對繁瑣的操作,但也相應地失去了效果和效能的更強控制。個人理解是個人專案可以用表面著色器,商業專案或者需要更自定義的效果最好用頂點片元著色器。
最終結果例樣
整體思路:
自定義光照模型(意味著在UE4裡比較難搞需要改引擎)
支援漫反射、凹凸法線貼圖
高光大小可調,並支援高光貼圖,可以根據美術的需求,更改高光的形狀
支援輪廓線高亮,並且輪廓線粗細可調,同時可選擇輪廓線是否受陰影影響而消失
漫反射、高光、輪廓線、自發光顏色都可自定義
支援階梯式過渡陰影,可根據所需要的層次感,控制階梯的級數,最高4層
程式碼詳解(開始囉嗦):
注:核心計算從(9)片元著色器開始
(1)程式碼首行命名後,接著色器屬性宣告,宣告可以由使用者在材質面板處控制的引數:
Shader
“Custom/CelShaded”
{
Properties
{
_Color
(
“Color”
,
Color
)
=
(
1
,
1
,
1
,
1
)
_MainTex
(
“Albedo (RGB)”
,
2
D
)
=
“white”
{}
[
Normal
]
_Normal
(
“Normal”
,
2
D
)
=
“bump”
{}
_LightCutoff
(
“Light cutoff”
,
Range
(
0
,
1
))
=
0。5
_ShadowBands
(
“Shadow bands”
,
Range
(
1
,
4
))
=
1
[
Header
(
Specular
)]
_SpecularMap
(
“Specular map”
,
2
D
)
=
“white”
{}
_Glossiness
(
“Smoothness”
,
Range
(
0
,
1
))
=
0。5
[
HDR
]
_SpecularColor
(
“Specular color”
,
Color
)
=
(
0
,
0
,
0
,
1
)
[
Header
(
Rim
)]
_RimSize
(
“Rim size”
,
Range
(
0
,
1
))
=
0
[
HDR
]
_RimColor
(
“Rim color”
,
Color
)
=
(
0
,
0
,
0
,
1
)
[
Toggle
(
SHADOWED_RIM
)]
_ShadowedRim
(
“Rim affected by shadow”
,
float
)
=
0
[
Header
(
Emission
)]
[
HDR
]
_Emission
(
“Emission”
,
Color
)
=
(
0
,
0
,
0
,
1
)
}
(2)宣告SubShader和Opaque不透明渲染型別:
多個SubShader主要用於適應擁有不同效能的平臺,如手機和PC的效能不同,所能承載的shader計算量也不同,所以需要用SubShader來為同一個材質,設定有高有低的梯度式的渲染效果。
如果是用表面著色器的話必須將著色器程式碼直接寫在SubShader中,而不像頂點片元著色器一樣也可以寫在Pass路徑中。多個Pass則是為了提高程式碼的複用性,就像函式一樣,一個Pass定義一種渲染計算方式,比如前向渲染模式一般有兩種Pass - ForwardBase和ForwardAdd,不同的渲染路徑對待光照有不同的計算方式。
RenderType渲染模式這個Tag標籤設定為Opaque不透明,字面上就說明了在渲染不透明物體時定義,一般在需要渲染透明物體的時候將Opaque修改為TransparentBlend等。
SubShader
{
Tags
{
“RenderType”
=
“Opaque”
}
(3)宣告Pass,定義LightMode光照模式為ForwardBase前向渲染基本模式:
ForwardBase前向渲染基本模式只會計算環境光、最重要的平行光、逐頂點/SI-I 光源和 Lightmaps
靜態光源
:
另一種比較流行的是Deferred延遲渲染模式,在光照比較複雜的時候延遲渲染效能更佳,但是延遲渲染不支援高效的抗鋸齒功能,並且渲染透明物體的時候比較麻煩。
Pass
{
Tags
{
“LightMode”
=
“ForwardBase”
}
(4)CGPROGRAM標明開始Cg程式碼,執行預編譯指令,特別注意SHADOWED_RIM這個shader功能指令,為我們開啟了光照影響輪廓線的功能:
multi_compile_fwdbase:前向渲染base部分必寫,為了獲取正確的光照變數
target 3。0:將shader等級從預設的2級升為3級,意味著這個pass能力更牛可處理更多運算
vertex vert 和 fragment frag:告訴unity頂點著色器和片元著色器函式是哪個
UnityCG。cginc:獲取內建計算頂點輸入、視角光照方向的宏
Lighting。cginc:為了獲得LightColor0
環境光變數
等
AutoLight。cginc:為了獲得Unity計算陰影和光照衰減的宏
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma shader_feature SHADOWED_RIM
#pragma target 3。0
#pragma vertex vert
#pragma fragment frag
#include
“UnityCG。cginc”
#include
“Lighting。cginc”
#include
“AutoLight。cginc”
(5)定義了程式碼開頭定義的屬性相匹配的變數,以在計算之中使用使用者輸入的屬性,變數名稱和對應屬性必須一模一樣,可以看到每個使用者輸入的變數名字前都會加個“_”,比如_Color:
fixed4
_Color
;
sampler2D
_MainTex
;
float4
_MainTex_ST
;
sampler2D
_Normal
;
float
_LightCutoff
;
float
_ShadowBands
;
sampler2D
_SpecularMap
;
float4
_SpecularMap_ST
;
half
_Glossiness
;
fixed4
_SpecularColor
;
float
_RimSize
;
fixed4
_RimColor
;
fixed4
_Emission
;
(6)定義頂點著色器的輸入結構體,向unity索要頂點模型空間位置、法線、切線、紋理座標資訊:
struct
a2v
{
float4
vertex
:
POSITION
;
float3
normal
:
NORMAL
;
float4
tangent
:
TANGENT
;
float4
texcoord
:
TEXCOORD0
;
};
(7)定義頂點著色器的輸出結構體(也就是片元著色器的輸入結構體):
頂點著色器將會輸出,由模型空間變換到裁剪空間之後的
頂點座標
紋理座標uv
由三個
插值暫存器
組成的,切線座標空間到世界座標空間的變換矩陣
SHADOW_COORDS(4):新增計算
陰影紋理座標
的宏,以在後續計算陰影和光照衰減,注意括號中的數字代表其佔用的插值暫存器的序號,也就是TEXCOORD後面那個數字,數下來他應該佔用第4個所以括號內寫4
struct
v2f
{
float4
pos
:
SV_POSITION
;
float4
uv
:
TEXCOORD0
;
float4
TtoW0
:
TEXCOORD1
;
float4
TtoW1
:
TEXCOORD2
;
float4
TtoW2
:
TEXCOORD3
;
SHADOW_COORDS
(
4
)
};
(8)
頂點著色器函式
,完成了頂點座標的空間轉換這個最重要的本職工作,將兩個紋理座標儲存到uv的四個分量xyzw中,將法線Normal的變換
矩陣
存在了三個插值暫存器裡(以便在片元著色器中對法線紋理進行取樣之後,將其座標從切線空間轉到世界空間),最後用宏 TRANSFER_SHADOW(o)計算
陰影座標
然後返回結構體:
注意到儲存變換矩陣的三個插值暫存器的w分量儲存了世界座標位置worldPos的三個分量,這是因為一個插值暫存器最多可以有4個分量xyzw,比如float4,而法線變換矩陣只需要3x3個分量,多出3個分量可以利用來儲存頂點世界座標
v2f
vert
(
a2v
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
o
。
uv
。
xy
=
v
。
texcoord
。
xy
*
_MainTex_ST
。
xy
+
_MainTex_ST
。
zw
;
o
。
uv
。
zw
=
v
。
texcoord
。
xy
*
_SpecularMap_ST
。
xy
+
_SpecularMap_ST
。
zw
;
float3
worldPos
=
mul
(
unity_ObjectToWorld
,
v
。
vertex
)。
xyz
;
fixed3
worldNormal
=
UnityObjectToWorldNormal
(
v
。
normal
);
fixed3
worldTangent
=
UnityObjectToWorldDir
(
v
。
tangent
。
xyz
);
fixed3
worldBinormal
=
cross
(
worldNormal
,
worldTangent
)
*
v
。
tangent
。
w
;
o
。
TtoW0
=
float4
(
worldTangent
。
x
,
worldBinormal
。
x
,
worldNormal
。
x
,
worldPos
。
x
);
o
。
TtoW1
=
float4
(
worldTangent
。
y
,
worldBinormal
。
y
,
worldNormal
。
y
,
worldPos
。
y
);
o
。
TtoW2
=
float4
(
worldTangent
。
z
,
worldBinormal
。
z
,
worldNormal
。
z
,
worldPos
。
z
);
TRANSFER_SHADOW
(
o
);
return
o
;
}
主要計算位置-片元著色器函式:
(9)片元著色器函式,開頭先宣告和計算出頂點世界座標、光照方向、視角方向,宣告的同時也對光照方向和視角方向進行歸一化方便後面直接使用:
half4
frag
(
v2f
i
)
:
SV_Target
{
float3
worldPos
=
float3
(
i
。
TtoW0
。
w
,
i
。
TtoW1
。
w
,
i
。
TtoW2
。
w
);
fixed3
lightDir
=
normalize
(
UnityWorldSpaceLightDir
(
worldPos
));
fixed3
viewDir
=
normalize
(
UnityWorldSpaceViewDir
(
worldPos
));
(10)然後開始關鍵的光照模型,這一段從下往上看可以看出是計算漫反射和陰影的。先是常規的對法線紋理進行取樣,並用變換矩陣將其從切線空間轉到世界空間之後,計算出法線和光照方向的點乘值nDotL, 這一步跟標準光照模型計算漫反射相似。
為了控制被照明表面的光照面積,將點乘結果nDotL除以光線消減屬性_LightCutoff。並用saturate鉗制相除結果在[0,1],saturate同時也在nDotL是負值時將值鉗為0。
將結果乘以陰影段數_ShadowBands並用round將結果四捨五入,然後除以陰影段數,這個結果在計算陰影時與取整的光照衰減數atten相乘,便可以得到陰影分段的視覺效果。這一步是最抽象的,引用部落格裡Harry的解釋,如果我需要4個陰影段的話,將nDotL運算後的值乘以4(這時候的值範圍是[0,4]),[0,4]範圍的值四捨五入的話就有0,1,2,3,4五種值,用這五種值除以陰影段數4就得到0、0。25、0。5、0。75、1這五種值。
也就是說這時diff的值這時候與法線和光照方向的點乘值nDotL正相關(符合phong光照模型漫反射計算經驗),同時當陰影段數為4時有0、0。25、0。5、0。75、1這五種值,而當我們在下面將diff值乘以取整的光照衰減數atten後(使得陰影值shadow與光照atten正相關),就可以得到分段式的陰影效果了。
注意UNITY_LIGHT_ATTENUATION(atten, i, worldPos)就是計算光照衰減的宏,其會自動宣告atten這個變數。
fixed3
normal
=
UnpackNormal
(
tex2D
(
_Normal
,
i
。
uv
。
xy
));
normal
=
normalize
(
half3
(
dot
(
i
。
TtoW0
。
xyz
,
normal
),
dot
(
i
。
TtoW1
。
xyz
,
normal
),
dot
(
i
。
TtoW2
。
xyz
,
normal
)));
half
nDotL
=
saturate
(
dot
(
normal
,
lightDir
));
half
diff
=
round
(
saturate
(
nDotL
/
_LightCutoff
)
*
_ShadowBands
)
/
_ShadowBands
;
UNITY_LIGHT_ATTENUATION
(
atten
,
i
,
worldPos
);
half
stepAtten
=
round
(
atten
);
half
shadow
=
diff
*
stepAtten
;
上圖可以看出分段式的過渡陰影、輪廓線、大塊的高光
(11)然後是高光specular的計算。用cg自帶的reflect函式計算出反向的反射光方向,用正向的反射光方向與視角方向點乘得vDotRefl(與phong模型相似)。然後取樣高光貼圖值與_Glossiness相乘賦值給smoothness(貼圖控制高光形狀,_Glossiness控制高光大小)。使用step函式來對高光值取0或1(也就得到了硬的成塊的高光效果)乘以_SpecularColor高光顏色,然後賦值給specular: