【譯】Unity3D Shader 新手教程(6/6) —— 更好的卡通Shader (轉(zhuǎn))

轉(zhuǎn)自:http://www.cnblogs.com/polobymulberry/p/4395127.html

動(dòng)機(jī)

如果你想了解以下幾件事,我建議你閱讀以下這篇教程:

想知道如何寫一個(gè)multipass的toon shader。

在shader中學(xué)習(xí)更多不同參考坐標(biāo)系(空間space)以及其作用。

深入學(xué)習(xí)一個(gè)實(shí)用的fragment shader。

學(xué)習(xí)矩陣相乘和Unity內(nèi)建矩陣的使用。

該教程比第五篇教程更實(shí)用。

準(zhǔn)備工作

為了實(shí)現(xiàn)一個(gè)描邊的toon shader,我們需要做的是:

為模型描邊。

第四篇文章中的介紹的toon shader(使用的是surface shader)移植到vertex&fragment shader中。

描邊

有很多方法進(jìn)行描邊,在第四篇文章中,我們使用了rim lighting(邊緣光照)來(lái)給我們?nèi)宋锛由厦柽呅Ч,F(xiàn)在我們采用另一種方法,額外使用一個(gè)Pass改善已有的描邊效果。

不同于之前描邊效果的實(shí)現(xiàn),在這篇教程中,你可以將你看不到的模型部分(比如背面)放大一些,再渲染成全黑,這樣也是可以實(shí)現(xiàn)描邊效果的。這種方法可以將原模型的正面完好無(wú)損呈現(xiàn)出來(lái)。

所以我們首先試著:

單獨(dú)寫一個(gè)僅僅用來(lái)繪制模型背面的Pass。

擴(kuò)展模型背面的頂點(diǎn),使其看起來(lái)變大了一些。

下面這個(gè)Pass就是用來(lái)僅僅繪制模型背面(Cull Front,剔除正面的多邊形):

Pass{

CullFront

LightingOff

}

現(xiàn)在讓我們考慮最簡(jiǎn)單的部分 — 將傳入該P(yáng)ass的所有像素值繪制成黑色!

CGPROGRAM

#pragmavertex vert

#pragmafragment frag

#include"UnityCG.cginc"

//剩下的功能在此處實(shí)現(xiàn)

float4 frag(v2f IN):COLOR

{

returnfloat4(0,0,0,1);

}

ENDCG

該fragment函數(shù)返回float4(0,0,0,1) — 全黑。

現(xiàn)在為我們的shader添加輸入結(jié)構(gòu)體。我們利用該結(jié)構(gòu)體(包含vertex和normal)來(lái)將我們模型的每個(gè)頂點(diǎn)沿法向進(jìn)行延伸擴(kuò)展 — 該頂點(diǎn)是背面面片上的點(diǎn)。所以我們輸入結(jié)構(gòu)體必須含有頂點(diǎn)位置vertex和頂點(diǎn)法向normal信息。

structa2v

{

float4 vertex:POSITION;

float3 normal:NORMAL;

};

structv2f

{

float4 pos:POSITION;

};

接下來(lái)我們?cè)赑roperties代碼區(qū)域定義一個(gè)_Outline屬性值,范圍為0.0~1.0,我們?cè)贑G代碼中定義一個(gè)相同的變量float _Outline。

最后我們?cè)趘ertex函數(shù)vert中延著法向normal伸展頂點(diǎn):

float_Outline;

v2f vert(a2v v)

{

v2f o;

o.pos=mul(UNITY_MATRIX_MVP,v.vertex+(float4(v.normal,0)*_Outline));

returno;

}

我們所做的就是將v.vertex沿著normal伸展了_Outline比例大小,然后使用Unity內(nèi)置的矩陣UNITY_MATRIX_MVP將結(jié)果轉(zhuǎn)換到投影空間(projection space)。

矩陣在shader中用來(lái)轉(zhuǎn)化很多事情。我們可以從下圖看出,一個(gè)4x4的矩陣乘上一個(gè)4x1的矩陣,得到還是一個(gè)4*1的矩陣。Unity中有很多預(yù)定好的矩陣,我們可以使用這些矩陣得到各種空間坐標(biāo)系的轉(zhuǎn)換。

目前你的代碼應(yīng)該保證像下面這樣了(注意這是在第五部分教程的基礎(chǔ)上添加的代碼):

Pass{

//剔除模型正面,只渲染背面

CullFront

LightingOff

CGPROGRAM

#pragmavertex vert

#pragmafragment frag

#include"UnityCG.cginc"

structa2v

{

float4 vertex:POSITION;

float3 normal:NORMAL;

};

structv2f

{

float4 pos:POSITION;

}

float_Outline;

v2f vert(a2v v)

{

v2f o;

o.pos=mul(UNITY_MATRIX_MVP,v.vertex+(float4(v.normal,0)*_Outline));

returno;

}

floatfrag(v2f IN):COLOR

{

returnfloat4(0,0,0,1)

}

ENDCG

}

看上去好像有點(diǎn)效果,但是仔細(xì)看他的嘴巴,我們可以看到是有很大問(wèn)題。這是因?yàn)閷?shí)現(xiàn)邊緣效果的Pass是可以寫入深度緩存的。所以在有些情況下,模型正面是無(wú)法正常繪制的。

拿此處的嘴舉例,此處的嘴巴的上嘴唇是屬于正面的,而下嘴唇是反面(多邊形方向?yàn)槟鏁r(shí)針)。所以Cull Front后會(huì)剔除上嘴唇,保留下嘴唇。而下嘴唇的法向很明顯差不多是朝上的,所以在vert函數(shù)中會(huì)在下嘴唇上方產(chǎn)生這種黑條狀的面片。又因?yàn)槲覀兪强梢詫懭肷疃染彺娴?,所以?huì)將這黑色面片寫入到深度緩存,而這黑色面片恰好在嘴唇前面,所以嘴唇正面在繪制時(shí)通過(guò)不了深度測(cè)試,只留下這黑色的面片。

自然而然地我們肯定能想到,讓這個(gè)黑色面片不進(jìn)行深度緩存測(cè)試不就行了。下面這幅圖就是在該P(yáng)ass中關(guān)閉Z buffer測(cè)試的結(jié)果。

使用下面這段代碼:

Pass{

CullFront

LightingOff

ZWriteOff

關(guān)閉Z buffer測(cè)試后,哪些多余的黑色面片確實(shí)不存在了??墒怯钟幸粋€(gè)新問(wèn)題出現(xiàn)了。因?yàn)楹谏嫫冀K通過(guò)不了Z Buffer測(cè)試,所以模型本身的面片會(huì)覆掉這些黑色面片。我們看到下面這張圖,前面的模型擋住了后面模型產(chǎn)生的黑色邊緣。這又不是我們想要的。

現(xiàn)在我們大概知道問(wèn)題的本質(zhì)就是黑色面片是沿著法向擴(kuò)展了一定長(zhǎng)度,其Z值也就發(fā)生了變化。如果我們特意處理下Z值,使其產(chǎn)生的背面的黑色面片的Z值小一點(diǎn),也就是離視點(diǎn)遠(yuǎn)一些,而不是像一個(gè)新產(chǎn)生的模型一樣附在物體表面。這樣的話,對(duì)于邊緣效果,其主要作用的將是x和y分量,而不是z分量。

現(xiàn)在回到我們的vertex函數(shù),然后做一些矩陣變換。

將背面產(chǎn)生的黑色面片在Z方向壓扁

首先迎接的挑戰(zhàn)是我們的頂點(diǎn)和法向是在模型空間 — 但是我們要將其轉(zhuǎn)換到視空間(相機(jī)為原點(diǎn)的空間,還未經(jīng)過(guò)投影變換),這是因?yàn)樵谝暱臻g中,z軸指向相機(jī),也就是模型z值恰好表示模型距離相機(jī)的遠(yuǎn)近。

下面介紹幾個(gè)Unity內(nèi)建的矩陣。

首先我們不再將頂點(diǎn)轉(zhuǎn)換到投影空間中,而是將頂點(diǎn)先轉(zhuǎn)換到視空間中 — 這很簡(jiǎn)單,僅僅需要使用一個(gè)不同的矩陣。

然后我們要將對(duì)應(yīng)法向值轉(zhuǎn)化到視空間中 — 這里使用了一個(gè)trick,因?yàn)閷⒎ㄏ驈哪P涂臻g轉(zhuǎn)換到視空間不能簡(jiǎn)單使用矩陣UNITY_MATRIX_MV。得使用UNITY_MATRIX_MV的逆轉(zhuǎn)置矩陣UNITY_MATRIX_IT_MV(其中IT表示Inverse Transpose)。直接將法向乘以UNITY_MATRIX_MV得到的結(jié)果將不再垂直原來(lái)的面片。本質(zhì)原因其實(shí)是因?yàn)轫旤c(diǎn)是一個(gè)點(diǎn),而法向是一個(gè)方向向量。

比如下圖以及下面的推導(dǎo)公式:

所以我們所要做的就是:

將頂點(diǎn)轉(zhuǎn)化到視空間中?!?pos = mul( UNITY_MATRIX_MV, v.vertex);

將法向轉(zhuǎn)化到視空間中。— normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);

修正法向量的z分量為某個(gè)特定最小值 —normal.z= -0.4(這樣黑色邊緣延伸擴(kuò)展就會(huì)沿模型背面擴(kuò)展,不會(huì)出現(xiàn)在模型前面了)

重新單位化法向(因?yàn)樵谥暗牟襟E中,我們改變了法向,破壞了它的單位長(zhǎng)度)

使用_Outline縮放法向長(zhǎng)度,然后加到將頂點(diǎn)位置沿法向平移這么長(zhǎng)。

將頂點(diǎn)轉(zhuǎn)化到投影空間中。

所有代碼看起來(lái)就像下面這樣:

v2f vert(a2v v)

{

v2f o;

float4 pos=mul(UNITY_MATRIX_MV,v.vertex);

float3 normal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);

normal.z=-0.4;

pos=pos+float4(normalize(normal),0)*_Outline;

o.pos=mul(UNITY_MATRIX_P,pos);

returno;

}

注意Unity中使用的矩陣是4x4 — 但是我們的法向是float3類型 — 我們必須將矩陣轉(zhuǎn)化為3x3 — (float3x3)UNITY_MATRIX_IT_MV,否則我們會(huì)在Unity的控制臺(tái)得到很多錯(cuò)誤。

如果我們使用ZWrite On — 效果看起來(lái)像下面這樣:

這種效果對(duì)我們已經(jīng)足夠了。

卡通化

剩下的就是將我們之前使用表面著色器制作的Toon Shader應(yīng)用到vertex&fragment shader中。

首先我們像教程第四部分那樣定義一個(gè)_Ramp屬性值,并相應(yīng)的定義sampler2D _Ramp。

使用ramp texture(漸變紋理) — 然后我們添加一個(gè)_ColorMerge屬性變量(一個(gè)float類型的值),利用其降低模型顏色的種類。

我們改變教程第五部分的fragment函數(shù) — 就像下面這樣:

float4 frag(v2f i):COLOR

{

//根據(jù)uv坐標(biāo)從紋理中獲得對(duì)應(yīng)像素值

float4 c=tex2D(_MainTex,i.uv);

//降低顏色種類

c.rgb=(floor(c.rgb*_ColorMerge)/_ColorMerge);

//從bump紋理中得到對(duì)應(yīng)像素的法向

float3 n=UnpackNormal(tex2D(_Bump,i.uv2));

//獲得漫射光顏色

float3 lightColor=UNITY_LIGHTMODEL_AMBIENT.xyz;

//計(jì)算出光源距離

floatlengthSq=dot(i.lightDirection,i.lightDirection);

//根據(jù)計(jì)算出的光源位置計(jì)算光強(qiáng)的衰減

floatatten=1.0/(1.0+lengthSq);

//光的入射角

floatdiff=saturate(dot(n,normalize(i.lightDirection)));

//利用漸變紋理

diff=tex2D(_Ramp,float2(diff,0.5));

//根據(jù)入射角,光衰減得到最終光照亮度

lightColor+=_LightColor0.rgb*(diff*atten);

//將光照亮度與本身顏色相乘,得到最終顏色

c.rgb=lightColor*c.rgb*2;

returnc;

}

我們所要做的就是利用_MainTex紋理進(jìn)行采樣,然后降低顏色種類,最后使用漸變紋理獲得的數(shù)值作為光強(qiáng)。

下圖使我們最終的效果:

完整的源碼在這里。

對(duì)于其他光照的ForwardAdd部分,就留給你們自己寫吧!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容