轉(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部分,就留給你們自己寫吧!