在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值在透視投影后的結果:
$$ZZ_{c}={\frac{f}{f-n}Z_{c}}-{\frac{fn}{f-n}}$$
上面的方程中,$Z$即我們要求的深度值,$Z_{c}$是物體在Eye Space中的z坐標,f是視錐體遠裁剪平面在Eye Space中的z坐標,n是視錐體近裁剪平面在Eye Space中的z坐標。由上式可求得($ZZ_{c}$其實是Clip Space中的z值,除以$Z_{c}$就是透視除法,得到NDC空間的z值,也即是深度值Z):
?$$Z={\frac{f}{f-n}}-{\frac{fn}{(f-n)*Z_{c}}}\quad?①$$
對于$Z_{c}$,我們可以證明其關于物體在World Space中的z值$Z_{w}$為線性關系,那么根據上式可知$Z$與$Z_{c}$、$Z_{w}$皆不為線性關系。簡單起見,我們取f=1000,n=0.01,有:
?$$Z≈-{\frac{0.01}{Z_{c}}}+1\quad②$$
其函數圖像如下($Z_{c}>0$):
圖1
?
圖中A點表明了$Z_{c}$∈[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_{c}$是多少?65534映射到[0,1]中,值為65534/65535。連同f=1000,n=0.01代入①式(①比②可獲得更精確的結果)可解得:$Z_{c}$≈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≈-\frac{0.1}{Z_{c}}+1$$
其圖像如下:
圖2
對比圖1,圖2的情況好了很多,對比兩個圖中的點A,前0.9的深度值表示的范圍從0.1擴大到了1,說明有更多的深度值用來表示$Z_{c}$比較大的情況,如果還以16位無符號整數來存儲深度值,計算后可得$Z_{c}$在區間[868,1000]時共享65535這個深度值,這比[396,1000]的沖突少了非常多,降低了出現Z-Fighting的概率。而我們僅僅是將n從0.01提高到0.1而已,這對一般的應用場景幾乎不會產生影響。
既然如此,我們將n值繼續增大,比如取n=100,會怎樣呢?我們將n=100,f=1000(不變)代入1式得:
?$$Z=-\frac{1000}{9Z_{c}}+\frac{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={\frac{n}{n-f}}-{\frac{fn}{(n-f)*Z_{c}}}$$
我們取n=0.1,f=1000,有:
$$Z≈\frac{0.1}{Z_{c}}$$
函數圖像如下:
圖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_{c}$的增大,深度值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 語法寫成。