寫在前面
上一節我們使用AssImp加載了3d模型,效果已經令人激動了。但是繪制效率和場景真實感還存在不足,接下來我們還是要保持耐心,繼續學習一些高級主題,等學完後面的高級主題,我們再次來改進我們加載模型的過程。本節將會學習深度測試,文中示例程序源代碼均可以在我的github下載。
本節內容整理自
1.www.learnopengl.com Depth testing
2.depth buffer faq
3.Z buffer 和 W buffer 簡介
通過本節可以了解到
- 為什麽需要深度緩沖區?
- OpenGL中怎麽使用深度緩沖區 ?
- 可視化深度值
- 深度值的精度問題-ZFighting
問題背景
在繪制3D場景的時候,我們需要決定哪些部分對觀察者是可見的,或者說哪些部分對觀察者不可見,對於不可見的部分,我們應該及早的丟棄,例如在一個不透明的墻壁後的物體就不應該渲染。這種問題稱之為隱藏面消除(Hidden surface elimination),或者稱之為找出可見面(Visible surface detemination)。
解決這一問題比較簡單的做法是畫家算法(painter’s algorithm)。畫家算法的基本思路是,先繪制場景中離觀察者較遠的物體,再繪制較近的物體。例如繪制下面圖中的物體(來自Z buffer 和 W buffer 簡介),先繪制紅色部分,再繪制黃色,最後繪制灰色部分,即可解決隱藏面消除問題。
使用畫家算法時,只要將場景中物體按照離觀察者的距離遠近排序,由遠及近的繪制即可。畫家算法很簡單,但另一方面也存在缺陷,例如下面的圖中,三個三角形互相重疊的情況,畫家算法將無法處理:
解決隱藏面消除問題的算法有很多,具體可以參考Visible Surface Detection。結合OpenGL,我們使用的是Z-buffer方法,也叫深度緩沖區Depth-buffer。
深度緩沖區(Detph buffer)同顏色緩沖區(color buffer)是對應的,顏色緩沖區存儲的像素的顏色信息,而深度緩沖區存儲像素的深度信息。在決定是否繪制一個物體的表面時,首先將表面對應像素的深度值與當前深度緩沖區中的值進行比較,如果大於等於深度緩沖區中值,則丟棄這部分;否則利用這個像素對應的深度值和顏色值,分別更新深度緩沖區和顏色緩沖區。這一過程稱之為深度測試(Depth Testing)。在OpenGL中執行深度測試時,我們可以根據需要指定深度值的比較函數,後面會詳細介紹具體使用。
OpenGL中使用深度測試
深度緩沖區一般由窗口管理系統,例如GLFW來創建,深度值一般由16位,24位或者32位值表示,通常是24位。位數越高的話,深度的精確度越好。前面我們已經見過了如何在OpenGL中使用深度測試,這裏復習下過程。首先我們需要開啟深度測試,默認是關閉的:
glEnable(GL_DEPTH_TEST);
另外還需要在繪制場景前,清除顏色緩沖區時,清除深度緩沖區:
glClearColor(0.18f, 0.04f, 0.14f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
清除深度緩沖區的默認值是1.0,表示最大的深度值,深度值的範圍在[0,1]之間,值越小表示越靠近觀察者,值越大表示遠離觀察者。
上面提到了在進行深度測試時,當前深度值和深度緩沖區中的深度值,進行比較的函數,可以由用戶通過glDepthFunc指定,這個函數包括一個參數,具體的參數如下表所示:
函數 | 說明 |
---|---|
GL_ALWAYS | 總是通過測試 |
GL_NEVER | 總是不通過測試 |
GL_LESS | 在當前深度值 < 存儲的深度值時通過 |
GL_EQUAL | 在當前深度值 = 存儲的深度值時通過 |
GL_LEQUAL | 在當前深度值 <= 存儲的深度值時通過 |
GL_GREATER | 在當前深度值 > 存儲的深度值時通過 |
GL_NOTEQUAL | 在當前深度值 不等於 存儲的深度值時通過 |
GL_GEQUAL | 在當前深度值 >= 存儲的深度值時通過 |
例如我們可以使用GL_AWALYS參數,這與默認不開啟深度測試效果是一樣的:
glDepthFunc(GL_ALWAYS);
下面我們繪制兩個立方體和一個平面,通過對比開啟和關閉深度測試來理解深度測試。
當關閉深度測試時,我們得到的效果卻是這樣的:
這裏先繪制立方體,然後繪制平面,如果關閉深度測試,OpenGL只根據繪制的先後順序決定顯示結果。那麽後繪制的平面遮擋了一部分先繪制的本應該顯示出來的立方體,這種效果是不符合實際的。
我們開啟深度測試後繪制場景,得到正常的效果如下:
使用深度測試,最常見的錯誤時沒有使用
glEnable(GL_DEPTH_TEST);
開啟深度測試,或者沒有使用
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);清除深度緩沖區。
與深度緩沖區相關的另一個函數是glDepthMask,它的參數是布爾類型,GL_FALSE將關閉緩沖區寫入,默認是GL_TRUE,開啟了深度緩沖區寫入。
可視化深度值
在可視化深度值之前,首先我們要明白,這裏的深度值,實際上是屏幕坐標系下的
我們在著色器中以這個深度值為顏色輸出:
// 原樣輸出 float asDepth() { return gl_FragCoord.z; } void main() { float depth = asDepth(); color = vec4(vec3(depth), 1.0f); }
輸出後的效果如下圖所示:
可以看到圖中,只有離觀察者較近的部分有些黑色,其余的都是白色。這是因為深度值
作為深度值的可視化,我們能不能使用線性的關系來表達
在OpenGL中從規範化設備坐標系轉換到屏幕坐標系使用函數主要是:
glViewport(GLint
glDepthRangef(GLclampf
繼而可以得到規範化設備坐標系和屏幕設備坐標系之間的關系如下:
默認情況下glDepthRange函數的n=0,f=1,因此從(2)式可以得到:
從式子(1)我們可以得到:
上面的式子(5)如果用來作為深度值,由於結果是負數,會被截斷到0.0,結果都是黑色,因此我們對分母進行反轉,寫為式子(6)作為深度值。
對式子(6)的深度值進行歸一化,保持在[0,1]範圍內,則在著色器中實現為:
// 線性輸出結果 float near = 1.0f; float far = 100.0f; float LinearizeDepth() { // 計算ndc坐標 這裏默認glDepthRange(0,1) float Zndc = gl_FragCoord.z * 2.0 - 1.0; // 這裏分母進行了反轉 float Zeye = (2.0 * near * far) / (far + near - Zndc * (far - near)); return (Zeye - near)/ ( far - near); } void main() { float depth = LinearizeDepth(); color = vec4(vec3(depth), 1.0f); }
使用
很多網絡教程都近似表達
在著色器中實現為:
// 非線性輸出 float nonLinearDepth() { float Zndc = gl_FragCoord.z * 2.0 - 1.0; float Zeye = (2.0 * near * far) / (far + near - Zndc * (far - near)); return (1.0 / near - 1.0 / Zeye) / (1.0 / near - 1.0 / far); } void main() { float depth = nonLinearDepth(); color = vec4(vec3(depth), 1.0f); }
這個非線性關系輸出,和利用gl_FragCoord.z作為深度值輸出效果是差不多的。
深度的精確度問題-ZFighting
實際使用時不使用
我們看到,
實際上深度值是通過下式計算的(來自:depth buffer faq):
其中,
找到兩個特殊點,
取n = 0.01, f = 1000 and s = 65535,那麽有:
註意OpenGL中相機坐標系的+Z軸指向觀察者,因此上面的坐標是負數。從上面的值我們可以看到,當
當深度值精確度很低時,容易引起ZFighting現象,表現為兩個物體靠的很近時確定誰在前,誰在後時出現了歧義。例如上面繪制的平面和立方體,在y=-0.5的位置二者貼的很近,如果進入立方體內部觀察,則出現了ZFighting現象,立方體的底面紋理和平面的紋理出現了交錯現象,如下圖所示:
(如果你要親自觀察這個現象,只需要在本節代碼中,將相機位置放在立方體內部,稍微調整鼠標觀察角度就可以了)。
預防ZFighting的方法
1.不要將兩個物體靠的太近,避免渲染時三角形疊在一起。這種方式要求對場景中物體插入一個少量的偏移,那麽就可能避免ZFighting現象。例如上面的立方體和平面問題中,將平面下移0.001f就可以解決這個問題。當然手動去插入這個小的偏移是要付出代價的。
2.盡可能將近裁剪面設置得離觀察者遠一些。上面我們看到,在近裁剪平面附近,深度的精確度是很高的,因此盡可能讓近裁剪面遠一些的話,會使整個裁剪範圍內的精確度變高一些。但是這種方式會使離觀察者較近的物體被裁減掉,因此需要調試好裁剪面參數。
3.使用更高位數的深度緩沖區,通常使用的深度緩沖區是24位的,現在有一些硬件使用使用32位的緩沖區,使精確度得到提高。
當然還有其他方法,這裏不再展開了。
最後的說明
本節了解了深度測試的問題背景,OpenGL中的使用方法。通過可視化深度值和給出深度的計算過程,讓我們了解深度的精確度問題。還有一些問題沒有在本節探討,包括gl_FragCoord,gl_FragDepth的含義和計算方法,等待後面再繼續學習。另外關於fragment,pixel的區別還需要做進一步了解,本文關於這部分的表述還有待改善。
參考資料
1.www.learnopengl.com Depth testing
2.深度值計算 Real depth in OpenGL / GLSL
3.提供了深度值在線計算程序
4.opengl wiki Depth_Buffer_Precision
5.Z-buffering
6.上面提到的線性和非線性的計算方法 SO討論
Tags: testing 源代碼 觀察者 緩沖區 三角形
文章來源: