轉自:http://www.cnblogs.com/polobymulberry/p/4395127.html
動機
如果你想了解以下幾件事,我建議你閱讀以下這篇教程:
想知道如何寫一個multipass的toon shader。
在shader中學習更多不同參考坐標系(空間space)以及其作用夕晓。
深入學習一個實用的fragment shader。
學習矩陣相乘和Unity內建矩陣的使用厢破。
該教程比第五篇教程更實用。
準備工作
為了實現(xiàn)一個描邊的toon shader治拿,我們需要做的是:
為模型描邊摩泪。
將第四篇文章中的介紹的toon shader(使用的是surface shader)移植到vertex&fragment shader中。
描邊
有很多方法進行描邊劫谅,在第四篇文章中见坑,我們使用了rim lighting(邊緣光照)來給我們人物加上描邊效果∪侣樱現(xiàn)在我們采用另一種方法,額外使用一個Pass改善已有的描邊效果荞驴。
不同于之前描邊效果的實現(xiàn)不皆,在這篇教程中,你可以將你看不到的模型部分(比如背面)放大一些熊楼,再渲染成全黑霹娄,這樣也是可以實現(xiàn)描邊效果的。這種方法可以將原模型的正面完好無損呈現(xiàn)出來孙蒙。
所以我們首先試著:
單獨寫一個僅僅用來繪制模型背面的Pass项棠。
擴展模型背面的頂點悲雳,使其看起來變大了一些挎峦。
下面這個Pass就是用來僅僅繪制模型背面(Cull Front,剔除正面的多邊形):
Pass{
CullFront
LightingOff
}
現(xiàn)在讓我們考慮最簡單的部分 — 將傳入該Pass的所有像素值繪制成黑色!
CGPROGRAM
#pragmavertex vert
#pragmafragment frag
#include"UnityCG.cginc"
//剩下的功能在此處實現(xiàn)
float4 frag(v2f IN):COLOR
{
returnfloat4(0,0,0,1);
}
ENDCG
該fragment函數(shù)返回float4(0,0,0,1) — 全黑合瓢。
現(xiàn)在為我們的shader添加輸入結構體坦胶。我們利用該結構體(包含vertex和normal)來將我們模型的每個頂點沿法向進行延伸擴展 — 該頂點是背面面片上的點。所以我們輸入結構體必須含有頂點位置vertex和頂點法向normal信息晴楔。
structa2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
structv2f
{
float4 pos:POSITION;
};
接下來我們在Properties代碼區(qū)域定義一個_Outline屬性值顿苇,范圍為0.0~1.0,我們在CG代碼中定義一個相同的變量float _Outline税弃。
最后我們在vertex函數(shù)vert中延著法向normal伸展頂點:
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內置的矩陣UNITY_MATRIX_MVP將結果轉換到投影空間(projection space)。
矩陣在shader中用來轉化很多事情则果。我們可以從下圖看出幔翰,一個4x4的矩陣乘上一個4x1的矩陣,得到還是一個4*1的矩陣西壮。Unity中有很多預定好的矩陣遗增,我們可以使用這些矩陣得到各種空間坐標系的轉換。
目前你的代碼應該保證像下面這樣了(注意這是在第五部分教程的基礎上添加的代碼):
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
}
看上去好像有點效果做修,但是仔細看他的嘴巴,我們可以看到是有很大問題抡草。這是因為實現(xiàn)邊緣效果的Pass是可以寫入深度緩存的饰及。所以在有些情況下,模型正面是無法正常繪制的康震。
拿此處的嘴舉例旋炒,此處的嘴巴的上嘴唇是屬于正面的,而下嘴唇是反面(多邊形方向為逆時針)签杈。所以Cull Front后會剔除上嘴唇瘫镇,保留下嘴唇鼎兽。而下嘴唇的法向很明顯差不多是朝上的,所以在vert函數(shù)中會在下嘴唇上方產(chǎn)生這種黑條狀的面片铣除。又因為我們是可以寫入深度緩存的谚咬,所以會將這黑色面片寫入到深度緩存,而這黑色面片恰好在嘴唇前面尚粘,所以嘴唇正面在繪制時通過不了深度測試择卦,只留下這黑色的面片。
自然而然地我們肯定能想到郎嫁,讓這個黑色面片不進行深度緩存測試不就行了秉继。下面這幅圖就是在該Pass中關閉Z buffer測試的結果。
使用下面這段代碼:
Pass{
CullFront
LightingOff
ZWriteOff
關閉Z buffer測試后泽铛,哪些多余的黑色面片確實不存在了尚辑。可是又有一個新問題出現(xiàn)了盔腔。因為黑色面片始終通過不了Z Buffer測試杠茬,所以模型本身的面片會覆掉這些黑色面片。我們看到下面這張圖弛随,前面的模型擋住了后面模型產(chǎn)生的黑色邊緣瓢喉。這又不是我們想要的。
現(xiàn)在我們大概知道問題的本質就是黑色面片是沿著法向擴展了一定長度舀透,其Z值也就發(fā)生了變化栓票。如果我們特意處理下Z值,使其產(chǎn)生的背面的黑色面片的Z值小一點愕够,也就是離視點遠一些走贪,而不是像一個新產(chǎn)生的模型一樣附在物體表面。這樣的話链烈,對于邊緣效果厉斟,其主要作用的將是x和y分量,而不是z分量强衡。
現(xiàn)在回到我們的vertex函數(shù)擦秽,然后做一些矩陣變換。
將背面產(chǎn)生的黑色面片在Z方向壓扁
首先迎接的挑戰(zhàn)是我們的頂點和法向是在模型空間 — 但是我們要將其轉換到視空間(相機為原點的空間漩勤,還未經(jīng)過投影變換)感挥,這是因為在視空間中,z軸指向相機越败,也就是模型z值恰好表示模型距離相機的遠近触幼。
下面介紹幾個Unity內建的矩陣。
首先我們不再將頂點轉換到投影空間中究飞,而是將頂點先轉換到視空間中 — 這很簡單置谦,僅僅需要使用一個不同的矩陣堂鲤。
然后我們要將對應法向值轉化到視空間中 — 這里使用了一個trick,因為將法向從模型空間轉換到視空間不能簡單使用矩陣UNITY_MATRIX_MV媒峡。得使用UNITY_MATRIX_MV的逆轉置矩陣UNITY_MATRIX_IT_MV(其中IT表示Inverse Transpose)瘟栖。直接將法向乘以UNITY_MATRIX_MV得到的結果將不再垂直原來的面片。本質原因其實是因為頂點是一個點谅阿,而法向是一個方向向量半哟。
比如下圖以及下面的推導公式:
所以我們所要做的就是:
將頂點轉化到視空間中∏┎停— pos = mul( UNITY_MATRIX_MV, v.vertex);
將法向轉化到視空間中寓涨。— normal = mul( (float3x3)UNITY_MATRIX_IT_MV, v.normal);
修正法向量的z分量為某個特定最小值 —normal.z= -0.4(這樣黑色邊緣延伸擴展就會沿模型背面擴展氯檐,不會出現(xiàn)在模型前面了)
重新單位化法向(因為在之前的步驟中戒良,我們改變了法向,破壞了它的單位長度)
使用_Outline縮放法向長度男摧,然后加到將頂點位置沿法向平移這么長蔬墩。
將頂點轉化到投影空間中译打。
所有代碼看起來就像下面這樣:
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類型 — 我們必須將矩陣轉化為3x3 — (float3x3)UNITY_MATRIX_IT_MV耗拓,否則我們會在Unity的控制臺得到很多錯誤。
如果我們使用ZWrite On — 效果看起來像下面這樣:
這種效果對我們已經(jīng)足夠了奏司。
卡通化
剩下的就是將我們之前使用表面著色器制作的Toon Shader應用到vertex&fragment shader中乔询。
首先我們像教程第四部分那樣定義一個_Ramp屬性值,并相應的定義sampler2D _Ramp韵洋。
使用ramp texture(漸變紋理) — 然后我們添加一個_ColorMerge屬性變量(一個float類型的值)竿刁,利用其降低模型顏色的種類。
我們改變教程第五部分的fragment函數(shù) — 就像下面這樣:
float4 frag(v2f i):COLOR
{
//根據(jù)uv坐標從紋理中獲得對應像素值
float4 c=tex2D(_MainTex,i.uv);
//降低顏色種類
c.rgb=(floor(c.rgb*_ColorMerge)/_ColorMerge);
//從bump紋理中得到對應像素的法向
float3 n=UnpackNormal(tex2D(_Bump,i.uv2));
//獲得漫射光顏色
float3 lightColor=UNITY_LIGHTMODEL_AMBIENT.xyz;
//計算出光源距離
floatlengthSq=dot(i.lightDirection,i.lightDirection);
//根據(jù)計算出的光源位置計算光強的衰減
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紋理進行采樣,然后降低顏色種類副编,最后使用漸變紋理獲得的數(shù)值作為光強负甸。
下圖使我們最終的效果:
完整的源碼在這里。
對于其他光照的ForwardAdd部分痹届,就留給你們自己寫吧呻待!