Reversed-Z詳解(轉)

修改並轉自:https://www.cnblogs.com/jackmaxwell/p/6851728.html

在3D渲染管線中,Z這個傢伙幾乎無處不在,如Z-Buffer,Early-Z,Z-Cull,Z-Test,Z-Write等等,稍有接觸圖形學的人都會對這些術語有所耳聞。

  那麼Z到底是什麼呢?首先Z當然可以是任意座標系下的z座標值,但我們這裏要說的Z值,就是深度值,上面幾個包含Z的術語裏面的Z也都是深度值的意思,深度值是物體變換到屏幕空間後的z座標的值,因爲NDC空間轉屏幕空間時並不會改變z值,所以也可以說是NDC空間中z座標的值,有些讀者可能認爲在屏幕空間中Z值已經不存在了,這也是有道理的,因爲屏幕是一個2d空間,沒有z軸,但我們在這裏不做2d,3d區別,認爲都有z軸。在DirectX中,Z值得取值範圍是[0,1],在OpenGL中,其取值範圍爲[-1,1],這篇擬在DirectX環境下討論Z。

  Z值的推導請參見:

http://www.codeguru.com/cpp/misc/misc/graphics/article.php/c10123/Deriving-Projection-Matrices.htm

  這裏我們直接用上文中的一個結果(建議沒推導過的讀者按照這一篇的思路推導一遍,必定會受益匪淺), 即Z值在透視投影后的結果:

Z_ndc * W_clip = Z_clip = Far/(Far - Near) * Z_eye - Far * Near / (Far - Near)

  上面的方程中,$Z$即我們要求的深度值,Z_eye是物體在Eye Space中的z座標,Far是視錐體遠裁剪平面在Eye Space中的z座標,Near是視錐體近裁剪平面在Eye Space中的z座標。由上式可求得Z_clip其實是Clip Space中的z值,除以 W_clip就是透視除法,得到NDC空間的z值,也即是深度值Z):

Z_ndc = Z_clip/W_clip = Far/(Far - Near) - Far · Near / Z_eye * (Far - Near)  ①

  對於Z_eye,我們可以證明其關於物體在World Space中的z值Z_world爲線性關係,那麼根據上式可知Z_ndc與Z_eye、Z_world$皆不爲線性關係。簡單起見,我們取f=1000,n=0.01,有:

Z_ndc ≈ -0.01/ Z_eye + 1 ②

其函數圖像如下(Z_ndc > 0):

