Streak后處理效果開了個頭,這段時間準(zhǔn)備寫一些后處理相關(guān)的內(nèi)容,深度是后處理里面一個非常重要的點,我們需要將深度數(shù)據(jù)在不同空間進行轉(zhuǎn)換然后比較計算。我之前對深度的理解也只是基本理解原理然后把后處理代碼寫出來了,但對很多知識點非常迷惑,特別是深度重建世界坐標(biāo)那里,后續(xù)找到數(shù)學(xué)推導(dǎo)過程才最終解惑。
1.Babylon怎么獲取深度
Babylon中的深度分為Linear深度和NoLinear深度,在講Babylon怎么獲取深度之前,我們先簡單講一下各個空間系。當(dāng)我們繪制任意模型(后處理可以認(rèn)為是繪制了一個占滿屏幕的面片),我們先獲取的Position數(shù)據(jù)(模型空間),這是一個相對于模型原點的空間系;通過乘以(左乘)world矩陣變換到世界空間,這是一個相對世界原點的坐標(biāo)系;這是以再乘以view矩陣變換到觀察空間,這個是相對于攝像機的空間系......依次向右計算變化,NDC的數(shù)據(jù)范圍為-1到1,裁剪空間的范圍為0-1。當(dāng)我們獲取一級的數(shù)據(jù),可以通過向右計算變換得到下一級的數(shù)據(jù),也可以依次向左乘以對應(yīng)變換逆矩陣獲取上一級數(shù)據(jù)。

1.1Linear深度
顧名思義,這種模式的深度數(shù)據(jù)是線性的,它的范圍是0-1,它的值與片元離攝像機的距離等線性比例關(guān)系。通過查詢Babylon源碼,線性深度vDepthMetric是從gl_Position(投影空間數(shù)據(jù))歸一化生成的,其中depthValues.x的值為camera.minZ,depthValues.y的值為camera.minZ+camera.maxZ。在非反轉(zhuǎn)深度的情況下,深度值越大,片元離攝像機距離越遠(yuǎn)。
//Vertex
gl_Position = viewProjection * worldPos;
#ifdef USE_REVERSE_DEPTHBUFFER
vDepthMetric = ((-gl_Position.z + depthValues.x) / (depthValues.y));
#else
vDepthMetric = ((gl_Position.z + depthValues.x) / (depthValues.y));
#endif
//Fragment
#ifdef NONLINEARDEPTH
#ifdef PACKED
gl_FragColor = pack(gl_FragCoord.z);
#else
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);
#endif
#else
#ifdef PACKED
gl_FragColor = pack(vDepthMetric);
#else
gl_FragColor = vec4(vDepthMetric, 0.0, 0.0, 1.0);
#endif
#endif
1.2NoLinear深度
顧名思義,這種模式的深度數(shù)據(jù)是非線性的,它的范圍是0-1,數(shù)據(jù)的分布呈現(xiàn)近密遠(yuǎn)疏的效果,絕大部分的數(shù)據(jù)會密集的堆在1的附近,當(dāng)深度紋理是半浮點數(shù)時,離攝像機沒多遠(yuǎn)開始,精度就會開始非常糟糕。通過上述代碼可以看出,非線性深度數(shù)據(jù)來源于gl_FragCoord.z(裁剪空間數(shù)據(jù)),數(shù)據(jù)值越大,離攝像機距離越遠(yuǎn)。
1.3什么是pack
unpack情況下,使用R通道輸出紋理,剩下三個通道的數(shù)據(jù)全部浪費,我們使用單通道紋理就可以避免GBA通道的浪費。但是有些情況下webgl不支持單通道紋理,unpack就會造成內(nèi)/顯存的浪費,pack將float轉(zhuǎn)換成從低到高的4個byte,分別存在四個通道里面,就可以避免通道的浪費。需要注意的是,如果希望用pack存其它數(shù)據(jù),該pack會將大于1的值歸1,并不適用于儲存大于1的數(shù)據(jù)。
vec4 pack(float depth)
{
const vec4 bit_shift = vec4(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0);
const vec4 bit_mask = vec4(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
vec4 res = fract(depth * bit_shift);
res -= res.xxyz * bit_mask;
return res;
}
float unpack(vec4 color)
{
const vec4 bit_shift = vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0);
return dot(color, bit_shift);
}
2.深度重建世界坐標(biāo)
深度的常見用途之一是重建世界坐標(biāo)然后做一些基于世界坐標(biāo)的效果,例如空間掃光特效,后處理體積云,后處理貼花等。重建世界坐標(biāo)的方法有很多,這里我使用一種方便配置計算的方法:NDC轉(zhuǎn)換世界坐標(biāo),具體數(shù)學(xué)推導(dǎo)過程可以看鏈接的博客。在這種方法中,我們只需要籌備NDC空間的坐標(biāo),然后通過下列公式就可以計算出世界坐標(biāo)。
vec4 clip = invertProjectionView*ndc;
vec3 worldPos = (clip/clip.w).xyz;
2.1 Linear
由1.1,Linear的Depth深度是歸一化于投影空間,我們先得還原投影空間的z值,然后將其轉(zhuǎn)換到NDC空間,NDC的x和y可以通過vUV來還原,這樣我們就可以獲得每個片元對應(yīng)的NDC空間的坐標(biāo)。PlayGround
//還原Projection空間Z
float zclip = d * (zmax + zmin) - zmin;
//轉(zhuǎn)換到NDC的Z
float zndc = projection[2].z / (1.0 - projection[3].z / zclip);
//右手坐標(biāo)系中
// float zndc = -projection[2].z / (1.0 + projection[3].z / zclip);
//還原最終NDC
vec4 ndc = vec4((vUV.x - 0.5) * 2.0,
(vUV.y - 0.5) * 2.0,
zndc,
1.0);

2.2 NoLinear
由1.2,NoLinear的深度是從裁剪空間獲取的,裁剪空間的范圍是0到1,歸一化的范圍是-1到1,(需要注意的是,部分情況(Webgpu)NDC的Z的范圍是0-1,使用IS_NDC_HALF_ZRANGE進行區(qū)分。)變換就比較簡單。由圖可見float16的時候精度就不已經(jīng)支持世界坐標(biāo)的重建:PlayGround,當(dāng)設(shè)置為float32時,可以實現(xiàn)世界坐標(biāo)的重建,但旋轉(zhuǎn)攝像機的時候依舊會因為精度問題造成閃爍:PlayGround。
float zndc = d;
......
#ifdef IS_NDC_HALF_ZRANGE
zndc,
#else
(zndc - 0.5) * 2.0,
#endif
1.0);


3.基于深度的簡單應(yīng)用場景舉例
3.1 水體岸邊效果

3.2基于深度的Mask獲取,可以結(jié)合RenderTargetTexture獲取模型的遮罩。
