Z深度相关知识
渲染中深度信息很重要,但是也很讓人迷惑,透視投影是什么,為什么要做透視除法,view空間,clip空間,ndc空間對應的z值又代表什么,這里簡單總結下。
一.頂點變換的完整過程
二.View空間下的頂點和Z值
? ? ? ? 輸入頂點在經過MV矩陣變換后,變化到View空間,也就是相機視錐空間(上圖中的Eye Space),在這個空間內的z值代表著頂點到攝像機Z方向的距離(也就是相機到幕布的垂直距離,下圖中的Depth而不是Distance),在View空間內是線性的?
在該空間內,假設有一點(,?,) ,轉成齊次坐標后為(,?,,1)
?三.Clip空間下的頂點和Z值
? ? ? ? 在這一步,我們暫且先放下,只假設該空間內的頂點為(,,,)下看看接下來我們目標NDC空間所需的頂點和Z值長什么樣。
?四.NDC空間下的頂點和Z值
? ? ? ? NDC標準空間下的頂點值我們記為(,,, 1).在渲染管線NDC空間中,
范圍為-1到1,?在在openGL環境下是范圍是-1到1,DirectX中是0到1。
和,和之間是存在聯系的:
?具體推導過程詳見這篇文章:透視投影矩陣的推導 - bluebean - 博客園
這里單說結論:其中即,x即,y方向同理,下圖中的z為
?已知了他們的關系,假設存在一個矩陣變換,使得View空間中的頂點?(,?,,1)可以直接轉換到(,,, 1),那么該矩陣續滿足(變量腦補替換下,xyz對應):
我們發現求解
很難找出合適的m00、m02,因為左邊x和z是以加法的形式相鄰,右邊z確成為了x的分母。
解決方法:將右邊的以四維列向量表示的坐標每一項乘以z,所以有:
?所以可以求得矩陣為
再根據其他兩個特殊條件,在近平面時為-1.?在遠平面時為1.
最后求得投影矩陣為
?將這樣的矩陣乘以視錐體內的一個頂點坐標(,?,,1),得到一個新的向量(,,,),再將這個向量的每個分量除以第四個分量()(即透視除法),這樣就可以得到頂點映射到NDC的坐標(,,, 1),這時與不再是線性關系,?與的倒數是線性關系
五.再梳理一遍
那么以為的個人理解,開始回推一下整個流程。
1.為什么要有透視除法?
因為為了求出消除Z分量影響的投影矩陣
2.為什么要用消除Z分量影響的投影矩陣?
因為這樣整個渲染中,一個相機只需要一個投影矩陣,否則,每個頂點都要傳入其帶有Z分量影響的投影矩陣。
3.流程再梳理:
?(,?,,1),是線性的經過投影矩陣變換?
變為(,,,)
注意矩陣中的第四行三列,值為1,也即?=?。大家在很多shader中經常會看到用Clip中w分量去計算一些東西,原理其實就是這樣,其實它存的就是View空間中的值大小。
(,,,)=?(,,,????? ??),在Clip空間中與還是線性關系。
并且的范圍均為-到(Opengl),在Clip空間會進行裁剪,之后
(,,,)經過透視除法,除以后,就得到了NDC空間中的坐標
?(,,, 1)。這時與不再是線性關系,?與的倒數是線性關系
六.Unity中相關的一些問題
1.VertexShader中的頂點輸出是啥?
????????Vertex Shader的輸出在Clip Space,即?(,,,????? ??)
2.Fragment Shader的輸入是在什么空間?
????????不是NDC,而是屏幕空間Screen Space。
我們前面說到Vertex Shader的輸出在Clip Space,接著GPU會自己做透視除法變到NDC。這之后GPU還有一步,應用視口變換,轉換到Screen Space,輸入給Fragment Shader:
(Vertex Shader) => Clip Space => (透視除法) => NDC => (視口變換) => Screen Space => (Fragment Shader)
3.LinearEyeDepth&Linear01Depth
NDC空間中的深度值(深度貼圖中存儲的值(范圍為0到1,Opengl需要從-1到1映射到0到1))如何能反推得到View空間中的深度值呢,具體推倒見該文章Unity Shader-深度相關知識總結與效果實現(LinearDepth,Reverse Z,世界坐標重建,軟粒子,高度霧,運動模糊,掃描線效果)_puppet_master的專欄-CSDN博客公式為
(視空間) = 1 / (param1 * ?+ param2)?
其中param1 =?(N - F)/ NF,param2 = 1 / N。
?Unity自帶Shader中關于深度值LinearEyeDepth的處理:
// Z buffer to linear depth inline float LinearEyeDepth( float z ) {return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w); }// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt) // x = 1-far/near // y = far/near // z = x/far // w = y/far float4 _ZBufferParams;_ZBufferParams.z = _ZBufferParams.x / far = (1 - far / near)/ far = (near - far) / near * far
_ZBufferParams.w = _ZBufferParams.y / far = (far / near) / far = 1 / near
我們推導的param1 = _ZBufferParams.z,param2 = _ZBufferParams.w,實際上Unity中LinearEyeDepth就是將透視投影變換的公式反過來,用zbuffer圖中的屏幕空間depth反推回當前像素點的相機空間深度值。
下面再來看一下Linear01Depth函數,所謂01,其實也比較好理解,我們上面得到的深度值實際上是真正的視空間Z值,但是這個值沒有一個統一的比較標準,所以這個時候依然秉承著映射大法好的理念,把這個值轉化到01區間即可。由于相機實際上可以看到的最遠區間就是F(遠裁剪面),所以這個Z值直接除以F即可得到映射到(0,1)區間的Z值了:
Z(視空間01) = Z(視空間) / F = 1 / (((N - F)/ N) * depth + F / N)
Z(視空間01) = 1 / (param1 * depth + param2),param1 = (N - F)/ N = 1 - F/N,param2 = F / N。
再來看一下Unity中關于Linear01Depth的處理:
?
可以看出我們推導的param1 = _ZBufferParams.x,param2 = _ZBufferParams.y。也就是說,Unity中Linear01Depth的操作值將屏幕空間的深度值還原為視空間的深度值后再除以遠裁剪面的大小,將視空間深度映射到(0,1)區間。
Unity應該是OpenGL風格(矩陣,NDC等),上面的推導上是基于DX風格的DNC進行的,不過,如果是深度圖的話,不管怎么樣都會映射到(0,1)區間的,相當于OpenGL風格的深度再進行一步映射,就與DX風格的一致了。
4.unity_CameraProjection和UNITY_MATRIX_P (float4x4)
Unity shader 里面,要獲取投影矩陣,有兩個變量:unity_CameraProjection (float4x4) 和 UNITY_MATRIX_P (float4x4)。需要注意的是,這兩個矩陣的內容實際上不一樣。unity_CameraProjection:
?
?UNITY_MATRIX_P:
他們兩個的區別是:unity_CameraProjection一直是MainCamera的投影矩陣,并且是OpenGL規范的。
UNITY_MATRIX_P是當前渲染投影矩陣,不一定是OpenGL并且也不一定是MainCamera的
?What's difference between UNITY_MATRIX_P and unity_CameraProjection? - Unity Forum
?5.向量從 camera space 映射到 clip space / screen space
如果想要把一個camera space的向量,從 camera space 映射到 clip space / screen space,需要采取的操作是:用投影矩陣(unity_CameraProjection) 去乘那個向量(向量的齊次坐標 w 分量為0),例如:
6.ComputeScreenPos
inline float4 ComputeScreenPos (float4 pos)
{
? ? float4 o = pos * 0.5f;
? ? o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
? ? o.zw = pos.zw;
? ??
? ? return o;
}
首先,該函數傳入的參數pos為頂點變換到齊次坐標系下的坐標(ClipSpace),也就是說,在shader中,你需要這么使用:
o.pos = UnityObjectToClipPos(v.vertex); o.screenPos = ComputeScreenPos(o.pos);ComputeScreenPos返回的值是齊次坐標系下的屏幕坐標值,其范圍為[0, w]。那么為什么Unity要這么做呢?Unity的本意是希望你把該坐標值用作tex2Dproj指令的參數值,tex2Dproj會在對紋理采樣前除以w分量。當然你也可以像下面代碼那樣自己除以w分量后進行采樣,但是效率不如內置指令tex2Dproj:
pos = UnityObjectToClipPos(v.vertex); screenPos = ComputeScreenPos(o.pos); tex2D(_ScreenTex, float2(screenPos.xy / screenPos.w))七.其它相關知識
1.ZBuffer的精度問題
2.Reverse-Z
3.Z&1/Z
這些相關知識就查看這篇博客吧Unity Shader-深度相關知識總結與效果實現(LinearDepth,Reverse Z,世界坐標重建,軟粒子,高度霧,運動模糊,掃描線效果)_puppet_master的專欄-CSDN博客,大佬已經寫的非常詳細了,在此感謝大佬們的無私分享
參考資料:
透視投影矩陣的推導 - bluebean - 博客園
Unity Shader-深度相關知識總結與效果實現(LinearDepth,Reverse Z,世界坐標重建,軟粒子,高度霧,運動模糊,掃描線效果)_puppet_master的專欄-CSDN博客
?寫給大家看的“透視除法” —— 齊次坐標和投影 - 簡書
實時渲染中的坐標系變換(4):投影變換-2 - 知乎
?掘金
Unity Shader中的ComputeScreenPos函數_linuxheik的專欄-CSDN博客
?What's difference between UNITY_MATRIX_P and unity_CameraProjection? - Unity Forum
八.留個疑問待研究
為什么剪裁是在Clip空間而不是NDC空間?
有知道的大佬可以指教下,知乎上的一個回答是:
?
????????
總結
- 上一篇: 《桃花源记2》首部资料片将推副本X计划
- 下一篇: 堡垒之夜世界杯企鹅资格赛选手分析: 路人