您當前的位置:首頁 > 遊戲

Shader學習分享:Unity製作卡通風格化Shader

作者:由 冠霖 發表于 遊戲時間:2021-04-06

之前偶然在80level看到一篇用表面著色器寫的卡通風格化Shader的教程部落格覺得挺有趣:

教程中的shader是用表面著色器(

surface shader

)寫的,我用了頂點片元著色器(

vertex-fragment shader

)復現一遍教程的思路,如有錯誤請大佬指正:

注:表面著色器是unity為了簡化shader製作而設計的一種封裝層級更高的著色器,其本質還是頂點片元著色器,表面著色器會自動處理座標空間轉換和光照計算這些相對繁瑣的操作,但也相應地失去了效果和效能的更強控制。個人理解是個人專案可以用表面著色器,商業專案或者需要更自定義的效果最好用頂點片元著色器。

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

}

Shader學習分享:Unity製作卡通風格化Shader

(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

Shader學習分享:Unity製作卡通風格化Shader

上圖可以看出分段式的過渡陰影、輪廓線、大塊的高光

(11)然後是高光specular的計算。用cg自帶的reflect函式計算出反向的反射光方向,用正向的反射光方向與視角方向點乘得vDotRefl(與phong模型相似)。然後取樣高光貼圖值與_Glossiness相乘賦值給smoothness(貼圖控制高光形狀,_Glossiness控制高光大小)。使用step函式來對高光值取0或1(也就得到了硬的成塊的高光效果)乘以_SpecularColor高光顏色,然後賦值給specular:

step(a,b)函式:當b

float3

refl

=

reflect

lightDir

normal

);

float

vDotRefl

=

dot

viewDir

-

refl

);

float

smoothness

=

tex2D

_SpecularMap

i

uv

zw

)。

x

*

_Glossiness

float3

specular

=

_SpecularColor

rgb

*

step

1

-

smoothness

vDotRefl

);

Shader學習分享:Unity製作卡通風格化Shader

reflect函式計算,是以真實的物理模型為方向標準

Shader學習分享:Unity製作卡通風格化Shader

cg的方向矢量表示,都是以表面上點為起點

(12)接下來就是

邊緣輪廓線

的計算。用視角方向viewDir和法線方向normal點乘(類似菲涅耳反射Fresnel reflection),然後使用step函式使得_RimSize可以控制輪廓線的粗細。

然後對主貼圖進行取樣並與_Color屬性相乘賦值給albedo,加上specular高光,與_LightColor0環境光變數(從Lighting。cginc得到)相乘,結果賦值給col。接著進入是否開啟SHADOWED_RIM功能的條件判定語句,如果啟用功能的話rim將會與shadow值相乘(即會被shadow影響)後再與col * shadow的值相加。可以看到至此已經基本完成了漫反射、高光、輪廓線的計算:

float3

rim

=

_RimColor

*

step

1

-

_RimSize

1

-

saturate

dot

viewDir

normal

)));

fixed4

albedo

=

tex2D

_MainTex

i

uv

xy

*

_Color

half3

col

=

albedo

rgb

+

specular

*

_LightColor0

half4

c

#ifdef SHADOWED_RIM

c

rgb

=

col

+

rim

*

shadow

#else

c

rgb

=

col

*

shadow

+

rim

#endif

(13)最後用albedo的alpha通道賦值c的alpha通道,再加上_Emission自發光影響以在結果上做一個最終的方便的調整,ENDCG結束cg程式碼段,在SubShader後加一個Diffuse的FallBack後路(如果顯示卡不支援所有SubShader的話就回退到預設的Diffuse Shader,這個shader也包含了預設的陰影投射pass):

c

a

=

albedo

a

c

rgb

=

c

rgb

+

albedo

rgb

*

_Emission

rgb

return

c

}

ENDCG

}

}

FallBack

“Diffuse”

}

標簽: 著色器  光照  高光  頂點  陰影