Unity Shader 獲取深度紋理和法線紋理

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)變化。

在透視投影中,投影矩陣首先對(duì)頂點(diǎn)進(jìn)行了縮放。在經(jīng)過(guò)齊次除法后,透視投影的裁剪空間會(huì)變換到一個(gè)立方體。圖中標(biāo)注了4個(gè)關(guān)鍵點(diǎn)經(jīng)過(guò)投影矩陣變換后的結(jié)果

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

在正交投影中,投影矩陣對(duì)頂點(diǎn)進(jìn)行了縮放。在經(jīng)過(guò)齊次除法后,正交投影的裁剪空間會(huì)變換到一個(gè)立方體。圖中標(biāo)注了4個(gè)關(guān)鍵點(diǎn)經(jīng)過(guò)投影矩陣變換后的結(jié)果

在得到NDC后,深度紋理中的像素值就可以很方便地計(jì)算得到了,這些深度值就對(duì)應(yīng)了NDC中頂點(diǎn)坐標(biāo)的z分量的值。由于NDC中 z分量的范圍在[-1, I], 為了讓這些值能夠存儲(chǔ)在一張圖像中,需要使用下面的公式對(duì)其進(jìn)行映射:
d = 0.5?Z_{ndc} + 0.5
其中,d對(duì)應(yīng)了深度紋理中的像素值,Z_{ndc}對(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)我們使用透視投影的裁剪矩陣P_{clip}對(duì)視角空間下的一個(gè)頂點(diǎn)進(jìn)行變換后,裁剪空間下頂點(diǎn)的z和w分量為:
Z_{clip} = -Z_{view}\frac{Far + Near}{Far - Near}-\frac{2?Near?Far}{Far - Near}
W_{clip} = -Z_{view}

其中,F(xiàn)ar和Near分別是遠(yuǎn)近裁剪平面的距離。然后,我們通過(guò)齊次除法就可以得到NDC下的z分量:
Z_{ndc} = \frac{Z_{clip}}{W_{clip}} = \frac{Far + Near}{Far - Near}+\frac{2?Near?Far}{(Far - Near)?Z_{view}}

而深度紋理中的深度值是通過(guò)下面的公式由NDC 計(jì)算而得的:
d = 0.5?Z_{ndc} + 0.5
由上面的這些式子,可以推導(dǎo)出用d表示而得的Z_{view}的表達(dá)式:
Z_{view} = \frac{1}{\frac{Far-Near}{Near?Far}d-\frac{1}{Near}}

由于在Unity 使用的視角空間中,攝像機(jī)正向?qū)?yīng)的z值均為負(fù)值,因此為了得到深度值的正數(shù)表示,我們需要對(duì)上面的結(jié)果取反,最后得到的結(jié)果如下:
Z_{view} = \frac{1}{\frac{Near-Far}{Near?Far}d+ \frac{1}{Near}}

它的取值范圍就是視錐體深度范圍,即[Near, Far]。如果我們想得到范圍在[0, l]之間的深度值,只需要把上面得到的結(jié)果除以Far即可。這樣,0就表示該點(diǎn)與攝像機(jī)位于同一位置,1表示該點(diǎn)位于視錐體的遠(yuǎn)裁剪平面上。結(jié)果如下:
Z_{01} = \frac{1}{\frac{Near-Far}{Near}d+ \frac{Far}{Near}}

其實(shí),Unity提供了兩個(gè)輔助函數(shù)來(lái)為我們進(jìn)行上述的計(jì)算過(guò)程LinearEyeDepth 和LinearOlDepth。LinearEyeDepth 負(fù)責(zé)把深度紋理的采樣結(jié)果轉(zhuǎn)換到視角空間下的深度值,也就是我們上面得到的Z_{view}。而Linear01Depth則會(huì)返回一個(gè)范圍在[0, 1]的線性深度值,也就是我們上面得到的 Z_{01}。這兩個(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)解碼深度+法線紋理中的深度和法線信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前面學(xué)習(xí)的屏幕后處理技術(shù)都只是在屏幕顏色圖像上進(jìn)行各種操作,但當(dāng)希望得到深度和法線信息時(shí),就無(wú)能為力;邊緣檢測(cè) ,...
    無(wú)職轉(zhuǎn)生者閱讀 1,291評(píng)論 0 1
  • 1.什么是深度紋理? 深度紋理實(shí)際就是一張渲染紋理,只不過(guò)它里面存儲(chǔ)的像素值不是顏色值,而是一個(gè)高精度的深度值。由...
    SwordMaster閱讀 4,373評(píng)論 1 7
  • 本文同時(shí)發(fā)布在我的個(gè)人博客上:https://dragon_boy.gitee.io 獲取深度和法線紋理 原理 深...
    Dragon_boy閱讀 961評(píng)論 0 0
  • 在實(shí)現(xiàn)一些特定的效果,我們需要獲取場(chǎng)景中的深度紋理和法線紋理。雖然,在ShaderLab中,獲取它們很簡(jiǎn)單,但在使...
    usagiowl閱讀 2,296評(píng)論 0 1
  • 屏幕后處理技術(shù) 很多信息受光照影響和別的影響。在一些操作計(jì)算時(shí)不時(shí)很準(zhǔn)確。比較邊緣檢測(cè)。另外一種更好的辦法就是利用...
    李偌閑閱讀 2,586評(píng)論 0 2

友情鏈接更多精彩內(nèi)容