需要從shader?中獲取深度值,主要涉及到渲染流水線中的以下幾個(gè)節(jié)點(diǎn)
1.觀察空間:觀察空間是以攝像機(jī)所在位置為原點(diǎn)的空間。我們嘗試獲取的深度信息就是在這個(gè)空間之下。
2.裁剪空間(投影空間、齊次裁剪空間):將頂點(diǎn)轉(zhuǎn)換到此空間下來判斷是否在視椎體內(nèi),從而裁剪掉不在攝像機(jī)視野內(nèi)的頂點(diǎn);將頂點(diǎn)變換到此空間下可以方便做后續(xù)的投影工作。
3.屏幕空間:將沒有被裁剪的頂點(diǎn)從裁剪空間轉(zhuǎn)換到此空間,從而最終呈現(xiàn)在屏幕上。
主要涉及到的術(shù)語
1.裁剪矩陣(投影矩陣):
用于將頂點(diǎn)從觀察空間轉(zhuǎn)換到裁剪空間的矩陣。
經(jīng)過裁剪矩陣的操作后,頂點(diǎn)的x、y、z分量則被轉(zhuǎn)換到裁剪空間中。
裁剪空間下的頂點(diǎn)滿足以下條件的,即為在視野內(nèi)的頂點(diǎn):
-w<=x<=w;
-w<=y<=w;
-w<=z<=w;
以上裁剪原理與深度獲取關(guān)系不大,只需要注意:
經(jīng)過裁剪矩陣的操作后,頂點(diǎn)的w?分量保存了頂點(diǎn)在觀察空間下的深度信息(觀察空間的頂點(diǎn)的z?分量)。
2.齊次除法(透視除法):
經(jīng)過裁剪矩陣的操作之后,將裁剪空間下的頂點(diǎn)的x、y、z?分量分別除以w?分量的過程。此過程完成后,裁剪空間下的頂點(diǎn)將會轉(zhuǎn)換到NDC?中,即變換到一個(gè)各分量從-1到1長度為2的立方體內(nèi)部。如果使用的是透視攝像機(jī),因?yàn)閦?分量保留了深度信息,經(jīng)過透視除法以后,越遠(yuǎn)離攝像機(jī)的頂點(diǎn)其xy分量數(shù)值將越小(因?yàn)閣?分量是觀察空間中頂點(diǎn)的z?分量,對于兩個(gè)頂點(diǎn),如果他們x、y?分量相同,而w?分量不同即深度不同,越遠(yuǎn)的頂點(diǎn)經(jīng)過透視除法后,其數(shù)值也就越?。?。齊次除法完成后,頂點(diǎn)的x、y?分量再經(jīng)過簡單的縮放映射即可投射到屏幕空間二維坐標(biāo)中,而z?分量通過d = 0.5*z + 0.5?轉(zhuǎn)變?yōu)?-1?范圍并作為最終會存貯于深度圖中的數(shù)據(jù)。
3.屏幕空間:
渲染管線中最后一個(gè)流程將裁剪空間中的頂點(diǎn)通過透視除法和屏幕映射映射最終從3D?的裁剪空間轉(zhuǎn)換到2D?的空間中,這個(gè)2D?空間就是屏幕空間。屏幕空間是左下角為(0,0),右上角為(screenwidth, screenheight)的二維空間。
Shader?中獲取深度(unity 2019.4.19 urp)
Shader?中將頂點(diǎn)從裁剪空間轉(zhuǎn)換到屏幕空間(即齊次除法和屏幕映射)由底層完成,而獲取深度的原理就是再現(xiàn)這一操作中某些步驟的過程。
通過頂點(diǎn)數(shù)據(jù)可以直接獲得頂點(diǎn)的深度;通過頂點(diǎn)數(shù)據(jù)和屏幕映射公式可以逆推獲得屏幕紋理坐標(biāo),使用屏幕紋理坐標(biāo)可以采樣深度圖,然后將深度圖中的數(shù)據(jù)逆推回模型空間就可以獲得需要的場景深度。
1.頂點(diǎn)著色器中需要完成將頂點(diǎn)從模型空間轉(zhuǎn)換到裁剪空間下的任務(wù)。
2.來到片元著色器中,經(jīng)過Unity?渲染流程的處理,頂點(diǎn)輸出的裁剪空間頂點(diǎn)(SV_POSITION?語義)坐標(biāo)的xy?分量已經(jīng)做了透視除法和屏幕映射處理轉(zhuǎn)換到了屏幕空間當(dāng)中,z 分量也做了透視除法并轉(zhuǎn)換到(0,1)范圍內(nèi),w?分量仍然是觀察空間下的深度值。
獲取頂點(diǎn)的深度值:
裁剪空間頂點(diǎn)坐標(biāo)w?分量就是視角空間下的深度值,將其除以遠(yuǎn)裁面就是0-1?的深度值。
遠(yuǎn)裁剪面的距離可以通過內(nèi)置的_ProjectionParams.z?變量來獲取。?
或者比較傻的辦法:
1.上面講到片元著色器中,裁剪空間中的頂點(diǎn)(SV_POSITION?語義)坐標(biāo)的z?分量已經(jīng)做了透視除法并轉(zhuǎn)換到了(0,1)范圍中,此時(shí)只需要將其逆推回觀察空間即可;
2.利用公式:(n表示遠(yuǎn)裁剪面;f表示近裁剪面;d表示ndc中重映射后的深度值,即第2步驟計(jì)算出的數(shù)值)
zv?= 1/((n-f/n*f)*d + 1/n)?計(jì)算出觀察空間下的深度值,將其除以遠(yuǎn)裁剪面就是0-1范圍的觀察空間下的深度值,公式:
z01=1/((n-f)/n*d + f/n)
以上公式實(shí)際上是將渲染管線處理后的頂點(diǎn)逆推回觀察空間中,實(shí)際上是結(jié)合了投影矩陣z?分量上的處理公式、透視除法公式、屏幕映射公式三個(gè)步驟逆推出來的公式。
之所以說這個(gè)方法很傻,是因?yàn)槔@了一個(gè)圈,因?yàn)轫旤c(diǎn)從裁剪空間轉(zhuǎn)到屏幕空間下又逆推回了觀察空間,之所以要介紹這種辦法,是因?yàn)樵谟龅叫枰@取場景深度值的需求時(shí),我們可以拿到的數(shù)據(jù)(即深度圖)存儲的正是步驟1?中頂點(diǎn)z?分量的值。
由于頂點(diǎn)在轉(zhuǎn)換到裁剪空間時(shí),其w?分量就是觀察空間下的z?分量,如果只需要頂點(diǎn)的深度,還是建議直接使用裁剪空間頂點(diǎn)的w?分量作為深度值。
?
獲取場景的深度值:
1.上面提到在片元著色器中,頂點(diǎn)坐標(biāo)的xy?分量已經(jīng)從裁剪空間轉(zhuǎn)換到了屏幕空間。
2.直接將頂點(diǎn)坐標(biāo)的xy?分量除以屏幕的長寬就可以得到0-1?范圍的uv?坐標(biāo)(屏幕紋理坐標(biāo)),屏幕的長寬可以通過內(nèi)置變量_ScreenParams?獲取。
3.對深度圖進(jìn)行采樣: SAMPLE_TEXTURE2D_X(_CamearaDepthTexture,?sampler_CameraDepthTexture,?uv)
采樣深度圖需要在shader?中作如下聲明:
TEXTURE2D_X_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
4.利用公式:(n表示遠(yuǎn)裁剪面;f表示近裁剪面;d表示深度圖中的數(shù)據(jù),即第四步結(jié)果的x分量)
zv?= 1/((n-f/n*f)*d + 1/n)?計(jì)算出模型空間下的深度值,將其除以遠(yuǎn)裁剪面就是0-1范圍的深度值,公式:
z01=1/((n-f)/n*d + f/n)
也可以在頂點(diǎn)著色器中計(jì)算采樣坐標(biāo),但是因?yàn)閺捻旤c(diǎn)到片元著色器有一個(gè)插值過程,所以不能在頂點(diǎn)著色器中進(jìn)行齊次除法,因此需要乘回w?分量,計(jì)算公式:(vc 為裁剪空間下的頂點(diǎn)坐標(biāo)、vcw為該坐標(biāo)的w分量)
vcw*(vc/vcw*0.5+0.5) => 0.5vc+0.5vcw
這也是內(nèi)置函數(shù)ComputeScreenPos?的實(shí)現(xiàn)。
然后在片元著色器中進(jìn)行齊次除法獲得uv?坐標(biāo),然后從步驟3繼續(xù)往下執(zhí)行
如果在頂點(diǎn)著色器使用了ComputeScreenPos獲得Vs;
那么對于步驟5可以使用unity?內(nèi)部提供的兩個(gè)方法Linear01Depth、LinearEyeDepth?傳入屏幕紋理坐標(biāo)和_ZBufferParams?來獲取觀察空間的場景深度和0-1線性深度,函數(shù)內(nèi)部的原理即為上述步驟5。這也是shaderGraph?中SceneDepth?節(jié)點(diǎn)的做法。_ZBufferParams?是Unity?提供的內(nèi)置變量,里面包含了遠(yuǎn)近裁剪平面相關(guān)的預(yù)計(jì)算。?同理,頂點(diǎn)的坐標(biāo)也可以使用這種方式來獲得,只不過頂點(diǎn)的深度我們可以直接通過齊次除法來取得。
源碼
Shader "Custom/Depth"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Geometry" "RenderPipeline" = "UniversalRenderPipeline"}
LOD 200
Pass
{
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#pragma vertex vert
#pragma fragment frag
struct a2v
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
//結(jié)合ComputeScreenPos 使用
//float4 screenUV : TEXCOORD5;
};
CBUFFER_START(UnityPerMaterial)
CBUFFER_END
TEXTURE2D_X_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
v2f vert(a2v i)
{
v2f o;
o.vertex = TransformObjectToHClip(i.vertex.xyz);
//需要在片元中做透視除法
//o.screenUV = ComputeScreenPos(o.vertex,_ProjectionParams.x);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
//頂點(diǎn)深度
half dv = i.vertex.w;
//頂點(diǎn)01 深度
half dv01 = i.vertex.w / _ProjectionParams.z;
//直接獲取屏幕紋理坐標(biāo)
float2 screenUV = i.vertex.xy / _ScreenParams.xy;
//結(jié)合ComputeScreenPos 計(jì)算屏幕紋理坐標(biāo)
//float2 screenUV = i.screenUV.xy / i.screenUV.w;
//深度圖中存儲的深度,需要逆推回到觀察空間
float dd = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r;
//觀察空間中的0-1場景深度
float ds01 = Linear01Depth(dd, _ZBufferParams);
//觀察空間中的場景深度
float ds = LinearEyeDepth(dd, _ZBufferParams);
return ds01;
}
ENDHLSL
}
}
}