Unity3D Shader入门指南(二)
關(guān)于本系列
這是Unity3D Shader入門(mén)指南系列的第二篇,本系列面向的對(duì)象是新接觸Shader開(kāi)發(fā)的Unity3D使用者,因?yàn)槲冶旧碜约阂彩荢hader初學(xué)者,因此可能會(huì)存在錯(cuò)誤或者疏漏,如果您在Shader開(kāi)發(fā)上有所心得,很歡迎并懇請(qǐng)您指出文中紕漏,我會(huì)盡快改正。在之前的開(kāi)篇中介紹了一些Shader的基本知識(shí),包括ShaderLab的基本結(jié)構(gòu)和語(yǔ)法,以及簡(jiǎn)單逐句地講解了一個(gè)基本的shader。在具有這些基礎(chǔ)知識(shí)后,閱讀簡(jiǎn)單的shader應(yīng)該不會(huì)有太大問(wèn)題,在繼續(xù)教程之前簡(jiǎn)單閱讀一下Unity的Surface Shader Example,以檢驗(yàn)?zāi)欠裾莆樟松弦还?jié)的內(nèi)容。如果您對(duì)閱讀大部分示例Shader并沒(méi)有太大問(wèn)題,可以正確地指出Shader的結(jié)構(gòu),聲明和使用的話,就說(shuō)明您已經(jīng)準(zhǔn)備好繼續(xù)閱讀本節(jié)的內(nèi)容了。
法線貼圖(Normal Mapping)
法線貼圖是凸凹貼圖(Bump mapping)的一種常見(jiàn)應(yīng)用,簡(jiǎn)單說(shuō)就是在不增加模型多邊形數(shù)量的前提下,通過(guò)渲染暗部和亮部的不同顏色深度,來(lái)為原來(lái)的貼圖和模型增加視覺(jué)細(xì)節(jié)和真實(shí)效果。簡(jiǎn)單原理是在普通的貼圖的基礎(chǔ)上,再另外提供一張對(duì)應(yīng)原來(lái)貼圖的,可以表示渲染濃淡的貼圖。通過(guò)將這張附加的表示表面凸凹的貼圖的因素于實(shí)際的原貼圖進(jìn)行運(yùn)算后,可以得到新的細(xì)節(jié)更加豐富富有立體感的渲染效果。在本節(jié)中,我們將首先實(shí)現(xiàn)一個(gè)法線貼圖的Shader,然后對(duì)Unity Shader的光照模型進(jìn)行一些討論,并實(shí)現(xiàn)一個(gè)自定義的光照模型。最后再通過(guò)更改shader模擬一個(gè)石頭上的積雪效果,并對(duì)模型頂點(diǎn)進(jìn)行一些修改使積雪效果看起來(lái)比較真實(shí)。在本節(jié)結(jié)束的時(shí)候,我們就會(huì)有一個(gè)比較強(qiáng)大的可以滿足一些真實(shí)開(kāi)發(fā)工作時(shí)可用的shader了,而且更重要的是,我們將會(huì)掌握它是如何被創(chuàng)造出來(lái)的。
關(guān)于法線貼圖的效果圖,可以對(duì)比看看下面。模型面數(shù)為500,左側(cè)只使用了簡(jiǎn)單的Diffuse著色,右側(cè)使用了法線貼圖。比較兩張圖片不難發(fā)現(xiàn),使用了法線貼圖的石頭在暗部和亮部都有著更好的表現(xiàn)。整體來(lái)說(shuō),凸凹感比Diffuse的結(jié)果增強(qiáng)許多,石頭看起來(lái)更真實(shí)也更具有質(zhì)感。
本節(jié)中需要用到的上面的素材可以在這里下載,其中包括上面的石塊的模型,一張貼圖以及對(duì)應(yīng)的法線貼圖。將下載的package導(dǎo)入到工程中,并新建一個(gè)material,使用簡(jiǎn)單的Diffuse的Shader(比如上一節(jié)我們實(shí)現(xiàn)的),再加上一個(gè)合適的平行光光源,就可以得到我們左圖的效果。另外,本節(jié)以及以后都會(huì)涉及到一些Unity內(nèi)建的Shader的內(nèi)容,比如一些標(biāo)準(zhǔn)常用函數(shù)和常量定義等,相關(guān)內(nèi)容可以在Unity的內(nèi)建Shader中找到,內(nèi)建Shader可以在Unity下載頁(yè)面的版本右側(cè)找到。
接下來(lái)我們實(shí)現(xiàn)法線貼圖。在實(shí)現(xiàn)之前,我們先簡(jiǎn)單地稍微多了解一些法線貼圖的基本知識(shí)。大多數(shù)法線圖一般都和下面的圖類似,是一張以藍(lán)紫色為主的圖。這張法線圖其實(shí)是一張RGB貼圖,其中紅,綠,藍(lán)三個(gè)通道分別表示由高度圖轉(zhuǎn)換而來(lái)的該點(diǎn)的法線指向:Nx、Ny、Nz。在其中絕大部分點(diǎn)的法線都指向z方向,因此圖更偏向于藍(lán)色。在shader進(jìn)行處理時(shí),我們將光照與該點(diǎn)的法線值進(jìn)行點(diǎn)積后即可得到在該光線下應(yīng)有的明暗特性,再將其應(yīng)用到原圖上,即可反應(yīng)在一定光照環(huán)境下物體的凹凸關(guān)系了。關(guān)于法向貼圖的更多信息,可以參考wiki上的相關(guān)條目。
回到正題,我們現(xiàn)在考慮的主要是Shader入門(mén),而不是圖像學(xué)的原理。再上一節(jié)我們寫(xiě)的Shader的基礎(chǔ)上稍微做一些修改,就可以得到適應(yīng)并完成法線貼圖渲染的新Shader。新加入的部分進(jìn)行了編號(hào)并在之后進(jìn)行說(shuō)明。
Shader "Custom/Normal Mapping" { Properties {_MainTex ("Base (RGB)", 2D) = "white" {}//1 _Bump ("Bump", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; //2 sampler2D _Bump; struct Input { float2 uv_MainTex; //3 float2 uv_Bump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); //4 o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }現(xiàn)在保存并且編譯這個(gè)Shader,創(chuàng)建新的material并使用這個(gè)shader,將石頭的材質(zhì)貼圖和法線圖分別拖放到Base和Bump里,再將其應(yīng)用到石頭模型上,應(yīng)該就可以看到右側(cè)圖的效果了。
光照模型
在我們之前的看到的Shader中(其實(shí)也就上一節(jié)的基本diffuse和這里的normal mapping),都只使用了Lambert的光照模型(#pragma surface surf Lambert),這是一個(gè)很經(jīng)典的漫反射模型,光強(qiáng)與入射光的方向和反射點(diǎn)處表面法向夾角的余弦成正比。關(guān)于Lambert和漫反射的一些詳細(xì)的計(jì)算和推論,可以參看wiki(Lambert,漫反射)或者其他地方的介紹。一句話的簡(jiǎn)單解釋就是一個(gè)點(diǎn)的反射光強(qiáng)是和該點(diǎn)的法線向量和入射光向量和強(qiáng)度和夾角有關(guān)系的,其結(jié)果就是這兩個(gè)向量的點(diǎn)積。既然已經(jīng)知道了光照計(jì)算的原理,我們先來(lái)看看如何實(shí)現(xiàn)一個(gè)自己的光照模型吧。
在剛才的Shader上進(jìn)行如下修改。
- 首先將原來(lái)的#pragma行改為這樣
- 然后在SubShader塊中添加如下代碼
- 最后保存,回到Unity。Shader將編譯,如果一切正常,你將不會(huì)看到新的shader和之前的在材質(zhì)表現(xiàn)上有任何不同。但是事實(shí)上我們現(xiàn)在的shader已經(jīng)與Unity內(nèi)建的diffuse光照模型撇清了關(guān)系,而在使用我們自己設(shè)定的光照模型了。
喵的,這些代碼都干了些什么!相信你一定會(huì)有這樣的疑惑...沒(méi)問(wèn)題,沒(méi)有疑惑的話那就不叫初學(xué)了,還是一行行講來(lái)。首先正像我們上一篇所說(shuō),#pragma語(yǔ)句在這里聲明了接下來(lái)的Shader的類型,計(jì)算調(diào)用的方法名,以及指定光照模型。在之前我們一直指定Lambert為光照模型,而現(xiàn)在我們將其換為了CustomDiffuse。
接下來(lái)添加的代碼是計(jì)算光照的實(shí)現(xiàn)。shader中對(duì)于方法的名稱有著比較嚴(yán)格的約定,想要?jiǎng)?chuàng)建一個(gè)光照模型,首先要做的是按照規(guī)則聲明一個(gè)光照計(jì)算的函數(shù)名字,即Lighting<Your Chosen Name>。對(duì)于我們的光照模型CustomDiffuse,其計(jì)算函數(shù)的名稱自然就是LightingCustomDiffuse了。光照模型的計(jì)算是在surf方法的表面顏色之后,根據(jù)輸入的光照條件來(lái)對(duì)原來(lái)的顏色在這種光照下的表現(xiàn)進(jìn)行計(jì)算,最后輸出新的顏色值給渲染單元完成在屏幕的繪制。
也許你已經(jīng)猜到了,我們之前用的Lambert光照模型是不是也有一個(gè)名字叫LightingLambert的光照計(jì)算函數(shù)呢?Bingo。在Unity的內(nèi)建Shader中,有一個(gè)Lighting.cginc文件,里面就包含了LightingLambert的實(shí)現(xiàn)。也許你也注意到了,我們所實(shí)現(xiàn)的LightingCustomDiffuse的內(nèi)容現(xiàn)在和Unity內(nèi)建中的LightingLambert是完全一樣的,這也就是使用新的shader的原來(lái)視覺(jué)上沒(méi)有區(qū)別的原因,因?yàn)閷?shí)現(xiàn)確實(shí)是完全一樣的。
首先來(lái)看輸入量,SurfaceOutput s這個(gè)就是經(jīng)過(guò)表面計(jì)算函數(shù)surf處理后的輸出,我們講對(duì)其上的點(diǎn)根據(jù)光線進(jìn)行處理,fixed3 lightDir是光線的方向,fixed atten表示光衰減的系數(shù)。在計(jì)算光照的代碼中,我們先將輸入的s的法線值(在Normal mapping中的話這個(gè)值已經(jīng)是法線圖中的對(duì)應(yīng)量了)和輸入光線進(jìn)行點(diǎn)積(dot函數(shù)是CG中內(nèi)置的數(shù)學(xué)函數(shù),希望你還記得,可以參考這里)。點(diǎn)積的結(jié)果在-1至1之間,這個(gè)值越大表示法線與光線間夾角越小,這個(gè)點(diǎn)也就應(yīng)該越亮。之后使用max來(lái)將這個(gè)系數(shù)結(jié)果限制在0到1之間,是為了避免負(fù)數(shù)情況的存在而導(dǎo)致最終計(jì)算的顏色變?yōu)樨?fù)數(shù),輸出一團(tuán)黑,一般來(lái)說(shuō)這是我們不愿意看到的。接下來(lái)我們將surf輸出的顏色與光線的顏色_LightColor0.rgb(由Unity根據(jù)場(chǎng)景中的光源得到的,它在Lighting.cginc中有聲明)進(jìn)行乘積,然后再與剛才計(jì)算的光強(qiáng)系數(shù)和輸入的衰減系數(shù)相乘,最后得到在這個(gè)光線下的顏色輸出(關(guān)于difLight * atten * 2中為什么有個(gè)乘2,這是一個(gè)歷史遺留問(wèn)題,主要是為了進(jìn)行一些光強(qiáng)補(bǔ)償,可以參見(jiàn)這里的討論)。
在了解了基本實(shí)現(xiàn)方式之后,我們可以看看做一些修改玩玩兒。最簡(jiǎn)單的比如將這個(gè)Lambert模型改亮一些,比如換成Half Lambert模型。Half Lambert是由Valve創(chuàng)造的可以使物體在低光線條件下增亮的技術(shù),最早被用于半條命(Half Life)中以避免在低光下物體的走形。簡(jiǎn)單說(shuō)就是把光強(qiáng)系數(shù)先取一半,然后在加0.5,代碼如下:
inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) { float difLight = dot (s.Normal, lightDir);float hLambert = difLight * 0.5 + 0.5; float4 col; col.rgb = s.Albedo * _LightColor0.rgb * (hLambert * atten * 2); col.a = s.Alpha; return col; }這樣一來(lái),原來(lái)光強(qiáng)0的點(diǎn),現(xiàn)在對(duì)應(yīng)的值變?yōu)榱?.5,而原來(lái)是1的地方現(xiàn)在將保持為1。也就是說(shuō)模型貼圖的暗部被增強(qiáng)變亮了,而亮部基本保持和原來(lái)一樣,防止過(guò)曝。使用Half Lambert前后的效果圖如下,注意最右側(cè)石頭下方的陰影處細(xì)節(jié)更加明顯了,而這一切都只是視覺(jué)效果的改變,不涉及任何貼圖和模型的變化。
表面貼圖的追加效果
OK,對(duì)于光線和自定義光照模型的討論暫時(shí)到此為止,因?yàn)槿绻归_(kāi)的話這將會(huì)一個(gè)龐大的圖形學(xué)和經(jīng)典光學(xué)的話題了。我們回到Shader,并且一起實(shí)現(xiàn)一些激動(dòng)人心的效果吧。比如,在你的游戲場(chǎng)景中有一幕是雪地場(chǎng)景,而你希望做一些石頭上白雪皚皚的覆蓋效果,應(yīng)該怎么辦呢?難道讓你可愛(ài)的3D設(shè)計(jì)師再去出一套覆雪的貼圖然后使用新的貼圖?當(dāng)然不,不是不能,而是不該。因?yàn)樾碌馁N圖不僅會(huì)增大項(xiàng)目的資源包體積,更會(huì)增大之后修改和維護(hù)的難度,想想要是有好多石頭需要實(shí)現(xiàn)同樣的覆雪效果,或者是要隨著游戲時(shí)間堆積的雪逐漸變多的話,你應(yīng)該怎么辦?難道讓設(shè)計(jì)師再把所有的石頭貼圖都蓋上雪,然后再按照雪的厚度出5套不同的貼圖么?相信我,他們會(huì)瘋的。
于是,我們考慮用Shader來(lái)完成這件工作吧!先考慮下我們需要什么,積雪效果的話,我們需要積雪等級(jí)(用來(lái)表示積雪量),雪的顏色,以及積雪的方向。基本思路和實(shí)現(xiàn)自定義光照模型類似,通過(guò)計(jì)算原圖的點(diǎn)在世界坐標(biāo)中的法線方向與積雪方向的點(diǎn)積,如果大于設(shè)定的積雪等級(jí)的閾值的話則表示這個(gè)方向與積雪方向是一致的,其上是可以積雪的,顯示雪的顏色,否則使用原貼圖的顏色。廢話不再多說(shuō),上代碼,在上面的Shader的基礎(chǔ)上,更改Properties里的內(nèi)容為
Properties { _MainTex ("Base (RGB)", 2D) = "white" {}_Bump ("Bump", 2D) = "bump" {} _Snow ("Snow Level", Range(0,1) ) = 0 _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0) _SnowDirection ("Snow Direction", Vector) = (0,1,0) }沒(méi)有太多值得說(shuō)的,唯一要提一下的是_SnowDirection設(shè)定的默認(rèn)值為(0,1,0),這表示我們希望雪是垂直落下的。對(duì)應(yīng)地,在CG程序中對(duì)這些變量進(jìn)行聲明:
sampler2D _MainTex; sampler2D _Bump; float _Snow; float4 _SnowColor; float4 _SnowDirection;接下來(lái)改變Input的內(nèi)容:
struct Input { float2 uv_MainTex;float2 uv_Bump;float3 worldNormal; INTERNAL_DATA };相對(duì)于上面的Shader輸入來(lái)說(shuō),加入了一個(gè)float3 worldNormal; INTERNAL_DATA,如果SurfaceOutput中設(shè)定了Normal值的話,通過(guò)worldNormal可以獲取當(dāng)前點(diǎn)在世界中的法線值。詳細(xì)的解說(shuō)可以參見(jiàn)Unity的Shader文檔。接下來(lái)可以改變surf函數(shù),實(shí)裝積雪效果了。
void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex);o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump));if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) > lerp(1,-1,_Snow)) {o.Albedo = _SnowColor.rgb;} else { o.Albedo = c.rgb; } o.Alpha = c.a; }和上面相比,加入了一個(gè)if…else…的判斷。首先看這個(gè)條件的不等式的左側(cè),我們對(duì)雪的方向和和輸入點(diǎn)的世界法線方向進(jìn)行點(diǎn)積。WorldNormalVector通過(guò)輸入的點(diǎn)及這個(gè)點(diǎn)的法線值,來(lái)計(jì)算它在世界坐標(biāo)中的方向;右側(cè)的lerp函數(shù)相信只要對(duì)插值有概念的同學(xué)都不難理解:當(dāng)Snow取最小值0時(shí),這個(gè)函數(shù)將返回1,而Snow取最大值時(shí),返回-1。這樣我們就可以通過(guò)設(shè)定Snow的值來(lái)控制積雪的閾值,要是積雪等級(jí)Snow是0時(shí),不等式左側(cè)不可能大于右側(cè),因此完全沒(méi)有積雪;相反要是_Snow取最大值1時(shí),由于左側(cè)必定大于-1,所以全模型積雪。而隨著取中間值的變化,積雪的情況便會(huì)有所不同。
應(yīng)用這個(gè)Shader,并且適當(dāng)?shù)卣{(diào)節(jié)一下積雪等級(jí)和顏色,可以得到如下右邊的效果。
更改頂點(diǎn)模型
到現(xiàn)在位置,我們還僅指是在原貼圖上進(jìn)行操作,不管是用法線圖使模型看起來(lái)凸凹有致,還是加上積雪,所有的計(jì)算和顏色的輸出都只是“障眼法”,并沒(méi)有對(duì)模型有任何實(shí)質(zhì)的改動(dòng)。但是對(duì)于積雪效果來(lái)說(shuō),實(shí)際上積雪是附加到石頭上面,而不應(yīng)當(dāng)簡(jiǎn)單替換掉原來(lái)的顏色。但是具體實(shí)施起來(lái),最簡(jiǎn)單的辦法還是直接替換顏色,但是我們可以稍微變更一下模型,使原來(lái)的模型在積雪的方向稍微變大一些,這樣來(lái)達(dá)到一種雪是附加到石頭上的效果。
我們繼續(xù)修改之前的Shader,首先我們需要告訴surface shadow我們要改變模型的頂點(diǎn)。首先將#param行改為
#pragma surface surf CustomDiffuse vertex:vert
這告訴Shader我們想要改變模型頂點(diǎn),并且我們會(huì)寫(xiě)一個(gè)叫做vert的函數(shù)來(lái)改變頂點(diǎn)。接下來(lái)我們?cè)偬砑右粋€(gè)參數(shù),在Properties中聲明一個(gè)_SnowDepth變量,表示積雪的厚度,當(dāng)然我們也需要在CG段中進(jìn)行聲明:
//In Properties{…} _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1 //In CG declare float _SnowDepth;接下來(lái)實(shí)現(xiàn)vert方法,和之前積雪的運(yùn)算其實(shí)比較類似,判斷點(diǎn)積大小來(lái)決定是否需要擴(kuò)大模型以及確定模型擴(kuò)大的方向。在CG段中加入以下vert方法
void vert (inout appdata_full v) { float4 sn = mul(transpose(_Object2World) , _SnowDirection);if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow * 2) / 3)) { v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; } }和surf的原理差不多,系統(tǒng)會(huì)輸入一個(gè)當(dāng)前的頂點(diǎn)的值,我們根據(jù)需要計(jì)算并填上新的值作為返回即可。上面第一行中使用transpose方法輸出原矩陣的轉(zhuǎn)置矩陣,在這里_Object2World是Unity ShaderLab的內(nèi)建值,它表示將當(dāng)前模型轉(zhuǎn)換到世界坐標(biāo)中的矩陣,將其與積雪方向做矩陣乘積得到積雪方向在物體的世界空間中的投影(把積雪方向轉(zhuǎn)換到世界坐標(biāo)中)。之后我們計(jì)算了這個(gè)世界坐標(biāo)中實(shí)際的積雪方向和當(dāng)前點(diǎn)的法線值的點(diǎn)積,并將結(jié)果與使用積雪等級(jí)的2/3進(jìn)行比較lerp后的閾值比較。這樣,當(dāng)前點(diǎn)如果和積雪方向一致,并且積雪較為完整的話,將改變?cè)擖c(diǎn)的模型頂點(diǎn)高度。
加入模型更改前后的效果對(duì)比如下圖,加入模型調(diào)整的右圖表現(xiàn)要更為豐滿真實(shí)。
這節(jié)就到這里吧。本節(jié)中實(shí)現(xiàn)的Shader可以在這里找到完整版本進(jìn)行參考,希望大家周末愉快~
原文:http://onevcat.com/2013/08/shader-tutorial-2/
轉(zhuǎn)載于:https://www.cnblogs.com/cm186man/p/4087594.html
總結(jié)
以上是生活随笔為你收集整理的Unity3D Shader入门指南(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: SQLyog 注册码(包含企业版注册码)
- 下一篇: JVM:如何分析线程堆栈