Unity Shader系列文章:Unity Shader目錄-初級(jí)篇
Unity Shader系列文章:Unity Shader目錄-中級(jí)篇
參考文章:使用深度紋理,計(jì)算像素的世界坐標(biāo)
背后的原理:
深度紋理實(shí)際就是一張渲染紋理,只不過(guò)它里面存儲(chǔ)的像素值不是顏色值,而是一個(gè)高精度的深度值。由于被存儲(chǔ)在一張紋理中,深度紋理里的深度值范圍是[0, l], 而且通常是非線性分布的。這些深度值來(lái)自于頂點(diǎn)變換后得到的歸一化的設(shè)備坐標(biāo) (Normalized Device Coordinates , 簡(jiǎn)稱NDC) 。一個(gè)模型要想最終被繪制在屏幕上,需要把它的頂點(diǎn)從模型空間變換到齊次裁剪坐標(biāo)系下,這是通過(guò)在頂點(diǎn)著色器中乘以 MVP 變換矩陣得到的 。在變換的最后一步,我們需要使用一個(gè)投影矩陣來(lái)變換頂點(diǎn),當(dāng)我們使用的是透視投影類型的攝像機(jī)時(shí),這個(gè)投影矩陣就是非線性的。
下圖顯示了Unity 中透視投影對(duì)頂點(diǎn)的變換過(guò)程。最左側(cè)的圖顯示了投影變換前,即觀察空間下視錐體的結(jié)構(gòu)及相應(yīng)的頂點(diǎn)位置,中間的圖顯示了應(yīng)用透視裁剪矩陣后的變換結(jié)果,即頂點(diǎn)著色器階段輸出的頂點(diǎn)變換結(jié)果 ,最右側(cè)的圖則是底層硬件進(jìn)行了透視除法后得到的歸一化的設(shè)備坐標(biāo)(NDC)。需要注意的是,這里的投影過(guò)程是建立在Unity對(duì)坐標(biāo)系的假定上的,也就是說(shuō),針對(duì)的是觀察空間為右手坐標(biāo)系,使用列矩陣在矩陣右側(cè)進(jìn)行相乘,且變換到NDC后 z分量范圍將在[-1, l] 之間的情況。而在類似 DirectX 這樣的圖形接口中,變換后z分量范圍將在[0, 1]之間 。如果是在其他圖形接口下,需要對(duì)一些計(jì)算參數(shù)做出相應(yīng)變化。

下圖顯示了在使用正交攝像機(jī)時(shí)投影變換的過(guò)程。同樣,變換后會(huì)得到一個(gè)范圍為 [-1, 1]的立方體。正交投影使用的變換矩陣是線性的。