圖1

 

  圖中A點表明了Z_eye∈[0.01,0.1]的物體佔用了十分之九(0~0.9)的深度值,這說明在z軸方向上與相機距離爲0.1到1000的物體只用到了十分之一(0.9~1.0)的深度值。這個結果是令人印象深刻的,因爲Z值的分佈太不均勻了,就好像世界上的絕大部分錢都被一個人佔有了一樣。那Z值的分佈情況對於3d渲染來說重要嗎?它意味着什麼呢?

  深度值的不均分分配會導致非常嚴重的後果,那就是Z-Fighting。深度值的取值範圍是[0,1],但這並不代表它存到Z-Buffer裏面後也一定是[0,1]的浮點數,事實上在過去很長一段時間乃至現在很多時候,深度值被保存在16位或者24位的無符號整數中。這裏我們用範圍更小的16位來存儲深度值,因爲這能更好的凸顯出問題。當深度值存儲爲16位無符號整型格式時,其取值範圍是[0,65535],現在我們來算一算當深度值爲65534時,Z_eye是多少?65534映射到[0,1]中,值爲65534/65535。連同f=1000,n=0.01代入①式(①比②可獲得更精確的結果)可解得:Z_eye≈395.9005401718437≈395.9,這說明在Eye Space中在z軸方向上距離相機395.9到1000的物體的深度值都是65535!當兩個物體擁有同樣的深度值時,就會產生非常醜陋的Z-Fighting(詳見:https://en.wikipedia.org/wiki/Z-fighting):

(相同的深度值導致GPU不能正確分辨哪個在前,哪個在後)。

    在3d渲染中,應該儘可能的避免產生Z-Fighting,即應該儘可能的改善深度值分佈的均勻程度。提高用來保存深度值類型的精度可以起到改善z值衝突的情況,比如用24位甚至32位的數據類型來存儲深度會比16位好很多,但由於硬件條件的限制和Z值的非線性增長,目前來說不可能用太多位的硬件出現。有的人也許會想到用浮點數來保存深度值,但其實這毫無作用的,甚至可以說更爲浪費,因爲對於32位浮點數,其尾數(Mantissa)只有23位二進制數,規格化浮點數加上一位保留位也只有24位,這與24位無符號整數表示的精度是一樣的,而浮點數還多使用了8位來存儲其他信息。另外,雖然浮點數本身表示的範圍更廣,但我們知道深度值的範圍不過爲[0,1],當我們用浮點數來存儲深度值時,當然不會再去做映射,這樣,深度值其實只佔到了範圍在[0,1]的浮點數所佔的精度,這勢必就更少了,不過好在浮點數的精度分佈也主要分佈在0值附近,0值附近的符點數擁有更好的精度,但不管怎樣,目前來說想依靠浮點數來改善狀況是不可取的。

  除了提高Z-Buffer的精度以外,還有一些方法也可以改善Z值衝突的情況,如增大近裁面與相機位置z值距離(即n值)就是一種方法。對①式 我們取n=0.1,f=1000(不變),有:

Z_ndc ≈ -0.1/ Z_eye + 1

其圖像如下:

圖2

  對比圖1,圖2的情況好了很多,對比兩個圖中的點A,前0.9的深度值表示的範圍從0.1擴大到了1,說明有更多的深度值用來表示Z_eye比較大的情況,如果還以16位無符號整數來存儲深度值,計算後可得Z_eye在區間[868,1000]時共享65535這個深度值,這比[396,1000]的衝突少了非常多,降低了出現Z-Fighting的概率。而我們僅僅是將n從0.01提高到0.1而已,這對一般的應用場景幾乎不會產生影響。

  既然如此,我們將n值繼續增大,比如取n=100,會怎樣呢?我們將n=100,f=1000(不變)代入1式得:

Z_ndc ≈ -1000 / 9 * Z_eye + 10/9

圖像如下(我必須把x軸壓縮400倍才能截個圖):

圖3

  可以看到0到0.9的深度值已經可以表示到大約=600的時候了,要知道n=0.01的時候, 0.9的深度值$Z_{c}$只能表示到0.1;n=0.1的時候$Z_{c}$只能表示到1。依然將深度值存入到無符號整型中,我們可以計算出當物體的$Z_{c}$∈[999.863,1000]時,它們才共用65535這個深度值,通過取n=100我們很好地改善了Z值的分佈情況。至少看起來已經是個很好——甚至可以說近乎完美的辦法了。但是,事實並非如此,由於取得n=100,我們捨棄了整個$Z_{c}$∈[0,100]的物體,我們將永遠看不到那些離相機z軸距離少於100的物體!增大近裁剪面的值以換取深度值的分佈均勻程度,難言利弊得失。

    難道就沒有更好的改善深度值分佈的辦法了嗎?當然有了,辦法就是神奇的Reversed-Z,Reversed-Z的做法其實是很簡單的,即將原本近裁剪平面映射到深度值0,遠裁剪平面映射到深度值1的映射關係反過來,讓近裁剪平面映射到深度值1,遠裁剪平面映射到深度值0。即將[n,f]映射到[1,0],按照上文給出的投影矩陣推導鏈接中的方法,我們可以推導出Reversed-Z的情況下Z與$Z_{c}$的關係(其實就是①式中n與f互換):

Z_ndc = Z_clip/W_clip = Near/(Near - Far) - Far · Near / Z_eye * (Near - Far)

  我們取n=0.1,f=1000,有:

Z_ndc ≈ 0.1 / Z_eye

函數圖像如下:

圖4

    看到上面的圖,細心的讀者可能會發現,這不跟圖2一樣嘛,都是$Z_{c}$=1的時候,深度值Z就用了十分之九(0.9)了,不過是前者是[0, 0.9],這裏是[0.1, 1]而已,有區別嗎?如果我們還是以無符號整型來存儲深度值,的確對我們達成目的沒有幫助,依然是靠近近裁剪平面的少數物體佔據了大多數深度值。但是我說過Reversed-Z是神奇的,它的神奇之處是當它搭配上我前面否定過的浮點數時,Reversed-Z在"提高深度值均分分佈程度" 這件事上就變得非常有效了。

    讓我們回到浮點數,前面有提到過 "0值附近的符點數擁有更好的精度",這是有依據的,浮點數具體介紹請參考維基百科:https://en.wikipedia.org/wiki/IEEE_floating_point,這裏以單精度符點類型做簡單說明。規約化單精度浮點數的有效位數只有7位(實際是7點多位,這裏簡單起見取7),當一個浮點數小於1的時候,它可以確保有6位小數位是精確的,也就是說,在(0,1)這個開區間內至少可以包含999999(6位)個誤差允許的單精度浮點數,1~9同理,但由於非規約化浮點數(主要是在0值左右)的存在,使得(0,1)這個區間內的浮點數個數要比(1,2),(2,3)…(9,10)這些區間內的符點數要多。在(10, 11)這個區間內,由於整數位佔去了兩位,所以這個區間內至少只可以包含99999(5位)個有效單精度浮點數,以此類推,(100,101)開區間內包含9999個有效單精度浮點數,(1000,1001)開區間內包含999個有效單精度浮點數等等,當數量級來到[1000000,1000001]時(注意這裏是閉區間),這個區間內能保證有效的單精度浮點數不過就兩個:1000000與1000001本身。這說明浮點數的分佈與深度值的分佈一樣是不均與的,越靠近0的浮點數分佈越密集,越遠離0的浮點數分佈越稀疏:

浮點數分佈情況圖

  當我們用正常的Z值系統([n,f]映射到[0,1])與浮點數配合時,符點數沒有任何幫助(原諒這個不一樣的畫風,由於我還不太會使用GeogeBra作圖,我把本文的主要參考文章Depth Precision Visualized的圖拿過來用了):

圖5

  圖5中z1到z2這麼遠的距離依然只共享一個深度值0.99。

    

但當我們將Reversed-Z([n,f]映射到[1,0])與浮點數結合起來,情況就變成了:

圖6

隨着Z_eye的增大,深度值Z的降幅越來越小,看似又要陷入精度不夠的死衚衕,但浮點數的分佈規律恰好彌補了這一不足,使得較大的也有足夠的精度表示,圖6中z1到z2比之圖5中多獲得了5個深度值。這樣,距離相機近和遠的物體分得的深度值就比較平均了,變相的實現了"改善深度值分佈狀況"這一目的,從而也達到了降低Z-Fighting出現的概率(是的,雖然Reversed-Z這麼神奇,但Z-Fighting還是不能完全避免的,雖然概率已經降到很低)。

作爲依賴Unity引擎的開發者,很高興看到Unity在其5.5以及以後的版本中引入了Reversed-Z的做法,在這裏也提醒一下大家以後爲Unity寫shader的時候,如果用到深度值Z,一定要記得 [n,f] 是映射到[1,0],否則就會寫出錯誤的效果J。

 

參考與說明:

本文參考:Depth Precision Visualized,對Reversed-Z進行思考與分析,希望能對讀者有所幫助。

文中的函數圖像使用GeoGeBra軟件繪製,公式用LaTex 語法寫成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章