版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.01.06 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對大家能有所幫助。下面內(nèi)容來自歡迎來到OpenGL的世界。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態(tài)機
3. OpenGL 圖形庫使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統(tǒng)之五種不同的坐標系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎(chǔ)光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質(zhì)
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結(jié)
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網(wǎng)格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
深度測試
在坐標系統(tǒng)小節(jié)中,我們渲染了一個3D箱子,并且運用了深度緩沖(Depth Buffer)來防止被阻擋的面渲染到其它面的前面。在這一節(jié)中,我們將會更加深入地討論這些儲存在深度緩沖(或z緩沖(z-buffer)
)中的深度值(Depth Value)
,以及它們是如何確定一個片段是處于其它片段后方的。
深度緩沖就像顏色緩沖(Color Buffer)
(儲存所有的片段顏色:視覺輸出)一樣,在每個片段中儲存了信息,并且(通常)和顏色緩沖有著一樣的寬度和高度。深度緩沖是由窗口系統(tǒng)自動創(chuàng)建的,它會以16、24或32位float的形式儲存它的深度值。在大部分的系統(tǒng)中,深度緩沖的精度都是24位的。
當深度測試(Depth Testing)
被啟用的時候,OpenGL會將一個片段的的深度值與深度緩沖的內(nèi)容進行對比。OpenGL會執(zhí)行一個深度測試,如果這個測試通過了的話,深度緩沖將會更新為新的深度值。如果深度測試失敗了,片段將會被丟棄。
現(xiàn)在大部分的GPU都提供一個叫做提前深度測試(Early Depth Testing)的硬件特性。提前深度測試允許深度測試在片段著色器之前運行。只要我們清楚一個片段永遠不會是可見的(它在其他物體之后),我們就能提前丟棄這個片段。
片段著色器通常開銷都是很大的,所以我們應該盡可能避免運行它們。當使用提前深度測試時,片段著色器的一個限制是你不能寫入片段的深度值。如果一個片段著色器對它的深度值進行了寫入,提前深度測試是不可能的。OpenGL不能提前知道深度值。
深度測試默認是禁用的,所以如果要啟用深度測試的話,我們需要用GL_DEPTH_TEST
選項來啟用它:
glEnable(GL_DEPTH_TEST);
當它啟用的時候,如果一個片段通過了深度測試的話,OpenGL會在深度緩沖中儲存該片段的z值;如果沒有通過深度緩沖,則會丟棄該片段。如果你啟用了深度緩沖,你還應該在每個渲染迭代之前使用GL_DEPTH_BUFFER_BIT
來清除深度緩沖,否則你會仍在使用上一次渲染迭代中的寫入的深度值:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
可以想象,在某些情況下你會需要對所有片段都執(zhí)行深度測試并丟棄相應的片段,但不希望更新深度緩沖。基本上來說,你在使用一個只讀的(Read-only)深度緩沖。OpenGL允許我們禁用深度緩沖的寫入,只需要設(shè)置它的深度掩碼(Depth Mask)設(shè)置為GL_FALSE就可以了:
glDepthMask(GL_FALSE);
注意這只在深度測試被啟用的時候才有效果。
深度測試函數(shù)
OpenGL允許我們修改深度測試中使用的比較運算符。這允許我們來控制OpenGL什么時候該通過或丟棄一個片段,什么時候去更新深度緩沖。我們可以調(diào)用glDepthFunc
函數(shù)來設(shè)置比較運算符(或者說深度函數(shù)(Depth Function)
):
glDepthFunc(GL_LESS);
這個函數(shù)接受下面表格中的比較運算符:
默認情況下使用的深度函數(shù)是GL_LESS
,它將會丟棄深度值大于等于當前深度緩沖值的所有片段。
讓我們看看改變深度函數(shù)會對視覺輸出有什么影響。我們將使用一個新的代碼配置,它會顯示一個沒有光照的基本場景,里面有兩個有紋理的立方體,放置在一個有紋理的地板上。你可以在這里找到源代碼。
在源代碼中,我們將深度函數(shù)改為GL_ALWAYS
:
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);
這將會模擬我們沒有啟用深度測試時所得到的結(jié)果。深度測試將會永遠通過,所以最后繪制的片段將會總是會渲染在之前繪制片段的上面,即使之前繪制的片段本就應該渲染在最前面。因為我們是最后渲染地板的,它會覆蓋所有的箱子片段:
將它重新設(shè)置為GL_LESS
,這會將場景還原為原有的樣子:
深度值精度
深度緩沖包含了一個介于0.0和1.0之間的深度值,它將會與觀察者視角所看見的場景中所有物體的z值進行比較。觀察空間的z值可能是投影平截頭體的近平面(Near)和遠平面(Far)之間的任何值。我們需要一種方式來將這些觀察空間的z值變換到[0, 1]范圍之間,其中的一種方式就是將它們線性變換到[0, 1]范圍之間。下面這個(線性)方程將z值變換到了0.0到1.0之間的深度值:
這里的near和far值是我們之前提供給投影矩陣設(shè)置可視平截頭體的(見坐標系統(tǒng))那個 near 和 far 值。這個方程需要平截頭體中的一個z值,并將它變換到了[0, 1]的范圍中。z值和對應的深度值之間的關(guān)系可以在下圖中看到:
注意所有的方程都會將非常近的物體的深度值設(shè)置為接近0.0的值,而當物體非常接近遠平面的時候,它的深度值會非常接近1.0。
然而,在實踐中是幾乎永遠不會使用這樣的線性深度緩沖(Linear Depth Buffer)
的。要想有正確的投影性質(zhì),需要使用一個非線性的深度方程,它是與 1/z 成正比的。它做的就是在z值很小的時候提供非常高的精度,而在z值很遠的時候提供更少的精度?;〞r間想想這個:我們真的需要對1000單位遠的深度值和只有1單位遠的充滿細節(jié)的物體使用相同的精度嗎?線性方程并不會考慮這一點。
由于非線性方程與 1/z 成正比,在1.0和2.0之間的z值將會變換至1.0到0.5之間的深度值,這就是一個float提供給我們的一半精度了,這在z值很小的情況下提供了非常大的精度。在50.0和100.0之間的z值將會只占2%的float精度,這正是我們所需要的。這樣的一個考慮了遠近距離的方程是這樣的:
如果你不知道這個方程是怎么回事也不用擔心。重要的是要記住深度緩沖中的值在屏幕空間中不是線性的(在透視矩陣應用之前在觀察空間中是線性的)。深度緩沖中0.5的值并不代表著物體的z值是位于平截頭體的中間了,這個頂點的z值實際上非常接近近平面!你可以在下圖中看到z值和最終的深度緩沖值之間的非線性關(guān)系:
可以看到,深度值很大一部分是由很小的z值所決定的,這給了近處的物體很大的深度精度。這個(從觀察者的視角)變換z值的方程是嵌入在投影矩陣中的,所以當我們想將一個頂點坐標從觀察空間至裁剪空間的時候這個非線性方程就被應用了。如果你想深度了解投影矩陣究竟做了什么,我建議閱讀這篇文章。
如果我們想要可視化深度緩沖的話,非線性方程的效果很快就會變得很清楚。
深度緩沖的可視化
我們知道片段著色器中,內(nèi)建gl_FragCoord
向量的z值包含了那個特定片段的深度值。如果我們將這個深度值輸出為顏色,我們可以顯示場景中所有片段的深度值。我們可以根據(jù)片段的深度值返回一個顏色向量來完成這一工作:
void main()
{
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}
如果你再次運行程序的話,你可能會注意到所有東西都是白色的,看起來就想我們所有的深度值都是最大的1.0。所以為什么沒有靠近0.0(即變暗)的深度值呢?
你可能還記得在上一部分中說到,屏幕空間中的深度值是非線性的,即它在z值很小的時候有很高的精度,而z值很大的時候有較低的精度。片段的深度值會隨著距離迅速增加,所以幾乎所有的頂點的深度值都是接近于1.0的。如果我們小心地靠近物體,你可能會最終注意到顏色會漸漸變暗,顯示它們的z值在逐漸變?。?/p>
這很清楚地展示了深度值的非線性性質(zhì)。近處的物體比起遠處的物體對深度值有著更大的影響。只需要移動幾厘米就能讓顏色從暗完全變白。
然而,我們也可以讓片段非線性的深度值變換為線性的。要實現(xiàn)這個,我們需要僅僅反轉(zhuǎn)深度值的投影變換。這也就意味著我們需要首先將深度值從[0, 1]范圍重新變換到[-1, 1]范圍的標準化設(shè)備坐標(裁剪空間)。接下來我們需要像投影矩陣那樣反轉(zhuǎn)這個非線性方程(方程2),并將這個反轉(zhuǎn)的方程應用到最終的深度值上。最終的結(jié)果就是一個線性的深度值了。聽起來是可行的,對吧?
首先我們將深度值變換為NDC,不是非常困難:
float z = depth * 2.0 - 1.0;
接下來使用獲取到的z值,應用逆變換來獲取線性的深度值:
float linearDepth = (2.0 * near * far) / (far + near - z * (far - near));
這個方程是用投影矩陣推導得出的,它使用了方程2來非線性化深度值,返回一個near與far之間的深度值。這篇注重數(shù)學的文章為感興趣的讀者詳細解釋了投影矩陣,它也展示了這些方程是怎么來的。
將屏幕空間中非線性的深度值變換至線性深度值的完整片段著色器如下:
#version 330 core
out vec4 FragColor;
float near = 0.1;
float far = 100.0;
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // back to NDC
return (2.0 * near * far) / (far + near - z * (far - near));
}
void main()
{
float depth = LinearizeDepth(gl_FragCoord.z) / far; // 為了演示除以 far
FragColor = vec4(vec3(depth), 1.0);
}
由于線性化的深度值處于near與far之間,它的大部分值都會大于1.0并顯示為完全的白色。通過在main函數(shù)中將線性深度值除以far,我們近似地將線性深度值轉(zhuǎn)化到[0, 1]的范圍之間。這樣子我們就能逐漸看到一個片段越接近投影平截頭體的遠平面,它就會變得越亮,更適用于展示目的。
如果我們現(xiàn)在運行程序,我們就能看見深度值隨著距離增大是線性的了。嘗試在場景中移動,看看深度值是怎樣以線性變化的。
顏色大部分都是黑色,因為深度值的范圍是0.1的近平面到100的遠平面,它離我們還是非常遠的。結(jié)果就是,我們相對靠近近平面,所以會得到更低的(更暗的)深度值。
深度沖突
一個很常見的視覺錯誤會在兩個平面或者三角形非常緊密地平行排列在一起時會發(fā)生,深度緩沖沒有足夠的精度來決定兩個形狀哪個在前面。結(jié)果就是這兩個形狀不斷地在切換前后順序,這會導致很奇怪的花紋。這個現(xiàn)象叫做深度沖突(Z-fighting),因為它看起來像是這兩個形狀在爭奪(Fight)誰該處于頂端。
在我們一直使用的場景中,有幾個地方的深度沖突還是非常明顯的。箱子被放置在地板的同一高度上,這也就意味著箱子的底面和地板是共面的(Coplanar)。這兩個面的深度值都是一樣的,所以深度測試沒有辦法決定應該顯示哪一個。
如果你將攝像機移動到其中一個箱子的內(nèi)部,你就能清楚地看到這個效果的,箱子的底部不斷地在箱子底面與地板之間切換,形成一個鋸齒的花紋:
深度沖突是深度緩沖的一個常見問題,當物體在遠處時效果會更明顯(因為深度緩沖在z值比較大的時候有著更小的精度)。深度沖突不能夠被完全避免,但一般會有一些技巧有助于在你的場景中減輕或者完全避免深度沖突、
1. 防止深度沖突
第一個也是最重要的技巧是永遠不要把多個物體擺得太靠近,以至于它們的一些三角形會重疊。通過在兩個物體之間設(shè)置一個用戶無法注意到的偏移值,你可以完全避免這兩個物體之間的深度沖突。在箱子和地板的例子中,我們可以將箱子沿著正y軸稍微移動一點。箱子位置的這點微小改變將不太可能被注意到,但它能夠完全減少深度沖突的發(fā)生。然而,這需要對每個物體都手動調(diào)整,并且需要進行徹底的測試來保證場景中沒有物體會產(chǎn)生深度沖突。
第二個技巧是盡可能將近平面設(shè)置遠一些。在前面我們提到了精度在靠近近平面時是非常高的,所以如果我們將近平面遠離觀察者,我們將會對整個平截頭體有著更大的精度。然而,將近平面設(shè)置太遠將會導致近處的物體被裁剪掉,所以這通常需要實驗和微調(diào)來決定最適合你的場景的近平面距離。
另外一個很好的技巧是犧牲一些性能,使用更高精度的深度緩沖。大部分深度緩沖的精度都是24位的,但現(xiàn)在大部分的顯卡都支持32位的深度緩沖,這將會極大地提高精度。所以,犧牲掉一些性能,你就能獲得更高精度的深度測試,減少深度沖突。
我們上面討論的三個技術(shù)是最普遍也是很容易實現(xiàn)的抗深度沖突技術(shù)了。還有一些更復雜的技術(shù),但它們依然不能完全消除深度沖突。深度沖突是一個常見的問題,但如果你組合使用了上面列舉出來的技術(shù),你可能不會再需要處理深度沖突了。
后記
未完,待續(xù)~~~