1. 程式人生 > >OpenGL學習腳印:模板測試(stencil testing)

OpenGL學習腳印:模板測試(stencil testing)

寫在前面
上一節介紹了深度測試,本節繼續學習一個高階主題-模板測試(stencil testing)。模板緩衝同之前介紹的顏色緩衝、深度緩衝類似,通過它我們可以實現很多的特效,例如輪廓、鏡面效果,陰影效果等。本節示例程式均可以從我的github下載

通過本節可以瞭解到

  • 模板緩衝的作用
  • 模板緩衝的使用方法-簡單的矩形模板
  • 模板緩衝實現的outline和refleciton效果

模板緩衝的作用

上一節介紹的深度緩衝用於進行深度測試,決定場景中物體的表面是否可見,解決隱藏面消除的問題,簡而言之,就是通過深度測試,OpenGL選擇性渲染片元。模板測試的是另外一種可以以一定標準丟棄片元的方法,這個標準就是藉助模板緩衝和我們指定的測試函式而運作的。實際上渲染管線裡面包括幾種測試,如下圖所示(來自

Improving Shadows and Reflections via the Stencil Buffer):
渲染管線

我們這裡展開上面圖中所有內容,但是我們看到模板測試是在深度測試之前進行的,可以作為一種丟棄片元的輔助方法。

模板緩衝的使用

模板緩衝一般為8位的,存貯整數,最大值為255。在使用的過程中步驟一般時,開啟模板緩衝,繪製一個物體作為我們的模板,這個過程實際上就是寫入模板緩衝的過程;接著我們利用模板緩衝中的值決定是丟棄還是保留後續繪圖中的片元。下面我們建立一個舉行模板,通過矩形模板選擇性地將上一節繪製的場景顯示出來,這個過程示意如下圖所示(來自Stencil testing

):
舉行模板

如圖中所示,模板緩衝中為1的地方我們選擇保留圖形,而其他部分則丟棄,形成最終的效果。

使用模板緩衝需要三個要素:

  • 正確的時間開啟和關閉深度緩衝
  • 模板測試函式
  • 模板測試函式失敗或者成功後的執行的動作

在OpenGL中開啟模板緩衝的方法如下:

   glEnable(GL_STENCIL_TEST);

