全面認識Depth - 這裡有關於Depth的一切
Depth的由來
從PerspectiveProjection 說起
所謂的PerspectiveProjection 其實就是將頂點從view 坐標系下,轉換到NDC下
這裡面包含了兩個步驟,將view 坐標系下的頂點乘以透視矩陣,轉換到Clip 坐標系,得到Clip坐標,
然後統一除以w,得到NDC 坐標。
透視矩陣就不推導了,
這裡再看下Zn和Ze之間的關係
其中n是近裁面,f是遠裁面,r是0.5 *width, t = .5 * height。其中
可以得出
其中A = -(f+n)/(f-n) , B = -2fn/(f-n).兩者的關係可以用下圖來表示
可以看出有下面幾點
- 兩者是非線性的關係。曲線是倒數的關係。(y=1/x)
- 根據曲線的特點可以知道在近裁面附近的Depth變化非常的明顯,越接近遠裁面,Depth變化越小,精度變差,這時候就會出現惱人的Z-Fighting
- 要避免Z-Fighting,n和f之間的距離就要盡量小,盡量將n大一些。
- 這樣的好處雖然遠的地方可能會有Z-fighting,但是近處的地方有了更好的深度精度
可能這還不是很清楚,後面還有更精確的數學推導。
viewportntransformation
viewport transformation都知道,就是將ndc坐標map到屏幕空間,那麼深度呢?從[-1,1]映射到[0,1],(OpenGL中)。0是最近,1是最遠. 默認Dw的值在[0,1]就是可以渲的,當然,也可以用 glDepthRange可以用來指定Zw的取值範圍。
viewport transformation 的計算過程如下
將之前計算的Zndc的式子代進來,得到
Zdepth = (We / Ze) * f*n/(f-n) + 0.5 *n(f+n)/(f-n) + 0.5
在存儲到depth buffder 的時候,需要將Zw乘以一個係數s = (2^n -1) ,其中n是Depth buffer的位數。
所以最終Depthbuff中存儲的是
Zw = s * [ (we / ze) *nf*n/(f-n) + 0.5 * (f+n)/(f-n) + 0.5 ]
下面推導一下關於Depth精度的問題。
將上面的式子進行變換,吧Ze放到左邊
Ze / we = f*n/(f-n) / ((zw / s) - 0.5 * (f+n)/(f-n) - 0.5)
= f * n /n((Zw / s) * (f-n) -n0.5 * (f+n) - 0.5 * (f-n))
= f * n /n((Zw / s) * (f-n) - f)n
假設是一個16位的深度緩衝,n = 0.01,nf = 1000,n那麼s = 65535
對於從Depth Buff采出來的值
Zw = 0 ,則 Ze/We = -nn= -0.01
Zw = s = 65535,則Ze/We= -f =n-1000
接下來去Zw = 1和s-1看看
Zw = 1 => Ze /nWe = -0.01000015
Zw = s-1 =>Zen/ We = -395.90054
仔細觀察上面的數據,發現了一個驚天秘密 –neye坐標系下的深度Ze,從-395.9到-1000在Depth buffder中所對應的範圍是[65535, 65536],這是相當恐怖的,導致的結果就是Z-Fighting,下面就是在Fighting。
對於深度造成的誤差,還可以參考這一篇Learningnto Love your Z中的例子。
由深度緩衝重建view pos
知道一個點的深度還有相機的一些參數,重新算出物體的坐標(不管是世界坐標還是view坐標系下的坐標),微軟的kinect利用的就是這個原理,進行三維重建的。
在Deferred shading還有很多後期效果中,我們都需要一個像素點精確的位置來給這個像素著色。
如上圖所示,已知Camera的pos,view Dir,只需要知道surface上的點到Camera的直線距離t就可以求出了: Peyen= Pcam + t*Dview。
現在問題是t怎麼來。
一種處理方法是將在GBuffer Pass中將t存放在一個rt里,具體的流程如下
1. n在GBuffer pass,計算camera到surface的距離,存到rt裡面。
2. n在Light Pass的VS中,計算vertex到camera位置的View Ray向量。
3. n在PS中,將View Ray 單位化,得到Dview。
4. n採樣rt獲取Camera到Surface Pos的距離t
5. nPeye = Pcam + t*Dview。
簡化的Shader如下
// G-Buffer vertex shadernn// Calculate view space position of the vertex and pass it to the pixel shadernnoutput.PositionVS = mul(input.PositionOS, WorldViewMatrix).xyz;n
// G-Buffer pixel shadernn// Calculate the length of the view space position to get the distance from camera->surfacennoutput.Distance.x = length(input.PositionVS);n
// Light vertex shadernn#if PointLight || SpotLightnn// Calculate the world space position for a light volumennfloat3 positionWS = mul(input.PositionOS, WorldMatrix);nn#elif DirectionalLightnn// Calculate the world space position for a full-screen quad (assume input vertex coordinates are in [-1,1] post-projection space)nnfloat3 positionWS = mul(input.PositionOS, InvViewProjMatrix);nn#endifnn// Calculate the view raynnoutput.ViewRay = positionWS - CameraPositionWS;n
// Light Pixel shadernn// Normalize the view ray, and apply the original distance to reconstruct positionnnfloat3 viewRay = normalize(input.ViewRay);nnfloat viewDistance = DistanceTexture.Sample(PointSampler, texCoord);nnfloat3 positionWS = CameraPositionWS + viewRay * viewDistance;n
但是還有優化的空間,上面提到的這種做法的缺點是浪費了顯存和帶寬,而Depth 就在BackBuffer,如果能夠從Depth中得到view pos的話,優化又進了一步。
由於硬體中的Depth Buffer是除以過w的,所以
第一步要做的就是將Depth buffer中的值恢復到view 坐標系下的Depth,就叫Ze,
由上面推導的結果
Zdepth = (We / Ze) * f*n/(f-n) + 0.5 * (f+n)/(f-n) + 0.5n
通過f和n就可以推算出Ze了。
另一種方法是利用透視矩陣M,根據透視變換
其中Zndc =2 * depth – 1.原理是一樣的。
沒線性化之後的深度渲染出來是這樣的(獅子的鼻子),稍微遠一些的地方深度值都很接近1了,所以很白,只有很近的地方才有灰色的部分。
將線性化之後的depth渲染出來
不是一片白了。
知道深度之後,通過相似三角形就可以求出
接下來就是求View ray,這個放在ps中做,
ViewRay = float3(positionVS.xy / positionVS.z, 1.0f);n
如果要將view坐標系的坐標轉換到世界坐標系,
還有一種簡單暴力的處理方法
vec3 DepthToPos(float depth, vec2 texCoord)nn{nnvec4 ndcspace = vec4(texCoord.x * 2.0 - 1.0, texCoord.y * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);nnvec4 temp = inverse(MatView) * inverse(MatProj) * ndcspace;nnreturn temp.xyz / temp.w;nn}n
直接用矩陣來處理,性能稍微會差一些(待考究)。
參考
12 The Depth Buffer
Learning to Love your Z-buffer.
OpenGL viewport transformation matrix - The Code Crate
詳解MVP矩陣之齊次坐標和ModelMatrix
詳解MVP矩陣之齊次坐標和ViewMatrix
詳解MVP矩陣之齊次坐標和ProjectionMatrix
推薦閱讀:
※請教各位在pc上使用webgl以及opengl渲染yuv數據的性能問題,有何改進方向?
※震驚!時間之神又給了這個古老的API+了0.1
※如何實現魚眼效果?
※在 OpenGL 中應用查找表(LUT)技術時,如何避免紋理數據自動歸一化導致的圖像異常?
※不同GLSL版本之間的區別?