OpenGL學習腳印:深度測試(depth testing)

分類:IT技術 時間:2016-10-17

寫在前面
上一節我們使用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,開啟了深度緩沖區寫入。

可視化深度值

在可視化深度值之前,首先我們要明白,這裏的深度值,實際上是屏幕坐標系下的zwin坐標,屏幕坐標系下的(x,y)坐標分別表示屏幕坐標系下以左下角(0,0)為起始點的坐標。zwin我們如何獲取呢? 可以通過著色器的輸入變量gl_FragCoord.z來獲取,這個gl_FragCoord的z坐標表示的就是深度值。

我們在著色器中以這個深度值為顏色輸出:

  // 原樣輸出
float asDepth()
{
  return gl_FragCoord.z;
}
void main()
{
    float depth = asDepth();
    color = vec4(vec3(depth), 1.0f);
}

輸出後的效果如下圖所示:
默認深度值可視化

可以看到圖中,只有離觀察者較近的部分有些黑色,其余的都是白色。這是因為深度值zwinzeye是成非線性關系的,在離觀察者近的地方,精確度較高,zwin值都保持在較小範圍,成黑色。但是一旦超出一定距離,精確度變小,zwin值都擠在1.0附近,因此成白色。當我們向後移動,拉遠場景與觀察者的距離後,zwin值都落在1.0附近,整個場景都變成白色,如下圖所示:
當拉遠場景與觀察者距離後深度值都變為1.0

作為深度值的可視化,我們能不能使用線性的關系來表達zwinzeye ? 這裏我們做一個嘗試,從zwinz_{eye}$。在投影矩陣和視口變換矩陣一節,我們計算出了相機坐標系下坐標和規範化設備坐標系下坐標之間的關系如下:

zndc=?f+nf?nzeye?2fnf?n?zeye=f+nf?n+2fn(f?n)zeye(1)

在OpenGL中從規範化設備坐標系轉換到屏幕坐標系使用函數主要是:
glViewport(GLint sx , GLint sy , GLsizei ws , GLsizei hs);
glDepthRangef(GLclampf ns , GLclampf fs );

繼而可以得到規範化設備坐標系和屏幕設備坐標系之間的關系如下:

zwin=fs?ns2zndc+fs+ns2(2)

默認情況下glDepthRange函數的n=0,f=1,因此從(2)式可以得到:
zwin=12zndc+12(3)
zndc=2zwin?1(4)

從式子(1)我們可以得到:
zeye=2fnzndc(f?n)?(f+n)(5)

上面的式子(5)如果用來作為深度值,由於結果是負數,會被截斷到0.0,結果都是黑色,因此我們對分母進行反轉,寫為式子(6)作為深度值。

zeye=2fn(f+n)?zndc(f?n)(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);
}

使用zwinzeye線性關系得到深度值,繪制的效果如下圖所示:
深度值線性可視化

很多網絡教程都近似表達zwinzeye的非線性關系,用來可視化,我們可以從(4)(6)得到非線性關系:
zwin=1n?1zeye1n?1f(7)
在著色器中實現為:

   // 非線性輸出
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

實際使用時不使用zwinzeye的線性關系,因為在場景中,近處的物體,我們想讓它看的清楚,自然 要求精度高;但是遠處的物體,我們不需要很清晰的看到細節,因此精確度不必和近處的物體一樣。使用公式(7)繪制的zwinzeye關系圖如下所示(來自:www.learnopengl.com Depth testing):

非線性深度值關系圖

我們看到,zeye在[1.0,2.0]範圍內時zwin保持在0.5的範圍內,精確度高。而當zeye超過10.0後,zwin的值就在0.9以後了,也就是說zwin在[10.0,50.0]範圍內的深度值將擠在[0.9,1.0]這麽一個小的範圍內,精確度很低。

實際上深度值是通過下式計算的(來自:depth buffer faq):
zwin=S?(fn(f?n)zeye+12f+nf?n+12)(8)
其中,S=2d?1,d表示深度緩沖區的位數(例如16,24,32)。這個式子的右邊括號部分是由(1)(4)得到,同時放大S倍數後得到最終的深度值(可以參看depth buffer faq)。

找到兩個特殊點,zwin=1zwin=S?1,得到:
zwin=1=>zeye=f?n/((1/s)?(f?n)?f)
zwin=S?1=>zeye=f?n/(((s?1)/s)?(f?n)?f)

取n = 0.01, f = 1000 and s = 65535,那麽有:
zwin=1=>zeye=?0.01000015
zwin=S?1=>zeye=?395.90054

註意OpenGL中相機坐標系的+Z軸指向觀察者,因此上面的坐標是負數。從上面的值我們可以看到,當zeye在[-395,-1000]範圍內時,深度值將全部擠在65534或者65535這兩個值上,也就是說幾乎60%的zeye只能分配1到2個深度值,可見當zeye超過一定範圍後,精度值是相當低的。(這個例子原本解釋來自depth buffer faq)。

當深度值精確度很低時,容易引起ZFighting現象,表現為兩個物體靠的很近時確定誰在前,誰在後時出現了歧義。例如上面繪制的平面和立方體,在y=-0.5的位置二者貼的很近,如果進入立方體內部觀察,則出現了ZFighting現象,立方體的底面紋理和平面的紋理出現了交錯現象,如下圖所示:

ZFighting1

ZFighting2

(如果你要親自觀察這個現象,只需要在本節代碼中,將相機位置放在立方體內部,稍微調整鼠標觀察角度就可以了)。

預防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 源代碼 觀察者 緩沖區 三角形

文章來源:


ads
ads

相關文章
ads

相關文章

ad