筆者介紹:姜雪偉,IT公司技術合伙人,IT高級講師,CSDN社區專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D游戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。
CSDN課程視頻網址:http://edu.csdn.net/lecturer/144
上節給讀者介紹了深度測試,本節介紹一下模版測試,模版測試跟深度測試是不同的,GPU都會執行片段著色器處理,當片段著色器處理完片段之后,模板測試(Stencil Test) 就開始執行了,和深度測試一樣,它能丟棄一些片段。仍然保留下來的片段進入深度測試階段,深度測試可能丟棄更多。模板測試基于另一個緩沖,這個緩沖叫做模板緩沖(Stencil Buffer),它是在深度測試之前執行的。
模板緩沖中的模板值(Stencil Value)通常是8位的,因此每個片段/像素共有256種不同的模板值(譯注:8位就是1字節大小,因此和char的容量一樣是256個不同值)。為了能讓讀者更直觀的認識模版測試,下面通過圖的方式說明:

模板緩沖先清空模板緩沖設置所有片段的模板值為0,然后開啟矩形片段用1填充。場景中的模板值為1的那些片段才會被渲染(其他的都被丟棄)。模版緩沖要遵守下面的規則:
開啟模板緩沖寫入。渲染物體,更新模板緩沖。關閉模板緩沖寫入。渲染(其他)物體,這次基于模板緩沖內容丟棄特定片段。使用模板緩沖我們可以基于場景中已經繪制的片段,來決定是否丟棄特定的片段。
你可以開啟GL_STENCIL_TEST來開啟模板測試。接著所有渲染函數調用都會以這樣或那樣的方式影響到模板緩沖。
glEnable(GL_STENCIL_TEST);要注意的是,像顏色和深度緩沖一樣,在每次循環,你也得清空模板緩沖。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);同時,和深度測試的
glDepthMask函數一樣,模板緩沖也有一個相似函數。glStencilMask允許我們給模板值設置一個位遮罩(Bitmask),它與模板值進行按位與(AND)運算決定緩沖是否可寫。默認設置的位遮罩都是1,這樣就不會影響輸出,但是如果我們設置為0x00,所有寫入深度緩沖最后都是0。這和深度緩沖的glDepthMask(GL_FALSE)很類似:// 0xFF == 0b11111111//此時,模板值與它進行按位與運算結果是模板值,模板緩沖可寫glStencilMask(0xFF); // 0x00 == 0b00000000 == 0//此時,模板值與它進行按位與運算結果是0,模板緩沖不可寫glStencilMask(0x00); 大多數情況你的模板遮罩(stencil mask)寫為0x00或0xFF就行,但是最好知道有一個選項可以自定義位遮罩。和深度測試一樣,模版測試也有兩個函數,決定何時模板測試通過或失敗以及它怎樣影響模板緩沖。模板測試的兩個函數:
func:設置模板測試操作。這個測試操作應用到已經儲存的模板值和glStencilFunc和glStencilOp。void glStencilFunc(GLenum func, GLint ref, GLuint mask)函數有三個參數:glStencilFunc的ref值上,可用的選項是:GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS。它們的語義和深度緩沖的相似。ref:指定模板測試的引用值。模板緩沖的內容會與這個值對比。mask:指定一個遮罩,在模板測試對比引用值和儲存的模板值前,對它們進行按位與(and)操作,初始設置為1。在上面簡單模板的例子里,方程應該設置為:
glStencilFunc(GL_EQUAL, 1, 0xFF)它會告訴OpenGL,無論何時,一個片段模板值等于(
GL_EQUAL)引用值1,片段就能通過測試被繪制了,否則就會被丟棄。但是
glStencilFunc只描述了OpenGL對模板緩沖做什么,而不是描述我們如何更新緩沖。這就需要glStencilOp登場了。sfail: 如果模板測試失敗將采取的動作。dpfail: 如果模板測試通過,但是深度測試失敗時采取的動作。dppass: 如果深度測試和模板測試都通過,將采取的動作。
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)函數包含三個選項,我們可以指定每個選項的動作:每個選項都可以使用下列任何一個動作。
操作 描述GL_KEEP 保持現有的模板值GL_ZERO 將模板值置為0GL_REPLACE 將模板值設置為用glStencilFunc函數設置的ref值GL_INCR 如果模板值不是最大值就將模板值+1GL_INCR_WRAP 與GL_INCR一樣將模板值+1,如果模板值已經是最大值則設為0GL_DECR 如果模板值不是最小值就將模板值-1GL_DECR_WRAP 與GL_DECR一樣將模板值-1,如果模板值已經是最小值則設為最大值
glStencilOp函數默認設置為 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何測試的任何結果,模板緩沖都會保留它的值。默認行為不會更新模板緩沖,所以如果你想寫入模板緩沖的話,你必須像任意選項指定至少一個不同的動作。使用
學習了模版以后,如何在開發中使用,這個是我們最關心的,下面展示一個用模板測試實現的一個特別的和有用的功能,叫做物體輪廓(Object Outlining)。效果如下圖所示:glStencilFunc和glStencilOp,我們就可以指定在什么時候以及我們打算怎么樣去更新模板緩沖了,我們也可以指定何時讓測試通過或不通過。什么時候片段會被拋棄。
物體輪廓就像它的名字所描述的那樣,它能夠給每個(或一個)物體創建一個有顏色的邊。在策略游戲中當你打算選擇一個單位的時候它特別有用。給物體加上輪廓的步驟如下:
在繪制物體前,把模板方程設置為GL_ALWAYS,用1更新物體將被渲染的片段。渲染物體,寫入模板緩沖。關閉模板寫入和深度測試。每個物體放大一點點。使用一個不同的片段著色器用來輸出一個純顏色。再次繪制物體,但只是當它們的片段的模板值不為1時才進行。開啟模板寫入和深度測試。這個過程將每個物體的片段模板緩沖設置為1,當我們繪制邊框的時候,我們基本上繪制的是放大版本的物體的通過測試的地方,放大的版本繪制后物體就會有一個邊。我們基本會使用模板緩沖丟棄所有的不是原來物體的片段的放大的版本內容。
我們先來創建一個非常基本的片段著色器,它輸出一個邊框顏色。我們簡單地設置一個固定的顏色值,把這個著色器命名為shaderSingleColor:
void main(){ outColor = vec4(0.04, 0.28, 0.26, 1.0);}我們只打算給兩個箱子加上邊框,所以我們不會對地面做什么。這樣我們要先繪制地面,然后再繪制兩個箱子(同時寫入模板緩沖),接著我們繪制放大的箱子(同時丟棄前面已經繪制的箱子的那部分片段)。
我們先開啟模板測試,設置模板、深度測試通過或失敗時才采取動作:
glEnable(GL_DEPTH_TEST);glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);如果任何測試失敗我們都什么也不做,我們簡單地保持深度緩沖中當前所儲存著的值。如果模板測試和深度測試都成功了,我們就將儲存著的模板值替換為
1,我們要用glStencilFunc來做這件事。我們清空模板緩沖為0,為箱子的所有繪制的片段的模板緩沖更新為1,實現代碼片段如下所示:
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要寫入模板緩沖glStencilMask(0xFF); // 設置模板緩沖為可寫狀態normalShader.Use();DrawTwoContainers();使用
GL_ALWAYS模板測試函數,我們確保箱子的每個片段用模板值1更新模板緩沖。因為片段總會通過模板測試,在我們繪制它們的地方,模板緩沖用引用值更新。現在箱子繪制之處,模板緩沖更新為1了,我們將要繪制放大的箱子,但是這次關閉模板緩沖的寫入:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);glStencilMask(0x00); // 禁止修改模板緩沖glDisable(GL_DEPTH_TEST);shaderSingleColor.Use();DrawTwoScaledUpContainers();我們把模板方程設置為
GL_NOTEQUAL,它保證我們只箱子上不等于1的部分,這樣只繪制前面繪制的箱子外圍的那部分。注意,我們也要關閉深度測試,這樣放大的的箱子也就是邊框才不會被地面覆蓋。做完之后還要保證再次開啟深度緩沖。
場景中的物體邊框的繪制方法最后看起來像這樣:
glEnable(GL_DEPTH_TEST);glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);glStencilMask(0x00); // 繪制地板時確保關閉模板緩沖的寫入normalShader.Use();DrawFloor() glStencilFunc(GL_ALWAYS, 1, 0xFF);glStencilMask(0xFF);DrawTwoContainers();glStencilFunc(GL_NOTEQUAL, 1, 0xFF);glStencilMask(0x00);glDisable(GL_DEPTH_TEST);shaderSingleColor.Use();DrawTwoScaledUpContainers();glStencilMask(0xFF);glEnable(GL_DEPTH_TEST);實現的效果圖如下所示:最后把Shader代碼的文件stencil_single_color.frag給讀者展示一下:
#version 330 coreout vec4 outColor;void main(){ outColor = vec4(0.04, 0.28, 0.26, 1.0);}stencil_testing.vs代碼如下所示:#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec2 texCoords;out vec2 TexCoords;uniform mat4 model;uniform mat4 view;uniform mat4 PRojection;void main(){ gl_Position = projection * view * model * vec4(position, 1.0f); TexCoords = texCoords;}stencil_testing.frag代碼如下所示:#version 330 corein vec2 TexCoords;out vec4 color;uniform sampler2D texture1;void main(){ color = texture(texture1, TexCoords);}接下來就是通過OpenGL的C++代碼進行處理Shader了,下面把核心代碼拿出來展示:// 定義視口尺寸 glViewport(0, 0, screenWidth, screenHeight); // 深度測試 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_STENCIL_TEST); glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // 加載Shader Shader shader("stencil_testing.vs", "stencil_testing.frag"); Shader shaderSingleColor("stencil_testing.vs", "stencil_single_color.frag");下面是對場景中的物體具體處理:// Clear the colorbuffer glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // Set uniforms shaderSingleColor.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shaderSingleColor.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shaderSingleColor.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); shader.Use(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Draw floor as normal, we only care about the containers. The floor should NOT fill the stencil buffer so we set its mask to 0x00 glStencilMask(0x00); // Floor glBindVertexArray(planeVAO); glBindTexture(GL_TEXTURE_2D, floorTexture); model = glm::mat4(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); // == ============= // 1st. Render pass, draw objects as normal, filling the stencil buffer glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilMask(0xFF); // Cubes glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_2D, cubeTexture); model = glm::translate(model, glm::vec3(-1.0f, 0.0f, -1.0f)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); model = glm::mat4(); model = glm::translate(model, glm::vec3(2.0f, 0.0f, 0.0f)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // == ============= // 2nd. Render pass, now draw slightly scaled versions of the objects, this time disabling stencil writing. // Because stencil buffer is now filled with several 1s. The parts of the buffer that are 1 are now not drawn, thus only drawing // the objects' size differences, making it look like borders. glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilMask(0x00); glDisable(GL_DEPTH_TEST); shaderSingleColor.Use(); GLfloat scale = 1.1;這樣模版測試的核心技術給讀者就介紹完了,希望對大家有所幫助。。。。。。
新聞熱點
疑難解答