本文繼續(xù)對《UnityShader入門精要》——馮樂樂 第十三章 使用深度和法線紋理 進(jìn)行學(xué)習(xí)
參考第13章 使用深度和法線紋理
在第12 章中姑丑,我們學(xué)習(xí)的屏幕后處理效果都只是在屏幕顏色圖像上進(jìn)行各種操作來實(shí)現(xiàn)的。然而切端,很多時(shí)候我們不僅需要當(dāng)前屏幕的顏色信息彻坛,還希望得到深度和法線信息。例如踏枣,在進(jìn)行邊緣檢測時(shí),直接利用顏色信息會(huì)使檢測到的邊緣信息受物體紋理和光照等外部因素的影響钙蒙,得到很多我們不需要的邊緣點(diǎn)茵瀑。一種更好的方法是,我們可以在深度紋理和法線紋理上進(jìn)行邊緣檢測躬厌,這些圖像不會(huì)受紋理和光照的影響马昨,而僅僅保存了當(dāng)前渲染物體的模型信息竞帽,通過這樣的方式檢測出來的邊緣更加可靠。
在本章中鸿捧,我們將學(xué)習(xí)如何在Unity 中獲取深度紋理和法線紋理來實(shí)現(xiàn)特定的屏幕后處理效果屹篓。
- 在13.1 節(jié)中匙奴,我們首先會(huì)學(xué)習(xí)如何在Unity 中獲取這兩種紋理泼菌。
- 在13.2 節(jié)中谍肤,我們會(huì)利用深度紋理來計(jì)算攝像機(jī)的移動(dòng)速度哗伯,實(shí)現(xiàn)攝像機(jī)的運(yùn)動(dòng)模糊效果。
- 在13.3 節(jié)中焊刹,我們會(huì)學(xué)習(xí)如何利用深度紋理來重建屏幕像素在世界空間中的位置系任,從而模擬屏幕霧效俩滥。
- 13.4 節(jié)會(huì)再次學(xué)習(xí)邊緣檢測的另一種實(shí)現(xiàn)举农,即利用深度和法線紋理進(jìn)行邊緣檢測颁糟。
一棱貌、獲取深度和法線紋理
深度紋理實(shí)際就是一張渲染紋理婚脱,只不過它里面存儲(chǔ)的像素值不是顏色值勺像,而是一個(gè)高精度的深度值吟宦。由于被存儲(chǔ)在一張紋理中殃姓,深度紋理里的深度值范圍是[0, 1]瓦阐,而且通常是非線性分布的睡蟋。那么戳杀,這些深度值是從哪里得到的呢豺瘤?要回答這個(gè)問題坐求,我們需要回顧在第4 章學(xué)過的頂點(diǎn)變換的過程桥嗤。
總體來說,這些深度值來自于頂點(diǎn)變換后得到的歸一化的設(shè)備坐標(biāo)( Normalized Device Coordinates , NDC )渊鞋∥危回顧一下执俩,一個(gè)模型要想最終被繪制在屏幕上役首,需要把它的頂點(diǎn)從模型空間變換到齊次裁剪坐標(biāo)系下衡奥,這是通過在頂點(diǎn)著色器中乘以MVP 變換矩陣得到的杰赛。
1.透視投影
在MVP變換的最后一步乏屯,我們需要使用一個(gè)投影矩陣來變換頂點(diǎn)辰晕,當(dāng)我們使用的是透視投影類型的攝像機(jī)時(shí)含友,這個(gè)投影矩陣就是非線性的,具體過程可回顧4.6.7 小節(jié)惠赫。
圖13.1 顯示了4.6.7 小節(jié)中給出的Unity 中透視投影對頂點(diǎn)的變換過程。圖13.1 中最左側(cè)的圖顯示了投影變換前奉呛,即觀察空間下視錐體的結(jié)構(gòu)及相應(yīng)的頂點(diǎn)位置瞧壮,中間的圖顯示了應(yīng)用透視裁剪矩陣后的變換結(jié)果咆槽,即頂點(diǎn)著色器階段輸出的頂點(diǎn)變換結(jié)果秦忿,最右側(cè)的圖則是底層硬件進(jìn)行了透視除法后得到的歸一化的設(shè)備坐標(biāo)灯谣。需要注意的是胎许,這里的投影過程是建立在Unity 對坐標(biāo)系的假定上的辜窑,也就是說穆碎,我們針對的是觀察空間為右手坐標(biāo)系惨远,使用列矩陣在矩陣右側(cè)進(jìn)行相乘北秽,且變換到NDC 后z 分量范圍將在[-1, 1]之間的情況蔚叨。而在類似DirectX 這樣的圖形接口中蔑水,變換后z 分量范圍將在[0, 1]之間搀别。如果需要在其他圖形接口下實(shí)現(xiàn)本章的類似效果歇父, 需要對一些計(jì)算參數(shù)做出相應(yīng)變化榜苫。關(guān)于變換時(shí)使用的矩陣運(yùn)算, 讀者可以參考4.6.7 小節(jié)驹饺。
2.正交投影
圖13.2 顯示了在使用正交攝像機(jī)時(shí)投影變換的過程劣光。同樣,變換后會(huì)得到一個(gè)范圍為[-1雄可, 1] 的立方體数苫。正交投影使用的變換矩陣是線性的虐急。
3.深度紋理中的像素值
在得到NDC 后止吁,深度紋理中的像素值就可以很方便地計(jì)算得到了赏殃,這些深度值就對應(yīng)了NDC 中頂點(diǎn)坐標(biāo)的z 分量的值仁热。由于NDC 中z 分量的范圍在[-1, 1]举哟,為了讓這些值能夠存儲(chǔ)在一張圖像中妨猩,我們需要使用下面的公式對其進(jìn)行映射:
其中, d 對應(yīng)了深度紋理中的像素值庐椒, Zndc 對應(yīng)了NDC 坐標(biāo)中的z 分量的值约谈。
4.那么Unity 是怎么得到這樣一張深度紋理的呢?
在Unity 中迈勋,深度紋理可以直接來自于真正的深度緩存粪躬,也可以是由一個(gè)單獨(dú)的Pass 渲染而得,這取決于使用的渲染路徑和硬件泳唠。
通常來講笨腥,當(dāng)使用延遲渲染路徑(包括遺留的延遲渲染路徑)時(shí),深度紋理理所當(dāng)然可以訪問到谆级,因?yàn)檠舆t渲染會(huì)把這些信息渲染到G-buffer 中肥照。而當(dāng)無法直接獲取深度緩存時(shí)鲤脏,深度和法線紋理是通過一個(gè)單獨(dú)的Pass 渲染而得的猎醇。具體實(shí)現(xiàn)是姑食, Unity 會(huì)使用著色器替換( Shader Replacement )技術(shù)選擇那些渲染類型〈即SubShader 的RenderType 標(biāo)簽)為Opaque 的物體则拷,判斷它們使用的渲染隊(duì)列是否小于等于2 500 (內(nèi)置的Background 煌茬、Geometry 和AlphaTest 渲染隊(duì)列均在此范圍內(nèi))晾蜘,如果滿足條件剔交,就把它渲染到深度和法線紋理中。因此竭鞍,要想讓物體能夠出現(xiàn)在深度和法線紋理中偎快,就必須在Shader 中設(shè)置正確的RenderType 標(biāo)簽。
在Unity 中惋戏,我們可以選擇讓一個(gè)攝像機(jī)生成一張深度紋理或是一張深度+法線紋理响逢。當(dāng)選擇前者些膨,即只需要一張單獨(dú)的深度紋理時(shí), Unity 會(huì)直接獲取深度緩存或是按之前講到的著色器替換技術(shù)洼哎,選取需要的不透明物體噩峦,并使用它投射陰影時(shí)使用的Pass (即LightMode 被設(shè)置為ShadowCaster 的Pass,詳見9.4 節(jié))來得到深度紋理凭涂。如果Shader 中不包含這樣一個(gè)Pass,那么這個(gè)物體就不會(huì)出現(xiàn)在深度紋理中(當(dāng)然白翻,它也不能向其他物體投射陰影)。
深度紋理的精度通常是24 位或16 位巢株,這取決于使用的深度緩存的精度困檩。如果選擇生成一張深度+法線紋理悼沿, Unity 會(huì)創(chuàng)建一張和屏幕分辨率相同、精度為32 位〈每個(gè)通道為8 位)的紋理义郑,其中觀察空間下的法線信息會(huì)被編碼進(jìn)紋理的R 和G 通道,而深度信息會(huì)被編碼進(jìn)B 和A 通道劫笙。法線信息的獲取在延遲渲染中是可以非常容易就得到的, Unity 只需要合并深度和法線緩存即可。而在前向渲染中,默認(rèn)情況下是不會(huì)創(chuàng)建法線緩存的曹仗,因此Unity 底層使用了一個(gè)單獨(dú)的Pass 把整個(gè)場景再次渲染一遍來完成。這個(gè)Pass 被包含在Unity 內(nèi)置的一個(gè)Unity Shader 中轨蛤,我們可以在內(nèi)置的
builtin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader 文件中找到這個(gè)用于渲染深度和法線信息的Pass。
5.如何獲取
在Unity 中缝呕,獲取深度紋理是非常簡單的摊聋,我們只需要告訴Unity:“嘿,把深度紋理給我悲立!”然后再在Shader 中直接訪問特定的紋理屬性即可。這個(gè)與Unity 溝通的過程是通過在腳本中設(shè)置攝像機(jī)的depthTextureMode 來完成的原献,例如我們可以通過下面的代碼來獲取深度紋理:
camera.depthTextureMode = DepthTextureMode.Depth;
一旦設(shè)置好了上面的攝像機(jī)模式后,我們就可以在Shader 中通過聲明 _CameraDepthTexture變量來訪問它埂淮。這個(gè)過程非常簡單姑隅,但我們需要知道這兩行代碼的背后, Unity 為我們做了許多工作(見13.1.1 節(jié)〉倔撞。
同理讲仰,如果想要獲取深度+法線紋理痪蝇,我們只需要在代碼中這樣設(shè)置:
camera.depthTextureMode = DepthTextureMode.DepthNormals;
然后在Shader 中通過聲明 _CameraDepthNormalsTexture 變量來訪問它鄙陡。
我們還可以組合這些模式,讓一個(gè)攝像機(jī)同時(shí)產(chǎn)生一張深度和深度+法線紋理:
camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在Unity 5 中躏啰,我們還可以在攝像機(jī)的Camera 組件上看到當(dāng)前攝像機(jī)是否需要渲染深度或深度+法線紋理趁矾。當(dāng)在Shader 中訪問到深度紋理 _CameraDepthTexture 后,我們就可以使用當(dāng)前像素的紋理坐標(biāo)對它進(jìn)行采樣给僵。絕大多數(shù)情況下毫捣,我們直接使用tex2D 函數(shù)采樣即可,但在某些平臺(tái)(例如PS3 和PSP2 )上帝际,我們需要一些特殊處理蔓同。Unity 為我們提供了一個(gè)統(tǒng)一的宏SAMPLE_DEPTH_TEXTURE,用來處理這些由于平臺(tái)差異造成的問題胡本。而我們只需要在Shader中使用SAMPLE_DEPTH_TEXTURE 宏對深度紋理進(jìn)行采樣牌柄,例如:
float d = SAMPLE_DEPTH_TEXTURE(_CarneraDepthTexture, i.uv);
其中, i.uv 是一個(gè)float2 類型的變量侧甫,對應(yīng)了當(dāng)前像素的紋理坐標(biāo)珊佣。
類似的宏還有SAMPLE_DEPTH_TEXTURE_PROJ 和SAMPLE_DEPTH_TEXTURE_LOD蹋宦。
SAMPLE_DEPTH_TEXTURE_PROJ 宏同樣接受兩個(gè)參數(shù)一一深度紋理和一個(gè)float3 或float4 類型的紋理坐標(biāo),它的內(nèi)部使用了tex2Dproj 這樣的函數(shù)進(jìn)行投影紋理采樣咒锻,紋理坐標(biāo)的前兩個(gè)分量首先會(huì)除以最后一個(gè)分量冷冗,再進(jìn)行紋理采樣。如果提供了第四個(gè)分量惑艇,還會(huì)進(jìn)行一次比較蒿辙,通常用于陰影的實(shí)現(xiàn)中。SAMPLE_DEPTH_TEXTURE PROJ 的第二個(gè)參數(shù)通常是由頂點(diǎn)著色器輸出插值而得的屏幕坐標(biāo)滨巴,例如:
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CarneraDepthTexture, UNITY_PROJ_COORD(i.scrPos));
其中思灌, i.scrPos 是在頂點(diǎn)著色器中通過調(diào)用ComputeScreenPos(o.pos)得到的屏幕坐標(biāo)。上述這些宏的定義恭取,讀者可以在Unity 內(nèi)置的HLSLSupport.cginc 文件中找到泰偿。
6.由深度紋理中的深度信息計(jì)算得到視角空間下的深度值
當(dāng)通過紋理采樣得到深度值后,這些深度值往往是非線性的蜈垮,這種非線性來自于透視投影使用的裁剪矩陣耗跛。然而,在我們的計(jì)算過程中通常是需要線性的深度值攒发,也就是說调塌,我們需要把投影后的深度值變換到線性空間下,例如視角空間下的深度值惠猿。那么羔砾,我們應(yīng)該如何進(jìn)行這個(gè)轉(zhuǎn)換呢?實(shí)際上偶妖,我們只需要倒推頂點(diǎn)變換的過程即可蜒茄。下面我們以透視投影為例,推導(dǎo)如何由深度紋理中的深度信息計(jì)算得到視角空間下的深度值餐屎。
由4.6.7節(jié)可知,當(dāng)我們使用透視投影的裁剪矩陣Pclip對視角空間下的一個(gè)頂點(diǎn)進(jìn)行變換后玩祟,裁剪空間下頂點(diǎn)的z和w分量為:
其中腹缩,F(xiàn)ar和Near分別是遠(yuǎn)近裁剪平面的距離。然后空扎,我們通過齊次除法就可以得到NDC下的z分量:
在13.1.1節(jié)中我們知道藏鹊,深度紋理中的深度值是通過下面的公式由NDC計(jì)算而得的:
由上面的這些式子,可以推導(dǎo)出用d表示而得的Zvisw的表達(dá)式:
由于在Unity使用的視角空間中转锈,攝像機(jī)正向?qū)?yīng)的z值均為負(fù)值盘寡,因此為了得到深度值的正數(shù)表示,我們需要對上面的結(jié)果取反撮慨,最后得到的結(jié)果如下:
它的取值范圍就是視椎體深度范圍竿痰,即[Near,Far]脆粥。如果我們想得到范圍在[0,1]之間的深度值,只需要把上面得到的結(jié)果除以Far即可影涉。這樣变隔,0就表示該點(diǎn)與攝像機(jī)位于同一位置,1表示該點(diǎn)位于視椎體的遠(yuǎn)裁剪平面上蟹倾。結(jié)果如下:
幸運(yùn)的是匣缘, Unity 提供了兩個(gè)輔助函數(shù)來為我們進(jìn)行上述的計(jì)算過程一一LinearEyeDepth 和 Linear01Depth。
- LinearEyeDepth 負(fù)責(zé)把深度紋理的采樣結(jié)果轉(zhuǎn)換到視角空間下的深度值鲜棠,也就是我們上面得到的Zvisw肌厨。
- 而Linear01Depth 則會(huì)返回一個(gè)范圍在[0, 1]的線性深度值,也就是我們上面得到的Z 01 豁陆。這兩個(gè)函數(shù)內(nèi)部使用了內(nèi)置的 _ZBufferParams 變量來得到遠(yuǎn)近裁剪平面的距離柑爸。
如果我們需要獲取深度+法線紋理,可以直接使用tex2D 函數(shù)對 _CameraDepthNormalsTexture 進(jìn)行采樣献联,得到里面存儲(chǔ)的深度和法線信息竖配。Unity 提供了輔助函數(shù)來為我們對這個(gè)采樣結(jié)果進(jìn)行解碼,從而得到深度值和法線方向里逆。這個(gè)函數(shù)DecodeDepthNorrnal, 它在UnityCG.cginc 里被定義:
inline void DecodeDepthNormal( float4 enc, out float depth, out float3 normal)
{
depth = DecodeFloatRG (enc.zw);
normal= DecodeViewNormalStereo(enc);
}
DecodeDepthNormal 的第一個(gè)參數(shù)是對深度+法線紋理的采樣結(jié)果进胯,這個(gè)采樣結(jié)果是Unity 對深度和法線信息編碼后的結(jié)果, 它的xy 分量存儲(chǔ)的是視角空間下的法線信息原押, 而深度信息被編碼進(jìn)了zw 分量胁镐。通過調(diào)用DecodeDepthNormal 函數(shù)對采樣結(jié)果解碼后,我們就可以得到解碼后的深度值和法線诸衔。這個(gè)深度值是范圍在[0, 1]的線性深度值(這與單獨(dú)的深度紋理中存儲(chǔ)的深度值不同〉盯漂,而得到的法線則是視角空間下的法線方向。同樣笨农, 我們也可以通過調(diào)用DecodeFloatRG 和 DecodeViewNormaLStereo 來解碼深度+法線紋理中的深度和法線信息就缆。
至此,我們已經(jīng)學(xué)會(huì)了如何在Unity 里獲取及使用深度和法線紋理谒亦。下面竭宰, 我們會(huì)學(xué)習(xí)如何使用它們實(shí)現(xiàn)各種屏幕特效。
二份招、查看深度和法線紋理
很多時(shí)候雄人, 我們希望可以查看生成的深度和法線紋理接箫,以便對Shader 進(jìn)行調(diào)試裂允。Unity 5 提供了一個(gè)方便的方法來查看攝像機(jī)生成的深度和法線紋理际插, 這個(gè)方法就是利用幀調(diào)試器( Frame Debugger)。圖13.3 顯示了使用幀調(diào)試器查看到的深度紋理和深度+法線紋理谐腰。
使用幀調(diào)試器查看到的深度紋理是非線性空間的深度值巩步,而深度+法線紋理都是由Unity 編碼后的結(jié)果旁赊。有時(shí),顯示出線性空間下的深度信息或解碼后的法線方向會(huì)更加有用椅野。此時(shí)终畅,我們可以自行在片元著色器中輸出轉(zhuǎn)換或解碼后的深度和法線值, 如圖13.4 所示竟闪。輸出代碼非常簡單离福,我們可以使用類似下面的代碼來輸出線性深度值:
float depth= SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float linearDepth = LinearOlDepth(depth);
return fixed4(linearDepth, linearDepth, linearDepth, 1.0);
或是輸出法線方向:
fixed3 normal = DecodeViewNormalStereo(tex2D( _CameraDepthNormalsTexture, i.uv).xy);
return fixed4 (normal * 0.5 + 0.5, 1.0);
在查看深度紋理時(shí),讀者得到的畫面有可能幾乎是全黑或全白的炼蛤。這時(shí)候讀者可以把攝像機(jī)的遠(yuǎn)裁剪平面的距離( Unity 默認(rèn)為1000 )調(diào)小妖爷, 使視錐體的范圍剛好覆蓋場景的所在區(qū)域。這是因?yàn)槔砼螅捎谕队白儞Q時(shí)需要覆蓋從近裁剪平面到遠(yuǎn)裁剪平面的所有深度區(qū)域絮识, 當(dāng)遠(yuǎn)裁剪平面的 距離過大時(shí), 會(huì)導(dǎo)致離攝像機(jī)較近的距離被映射到非常小的深度值嗽上,如果場景是一個(gè)封閉的區(qū)域 (如圖13.4 所示〉次舌, 那么這就會(huì)導(dǎo)致畫面看起來幾乎是全黑的。相反兽愤, 如果場景是一個(gè)開放區(qū)域彼念, 且物體離攝像機(jī)的距離較遠(yuǎn), 就會(huì)導(dǎo)致畫面兒乎是全白的浅萧。
三、再談運(yùn)動(dòng)模糊
在12.6 節(jié)中洼畅,我們學(xué)習(xí)了如何通過混合多張屏幕圖像來模擬運(yùn)動(dòng)模糊的效果吩案。但是,另一種應(yīng)用更加廣泛的技術(shù)則是使用速度映射圖帝簇。速度映射圖中存儲(chǔ)了每個(gè)像素的速度务热,然后使用這個(gè)速度來決定模糊的方向和大小。速度緩沖的生成有多種方法己儒,一種方法是把場景中所有物體的速度渲染到一張紋理中。但這種方法的缺點(diǎn)在于需要修改場景中所有物體的Shader 代碼捆毫,使其添加計(jì)算速度的代碼并輸出到一個(gè)渲染紋理中闪湾。
《GPU Gems3》在第27 章(http:http.developer.nvidia.com/GPUGems3/gpugems3_ch27.html) 中介紹了一種生成速度映射圖的方法。這種方法利用深度紋理在片元著色器中為每個(gè)像素計(jì)算其在世界空間下的位置绩卤,這是通過使用當(dāng)前的視角*投影矩陣的逆矩陣對NDC 下的頂點(diǎn)坐標(biāo)進(jìn)行變換得到的途样。當(dāng)?shù)玫绞澜缈臻g中的頂點(diǎn)坐標(biāo)后江醇,我們使用前一幀的視角*投影矩陣對其進(jìn)行變換,得到該位置在前一幀中的NDC 坐標(biāo)何暇。然后陶夜,我們計(jì)算前一幀和當(dāng)前幀的位置差,生成該像素的速度裆站。這種方法的優(yōu)點(diǎn)是可以在一個(gè)屏幕后處理步驟中完成整個(gè)效果的模擬条辟,但缺點(diǎn)是需要在片元著色器中進(jìn)行兩次矩陣乘法的操作,對性能有所影響宏胯。
1.實(shí)現(xiàn)
代碼細(xì)節(jié)見原書
2.總結(jié)
本節(jié)實(shí)現(xiàn)的運(yùn)動(dòng)模糊適用于場景靜止羽嫡、攝像機(jī)快速運(yùn)動(dòng)的情況,這是因?yàn)槲覀冊谟?jì)算時(shí)只考慮了攝像機(jī)的運(yùn)動(dòng)肩袍。因此杭棵,如果讀在把本節(jié)中的代碼應(yīng)用到一個(gè)物體快速運(yùn)動(dòng)而攝像機(jī)靜止的場景,會(huì)發(fā)現(xiàn)不會(huì)產(chǎn)生任何運(yùn)動(dòng)模糊效果氛赐。如果我們想要對快速移動(dòng)的物體產(chǎn)生運(yùn)動(dòng)模糊的效果魂爪,就需要生成更加精確的速度映射圖。讀者可以在Unity 自帶的lmageEffect 包中找到更多的運(yùn)動(dòng)模糊的實(shí)現(xiàn)方法艰管。
本節(jié)選擇在片元著色器中使用逆矩陣來重建每個(gè)像素在世界空間下的位置滓侍。但是,這種做法往往會(huì)影響性能蛙婴,在13.3 節(jié)中粗井,我們會(huì)介紹一種更快速的由深度紋理重建世界坐標(biāo)的方法。
四街图、全局霧效
霧效(Fog )是游戲里經(jīng)常使用的一種效果浇衬。Unity 內(nèi)置的霧效可以產(chǎn)生基于距離的線性或指數(shù)霧效。然而餐济,要想在自己編寫的頂點(diǎn)/片元著色器中實(shí)現(xiàn)這些霧效耘擂,我們需要在Shader 中添加 #pragma multi_compile_fog 指令,同時(shí)還需要使用相關(guān)的內(nèi)置宏絮姆,例如UNITY_FOG_COORDS, UNITY_TRANSFER_FOG 和 UNITY_APPLY_FOG 等醉冤。這種方法的缺點(diǎn)在于,我們不僅需要為場景中所有物體添加相關(guān)的渲染代碼篙悯,而且能夠?qū)崿F(xiàn)的效果也非常有限蚁阳。當(dāng)我們需要對霧效進(jìn)行一些個(gè)性化操作時(shí),例如使用基于高度的霧效等鸽照,僅僅使用Unity 內(nèi)置的霧效就變得不再可行螺捐。
在本節(jié)中,我們將會(huì)學(xué)習(xí)一種基于屏幕后處理的全局霧效的實(shí)現(xiàn)。使用這種方法定血,我們不需要更改場景內(nèi)渲染的物體所使用的Shader代碼赔癌,而僅僅依靠一次屏幕后處理的步驟即可。這種方法的自由性很高澜沟,我們可以方便地模擬各種霧效灾票,例如均勻的霧效、基于距離的線性/指數(shù)霧效茫虽、基于高度的霧效等刊苍。在學(xué)習(xí)完本節(jié)后,我們可以得到類似圖13.5 中的效果席噩。
基于屏幕后處理的全局霧效的關(guān)鍵是,根據(jù)深度紋理來重建每個(gè)像素在世界空間下的位置悼枢。盡管在13.2 節(jié)中埠忘,我們在模擬運(yùn)動(dòng)模糊時(shí)已經(jīng)實(shí)現(xiàn)了這個(gè)要求,即構(gòu)建出當(dāng)前像素的NDC 坐標(biāo)馒索,再通過當(dāng)前攝像機(jī)的視角*投影矩陣的逆矩陣來得到世界空間下的像索坐標(biāo)莹妒,但是,這樣的實(shí)現(xiàn)需要在片元著色器中進(jìn)行矩陣乘法的操作绰上,而這通常會(huì)影響游戲性能旨怠。
1.得到該像素在世界空間下的位置
在本節(jié)中,我們將會(huì)學(xué)習(xí)一個(gè)快速從深度紋理中重建世界坐標(biāo)的方法蜈块。這種方法首先對圖像空間下的視錐體射線(從攝像機(jī)出發(fā)鉴腻,指向圖像上的某點(diǎn)的射線〉進(jìn)行插值,這條射線存儲(chǔ)了該像素在世界空間下到攝像機(jī)的方向信息百揭。然后爽哎,我們把該射線和線性化后的視角空間下的深度值相乘,再加上攝像機(jī)的世界位置器一,就可以得到該像素在世界空間下的位置课锌。當(dāng)我們得到世界坐標(biāo)后,就可以輕松地使用各個(gè)公式來模擬全局霧效了祈秕。
2.如何從深度紋理中重建世界坐標(biāo)
我們知道渺贤,坐標(biāo)系中的一個(gè)頂點(diǎn)坐標(biāo)可以通過它相對于另一個(gè)頂點(diǎn)坐標(biāo)的偏移量來求得。重建像素的世界坐標(biāo)也是基于這樣的思想请毛。我們只需要知道攝像機(jī)在世界空間下的位置志鞍,以及世界空間下該像素相對于攝像機(jī)的偏移量,把它們相加就可以得到該像素的世界坐標(biāo)方仿。整個(gè)過程可以使用下面的代碼來表示:
float4 posWS = _worldSpaceCameraPos + linearDepth * interpolateRay;
其中固棚, _WorldSpaceCameraPos 是攝像機(jī)在世界空間下的位置街州,這可以由Unity 的內(nèi)置變量直接訪問得到。而
linearDepth * interpolatedRay 則可以計(jì)算得到該像素相對于攝像機(jī)的偏移量玻孟, linearDepth 是由深度紋理得到的線性深度值, interpolatedRay 是由頂點(diǎn)著色器輸出并插值后得到的射線鳍征,它不僅包含了該像素到攝像機(jī)的方向黍翎,也包含了距離信息。linearDepth 的獲取我們己經(jīng)在13.1.2 節(jié)中詳細(xì)解釋過了艳丛,因此匣掸,本節(jié)著重解釋 interpolatedRay 的求法。
3. interpolatedRay 的求法
interpolatedRay 來源于對近裁剪平面的4 個(gè)角的某個(gè)特定向量的插值氮双,這4 個(gè)向量包含了它們到攝像機(jī)的方向和距離信息碰酝,我們可以利用攝像機(jī)的近裁剪平面距離、FOV戴差、橫縱比計(jì)算而得送爸。圖13.6顯示了計(jì)算時(shí)使用的一些輔助向量。為了方便計(jì)算暖释,我們可以先計(jì)算兩個(gè)向量——toTop 和 toRight, 它們是起點(diǎn)位于近裁剪平面中心袭厂、分別指向攝像機(jī)正上方和正右方的向量。它們的計(jì)算公式如下:
其中球匕,Near是近裁剪平面的距離纹磺,F(xiàn)OV是豎直方向的視角范圍,camera.up camera.right分別對應(yīng)了攝像機(jī)的正上方和正右方亮曹。
當(dāng)?shù)玫竭@兩個(gè)輔助向量后橄杨,我們就可以計(jì)算4個(gè)角相對于攝像機(jī)的方向了。我們以左上角為例(見圖13.6中的TL點(diǎn))照卦,它的計(jì)算公式如下:
讀者可以依靠基本的矢量運(yùn)算驗(yàn)證上面的結(jié)果式矫。同理,其他3個(gè)角的計(jì)算也是類似的:
注意窄瘟,上面求得的4 個(gè)向量不僅包含了方向信息衷佃,它們的模對應(yīng)了4 個(gè)點(diǎn)到攝像機(jī)的空間距離。由于我們得到的線性深度值并非是點(diǎn)到攝像機(jī)的歐式距離蹄葱,而是在z 方向上的距離氏义,因此,我們不能直接使用深度值和4 個(gè)角的單位方向的乘積來計(jì)算它們到攝像機(jī)的偏移量图云,如圖13.7 所示惯悠。想要把深度值轉(zhuǎn)換成到攝像機(jī)的歐式距離也很簡單,我們以TL 點(diǎn)為例竣况,根據(jù)相似三角形原理克婶, TL 所在的射線上,像素的深度值和它到攝像機(jī)的實(shí)際距離的比等于近裁剪平面的距離和TL向量的模的比,即
由此可得情萤,我們需要的TL距離攝像機(jī)的歐氏距離dist:
由于4個(gè)點(diǎn)相互對稱鸭蛙,因此其他3個(gè)向量的模和TL相等,即我們可以使用同一個(gè)因子和單位向量相乘筋岛,得到它們對應(yīng)的向量值:
屏幕后處理的原理是使用特定的材質(zhì)去渲染一個(gè)剛好填充整個(gè)屏幕的四邊形面片娶视。這個(gè)四邊形面片的4 個(gè)頂點(diǎn)就對應(yīng)了近裁剪平面的4 個(gè)角。因此睁宰,我們可以把上面的計(jì)算結(jié)果傳遞給頂點(diǎn)著色器肪获,頂點(diǎn)著色器根據(jù)當(dāng)前的位置選擇它所對應(yīng)的向量,然后再將其輸出柒傻,經(jīng)插值后傳遞給片元著色器得到interpoIatedRay孝赫,我們就可以直接利用本節(jié)一開始提到的公式重建該像素在世界空間下的位置了。
4.實(shí)現(xiàn)
參考原書
五红符、再談邊緣檢測
在12.3 節(jié)中青柄,我們曾介紹如何使用Sobel 算子對屏幕圖像進(jìn)行邊緣檢測, 實(shí)現(xiàn)描邊的效果违孝。但是刹前,這種直接利用顏色信息進(jìn)行邊緣檢測的方法會(huì)產(chǎn)生很多我們不希望得到的邊緣線,如圖13.8 所示雌桑。
可以看出,物體的紋理校坑、陰影等位置也被描上黑邊拣技,而這往往不是我們希望看到的。在本節(jié)中耍目,我們將學(xué)習(xí)如何在深度和法線紋理上進(jìn)行邊緣檢測膏斤,這些圖像不會(huì)受紋理和光照的影響,而僅僅保存了當(dāng)前渲染物體的模型信息邪驮,通過這樣的方式檢測出來的邊緣更加可靠莫辨。在學(xué)習(xí)完本節(jié)后,我們可以得到類似圖13.9 中的效果毅访。
與12.3 節(jié)使用Sobel 算子不同喻粹,本節(jié)將使用Roberts 算子來進(jìn)行邊緣檢測蟆融。它使用的卷積核如圖13.10 所示。
Roberts 算子的本質(zhì)就是計(jì)算左上角和右下角的差值守呜,乘以右上角和左下角的差值型酥,作為評估邊緣的依據(jù)。在下面的實(shí)現(xiàn)中弥喉,我們也會(huì)按這樣的方式郁竟,取對角方向的深度或法線值枪孩,比較它們之間的差值拒担,如果超過某個(gè)閥值(可由參數(shù)控制)嘹屯,就認(rèn)為它們之間存在一條邊。
1.實(shí)現(xiàn)
參考原書
2.總結(jié)
本節(jié)實(shí)現(xiàn)的描邊效果是基于整個(gè)屏幕空間進(jìn)行的从撼,也就是說州弟,場景內(nèi)的所有物體都會(huì)被添加描邊效果。但有時(shí)低零,我們希望只對特定的物體進(jìn)行描邊婆翔,例如當(dāng)玩家選中場景中的某個(gè)物體后,我們想要在該物體周圍添加一層描邊效果掏婶。這時(shí)啃奴,我們可以使用Unity 提供的Graphics.DrawMesh 或 Graphics.DrawMeshNow 函數(shù)把需要描邊的物體再次渲染一遍(在所有不透明物體渲染完畢之后),然
后再使用本節(jié)提到的邊緣檢測算法計(jì)算深度或法線紋理中每個(gè)像素的梯度值雄妥,判斷它們是否小于某個(gè)閥值最蕾,如果是,就在Shader 中使用clip() 函數(shù)將該像素剔除掉老厌,從而顯示出原來的物體顏色瘟则。
六、擴(kuò)展閱讀
在本章中枝秤,我們介紹了如何使用深度和法線紋理實(shí)現(xiàn)諸如全局霧效醋拧、邊緣檢測等效果。盡管我們只使用了深度和法線紋理淀弹,但實(shí)際上我們可以在Unity 中創(chuàng)建任何需要的緩存紋理丹壕。
這可以通過使用Unity 的著色器替換( Shader Replacement )功能(即調(diào)用Camera.RenderWithShader(shader, replacementTag)函數(shù))把整個(gè)場景再次渲染一遍來得到,而在很多時(shí)候垦页,這實(shí)際也是Unity 創(chuàng)建深度和法線紋理時(shí)使用的方法雀费。
深度和法線紋理在屏幕特效的實(shí)現(xiàn)中往往扮演了重要的角色。許多特殊的屏幕效果都需要依靠這兩種紋理的幫助痊焊。Unity 曾在2011 年的SIGGRAPH (計(jì)算圖形學(xué)的頂級會(huì)議〉上做了一個(gè)關(guān)于使用深度紋理實(shí)現(xiàn)各種特效的演講
(http://blogs.unity3d.com/2011/09/08/special-effects-with-depth-talk-at-siggraph/ )盏袄。在這個(gè)演講中忿峻, Unity 的工作人員解釋了如何利用深度紋理來實(shí)現(xiàn)特定物體的描邊、角色護(hù)盾辕羽、相交線的高光模擬等效果逛尚。在Unity 的 Image Effect
( http://docs.unity3d.com/Manual/comp-ImageEffects.html )包中,讀者也可以找到一些傳統(tǒng)的使用深度紋理實(shí)現(xiàn)屏幕特效的例子刁愿,例如屏幕空間的環(huán)境遮擋(Screen Space Ambient Occlusion, SSAO )等效果绰寞。