在得到NDC后,深度紋理中的像素值就可以很方便地計(jì)算得到了,這些深度值就對(duì)應(yīng)了NDC中頂點(diǎn)坐標(biāo)的z分量的值。由于NDC中 z分量的范圍在[-1, I], 為了讓這些值能夠存儲(chǔ)在一張圖像中,需要使用下面的公式對(duì)其進(jìn)行映射:
其中,d對(duì)應(yīng)了深度紋理中的像素值,對(duì)應(yīng)了NDC坐標(biāo)中的z分量的值。
在Unity中,深度紋理可以直接來(lái)自于真正的深度緩存,也可以是由一個(gè)單獨(dú)的 Pass 渲染而得,這取決于使用的渲染路徑和硬件。通常來(lái)講,當(dāng)使用延遲渲染路徑(包括遺留的延遲渲染路徑)時(shí),深度紋理理所當(dāng)然可以訪問(wèn)到,因?yàn)檠舆t渲染會(huì)把這些信息渲染到 G-buffer 。而當(dāng)無(wú)法直接獲取深度緩存時(shí),深度和法線紋理是通過(guò)一個(gè)單獨(dú)的Pass渲染而得 。具體實(shí)現(xiàn)是Unity會(huì)使用著色器替換技術(shù)選擇那些渲染類型(即 SubShader RenderType 標(biāo)簽)為 Opaque 的物體,判斷它們使用的渲染隊(duì)列是否小于等于2500(內(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)來(lái)得到深度紋理。如果 Shader 中不包含這樣一個(gè) Pass, 那么這個(gè)物體就不會(huì)出現(xiàn)在深度紋理中(當(dāng)然,它也不能向其他物體投射陰影)。深度紋理的精度通常24 位或 16 位,這取決于使用的深度緩存的精度。如果選擇生成一張深度+法線紋理, Unity 創(chuàng)建一張和屏幕分辨率相同、精度為 32 位(每個(gè)通道為 位)的紋理,其中觀察空間下的法線信息會(huì)被編碼進(jìn)紋理的 通道,而深度信息會(huì)被編碼進(jìn) 通道。法線信息的獲取在延遲渲染中是可以非常容易就得到的, Unity 只需要合并深度和法線緩存即可。而在前向渲染中,默認(rèn)情況下是不會(huì)創(chuàng)建法線緩存的,因此 Unity 底層使用了一個(gè)單獨(dú)的 Pass 把整個(gè)場(chǎng)景再次渲染一遍來(lái)完成。這個(gè) Pass 被包含在 Unity 內(nèi)置的一個(gè) Unity Shader 中,可以在內(nèi)置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormaITexture.shader 文件中找到這個(gè)用于渲染深度和法線信息的 Pass。
如何獲?。?/h6>
獲取深度紋理,先設(shè)置攝像機(jī)的 depthTextureMode:
camera . depthTextureMode = DepthTextureMode.Depth;
然后在 Shader 中通過(guò)聲明_CameraDepthNormalsTexture 變量來(lái)訪問(wèn)它。
同理,如果需要獲取獲取深度+法線紋理,設(shè)置攝像機(jī)的 depthTextureMode為:
camera .depthTextureMode = DepthTextureMode.DepthNormals;
然后在 Shader 中通過(guò)聲明_CameraDepthN ormalsTexture 變量來(lái)訪問(wèn)它。
還可以組合這些模式,讓一個(gè)攝像機(jī)同時(shí)產(chǎn)生一張深度和深度+法線紋理:
camera.depthTextureMode I= DepthTextureMode.Depth;
camera.depthTextureMode I= DepthTextureMode.DepthNormals;
在 Shader 中訪問(wèn)到深度紋理_CameraDepthTexture 后,我們就可以使用當(dāng)前像素的紋理坐標(biāo)對(duì)它進(jìn)行采樣。絕大多數(shù)情況下,我們直接使用 tex2D 函數(shù)采樣即可,但在某些平臺(tái)(例如 PS3 PSP2) 上,我們需要 一些特殊 處理 Unity 為我們提供了一個(gè)統(tǒng)一的宏SAMPLE_DEPTH_TEXTURE, 用來(lái)處理這些由于平臺(tái)差異造成的問(wèn)題。而我們只需要在 Shader中使用 SAMPLE_DEPTH_TEXTURE 宏對(duì)深度紋理進(jìn)行采樣,例如:
float d = SAMPLE DEPTH TEXTURE (CameraDepthTexture, i. uv) ;
其中,i.scrPos是在頂點(diǎn)著色器中通過(guò)調(diào)用ComputeScreenPos(o.pos)得到的屏幕坐標(biāo)。上述這些宏的定義,可以在Unity 內(nèi)置的HLSLSupport.cginc文件中找到。
當(dāng)通過(guò)紋理采樣得到深度值后,這些深度值往往是非線性的,這種非線性來(lái)自于透視投影使用的裁剪矩陣。然而,在我們的計(jì)算過(guò)程中通常是需要線性的深度值,也就是說(shuō),我們需要把投影后的深度值變換到線性空間下,例如視角空間下的深度值,我們只需要倒推頂點(diǎn)變換的過(guò)程即可。下面以透視投影為例,推導(dǎo)如何由深度紋理中的深度信息計(jì)算得到視角空間下的深度值。
當(dāng)我們使用透視投影的裁剪矩陣對(duì)視角空間下的一個(gè)頂點(diǎn)進(jìn)行變換后,裁剪空間下頂點(diǎn)的z和w分量為:
其中,F(xiàn)ar和Near分別是遠(yuǎn)近裁剪平面的距離。然后,我們通過(guò)齊次除法就可以得到NDC下的z分量:
而深度紋理中的深度值是通過(guò)下面的公式由NDC 計(jì)算而得的:
由上面的這些式子,可以推導(dǎo)出用d表示而得的的表達(dá)式:
由于在Unity 使用的視角空間中,攝像機(jī)正向?qū)?yīng)的z值均為負(fù)值,因此為了得到深度值的正數(shù)表示,我們需要對(duì)上面的結(jié)果取反,最后得到的結(jié)果如下:
它的取值范圍就是視錐體深度范圍,即[Near, Far]。如果我們想得到范圍在[0, l]之間的深度值,只需要把上面得到的結(jié)果除以Far即可。這樣,0就表示該點(diǎn)與攝像機(jī)位于同一位置,1表示該點(diǎn)位于視錐體的遠(yuǎn)裁剪平面上。結(jié)果如下:
其實(shí),Unity提供了兩個(gè)輔助函數(shù)來(lái)為我們進(jìn)行上述的計(jì)算過(guò)程LinearEyeDepth 和LinearOlDepth。LinearEyeDepth 負(fù)責(zé)把深度紋理的采樣結(jié)果轉(zhuǎn)換到視角空間下的深度值,也就是我們上面得到的。而Linear01Depth則會(huì)返回一個(gè)范圍在[0, 1]的線性深度值,也就是我們上面得到的
。這兩個(gè)函數(shù)內(nèi)部使用了內(nèi)置的_ZBufferParams變量來(lái)得到遠(yuǎn)近裁剪平面的距離。
如果需要獲取深度+法線紋理,可以直接使用tex2D函數(shù)對(duì)_CameraDepthNormalsTexture 進(jìn)行采樣,得到里面存儲(chǔ)的深度和法線信息。Unity提供了輔助函數(shù)來(lái)為我們對(duì)這個(gè)采樣結(jié)果進(jìn)行解碼,從而得到深度值和法線方向。這個(gè)函數(shù)是DecodeDepthNormal,它在UnityCG.cginc 里被定義為:
inline void DecodeDepthNormal( float4 enc, out float depth, out float3 normal )
{
depth = DecodeFloatRG (enc.zw);
normal = DecodeViewNormalStereo (enc);
}
DecodeDepthNormal 的第一個(gè)參數(shù)是對(duì)深度+法線紋理的采樣結(jié)果,這個(gè)采樣結(jié)果是 Unity 深度和法線信息編碼后的結(jié)果 它的 xy 分量存儲(chǔ)的是視角空間下的法線信息 而深度信息被編碼進(jìn)了 zw 分量。通過(guò)調(diào)用 DecodeDepthNormal 函數(shù)對(duì)采樣結(jié)果解碼后, 我們就可 得到解碼后的深度值和法線。這個(gè)深度值是范圍在[O l] 的線性深度值(這與單獨(dú)的深度紋理中存儲(chǔ) 深度值不同),而得到的法線則是視角空間下的法線方向。同樣也可以通過(guò)調(diào)用 DecodeFloatRG和DecodeViewNormalStereo 來(lái)解碼深度+法線紋理中的深度和法線信息。