同時和深度緩衝一樣,需要清除,預設清除時寫入0,可以通過glClearStencil設定清除的指定值。

   glClearColor(0.18f, 0.04f, 0.14f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

一般地繪製模板以及利用模板選擇性地繪製物體時則開啟模板緩衝,繪製其他物體時關閉模板緩衝。使用模板快取的步驟一般如下:

  1. 開啟模板測試
  2. 繪製模板,寫入模板緩衝(不寫入color buffer和depth buffer)
  3. 關閉模板緩衝寫入
  4. 利用模板緩衝中的值,繪製後續場景

與模板測試相關的函式

glStencilMask 函式用於控制模板緩衝區的寫入,使用位掩碼的方式決定是否可以寫入模板緩衝區,使用得較多的是0x00表示禁止寫入,0xFF表示允許任何寫入。

glStencilFunc ,用於指定模板測試的函式,這裡指定是什麼情況下通過模板測試。

API void glStencilFunc(GLenum func, GLint ref, GLuint mask);
func同深度測試一樣,指定函式名GL_NEVER,GL_LESS,GL_LEQUAL等函式。
ref是和當前模板緩衝中的值stencil進行比較的指定值,這個比較方式使用了第三個引數mask,例如GL_LESS通過,當且僅當
滿足: ( ref & mask ) < ( stencil & mask ).GL_GEQUAL通過,當且僅當( ref & mask ) >= ( stencil & mask )。

一般地,我們將上述函式的mask置為0xFF,用於比較,則比較時計算方式比較直觀。例如:

glStencilFunc(GL_EQUAL, 1, 0xFF)

表示當前模板緩衝區中值為1的部分通過模板測試,這部分片元將被保留,其餘地則被丟棄。

glStencilOp 用於指定測試通過或者失敗時執行的動作,例如保留緩衝區中值,或者使用ref值替代等操作。

API void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
sfail表示深度測試失敗,dpfail表示模板測試通過但是深度測試失敗,dppass表示深度測試成功。GLenum部分填寫的是對應條件下執行的動作,例如GL_KEEP表示保留緩衝區中值,GL_REPLACE表示使用glStencilFunc設定的ref值替換。更完整的引數列表可以參考glStencilOp

繪製矩形模板

首先實現上面給出的矩形模板,通過矩形模板我們將場景中不被矩形覆蓋的部分丟棄,最終只顯示一個矩形遮蓋的區域。
繪製流程用程式碼簡要地表示如下:

// 清除顏色緩衝區 深度緩衝區 模板緩衝區
glClearColor(0.18f, 0.04f, 0.14f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// section1 繪製模板
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
// 在模板測試和深度測試都通過時更新模板緩衝區
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 

stencilShader.use();
glBindVertexArray(stencilVAOId);
glDrawArrays(GL_TRIANGLES, 0, 6);
// section 2繪製實際場景
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glStencilMask(0x00); // 禁止寫入stencil
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
shader.use();
// 繪製第一個立方體
// 繪製第二個立方體
// 繪製平面

需要注意的是,繪製模板時使用程式碼:

 glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDisable(GL_DEPTH_TEST);

禁止寫入顏色和深度緩衝區,因為矩形模板最終是不顯示在螢幕上的。後面我們可以看到,有些時候模板也需要顯示,注意在合適的時候調整這部分程式碼。

最終的效果如下圖所示:
舉行模板

outline 輪廓效果

outline就是輪廓的效果,在遊戲場景中,例如玩家選取了附件的物體時,通過輪廓線條來表示選取了哪些物體,十分有用。實現的思路是:

  • 現正常比例繪製物體,同時繪製的部分作為模板

  • 適當放大比例,在剛繪製的物體上繪製一個輪廓,使用模板緩衝實現。

實現的輪廓效果如下:

輪廓效果

這裡第一次繪製了原始的立方體,通過原始立方體填充了模板緩衝區為1,第二次繪製放大一點的立方體,對比緩衝區中值等於1則丟棄,因此只顯示處理輪廓部分。緩衝區比較函式實現為:

 // 使用模板緩衝區 繪製立方體邊緣
outlineShader.use();
glStencilMask(0x00); // 禁止寫入模板緩衝區
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glDisable(GL_DEPTH_TEST); // 這裡關閉深度測試 是為了讓輪廓不因為處在前面的平面而被消去
const GLfloat scale = 1.1f;
glBindVertexArray(cubeVAOId);
// 繪製第一個立方體輪廓
// 繪製第二個立方體輪廓

需要注意的是繪製輪廓線條時,需要暫時關閉深度測試,否則眼前的平面會把底部的輪廓線條遮擋掉,如果忘記了關閉深度測試,那麼錯誤的效果如下:

輪廓線錯誤

需要注意地是正確地開啟和允許模板緩衝區的讀寫,例如如果在第一次主迴圈結束之前忘記使用程式碼:

   glEnable(GL_STENCIL_TEST);
   glStencilMask(0xFF);

開啟和允許模板緩衝寫操作,那麼得到的錯誤效果如下:

錯誤的效果

planar reflections 鏡面效果

鏡面效果表示的是物體的原始和反射後的影象,形成的一種鏡面效果。實現思路為:

  • 繪製鏡面模板
  • 利用模板,使用映象變換(reflection transformation),繪製反射後物體
  • 使用混色(blend)繪製鏡面
  • 繪製原始物體

實現效果如下:

鏡面效果

實現繪製反射的物體,主要通過向縮放操作傳遞負數來實現映象變換,如果對映象變換不熟悉,可以回過頭去參考模型變換(model transformation) 。需要注意地是,在本節程式碼中鏡面位置在y=-0.5,那麼映象變換時,需要注意映象變換的中心問題。這裡關於y=-0.5變換,而不是y=0即x軸變換,因此首先將物體從變換中心(0.0,-0.5)移動到原點(0,0),縮放後將物體再次移動到(0.0,-0.5),這一過程表示為:

  // 繪製第一個立方體反射
glm::mat4 firstReflectModel;
firstReflectModel = glm::translate(firstReflectModel, 
glm::vec3(-1.0f, 0.0f, -1.0f)); // 移動到與原始物體對應位置
// T*S*T形式地縮放
firstReflectModel =glm::translate(firstReflectModel, 
glm::vec3(0.0f, -0.5f, 0.0f));
firstReflectModel = glm::scale(firstReflectModel, glm::vec3(1.0f, -1.0f, 1.0f));
firstReflectModel = glm::translate(firstReflectModel, glm::vec3(0.0f, 0.5f, 0.0f));
glUniformMatrix4fv(glGetUniformLocation(shader.programId, "model"),1, GL_FALSE, glm::value_ptr(firstReflectModel));
glDrawArrays(GL_TRIANGLES, 0, 36);

這裡模板緩衝的作用,主要是使映象看起來在鏡面上,如果不使用模板緩衝,效果如下:

沒有使用模板的映象

這裡紅色部分顯示在鏡面外面,帶有明顯的視覺bug,通過模板緩衝,我們決定只將映象在鏡面部分的內容顯示出來,形成的鏡面效果看起來才更真實。下面給出一個完整的不使用和使用模板的效果對比圖:

效果對比

在實現鏡面效果的時候,注意鏡面需要開啟blend效果,就是混色的效果,通過混色,我們在鏡面上顯示了少許鏡子的顏色(我們使用紅色模擬鏡面),看到了更多的是物體的映象的顏色。如果不開啟混色,那麼物體的映象將被鏡面遮住,看不到映象。使用混色(blend)繪製鏡面,可以通過如下程式碼實現:

// section3 繪製反射平面
planeShader.use();
glBindVertexArray(planeVAOId);
glEnable(GL_BLEND); // 為反射平面啟用混色
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisable(GL_BLEND);

關於blend的內容,下一節將會介紹,這裡不再展開。

最後的說明

模板測試使用的思路很簡單,先通過繪製物體寫入模板緩衝,這是一個建立的過程;第二步是利用模板緩衝中的值選擇性地丟棄或者保留片元,從而製造特效。不同的效果,調弄這些模板函式的方式各不相同。在使用過程中,尤其需要注意的是何時開始緩衝區,以及緩衝區寫入的模式設定問題。關於planar reflections,一個比較好的參考資料點選這裡

參考資料