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

模板緩沖先清空模板緩沖設(shè)置所有片段的模板值為0,然后開啟矩形片段用1填充。場(chǎng)景中的模板值為1的那些片段才會(huì)被渲染(其他的都被丟棄)。模版緩沖要遵守下面的規(guī)則:
開啟模板緩沖寫入。渲染物體,更新模板緩沖。關(guān)閉模板緩沖寫入。渲染(其他)物體,這次基于模板緩沖內(nèi)容丟棄特定片段。使用模板緩沖我們可以基于場(chǎng)景中已經(jīng)繪制的片段,來(lái)決定是否丟棄特定的片段。
你可以開啟GL_STENCIL_TEST來(lái)開啟模板測(cè)試。接著所有渲染函數(shù)調(diào)用都會(huì)以這樣或那樣的方式影響到模板緩沖。
glEnable(GL_STENCIL_TEST);要注意的是,像顏色和深度緩沖一樣,在每次循環(huán),你也得清空模板緩沖。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);同時(shí),和深度測(cè)試的
glDepthMask函數(shù)一樣,模板緩沖也有一個(gè)相似函數(shù)。glStencilMask允許我們給模板值設(shè)置一個(gè)位遮罩(Bitmask),它與模板值進(jìn)行按位與(AND)運(yùn)算決定緩沖是否可寫。默認(rèn)設(shè)置的位遮罩都是1,這樣就不會(huì)影響輸出,但是如果我們?cè)O(shè)置為0x00,所有寫入深度緩沖最后都是0。這和深度緩沖的glDepthMask(GL_FALSE)很類似:// 0xFF == 0b11111111//此時(shí),模板值與它進(jìn)行按位與運(yùn)算結(jié)果是模板值,模板緩沖可寫glStencilMask(0xFF); // 0x00 == 0b00000000 == 0//此時(shí),模板值與它進(jìn)行按位與運(yùn)算結(jié)果是0,模板緩沖不可寫glStencilMask(0x00); 大多數(shù)情況你的模板遮罩(stencil mask)寫為0x00或0xFF就行,但是最好知道有一個(gè)選項(xiàng)可以自定義位遮罩。和深度測(cè)試一樣,模版測(cè)試也有兩個(gè)函數(shù),決定何時(shí)模板測(cè)試通過或失敗以及它怎樣影響模板緩沖。模板測(cè)試的兩個(gè)函數(shù):
func:設(shè)置模板測(cè)試操作。這個(gè)測(cè)試操作應(yīng)用到已經(jīng)儲(chǔ)存的模板值和glStencilFunc和glStencilOp。void glStencilFunc(GLenum func, GLint ref, GLuint mask)函數(shù)有三個(gè)參數(shù):glStencilFunc的ref值上,可用的選項(xiàng)是:GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS。它們的語(yǔ)義和深度緩沖的相似。ref:指定模板測(cè)試的引用值。模板緩沖的內(nèi)容會(huì)與這個(gè)值對(duì)比。mask:指定一個(gè)遮罩,在模板測(cè)試對(duì)比引用值和儲(chǔ)存的模板值前,對(duì)它們進(jìn)行按位與(and)操作,初始設(shè)置為1。在上面簡(jiǎn)單模板的例子里,方程應(yīng)該設(shè)置為:
glStencilFunc(GL_EQUAL, 1, 0xFF)它會(huì)告訴OpenGL,無(wú)論何時(shí),一個(gè)片段模板值等于(
GL_EQUAL)引用值1,片段就能通過測(cè)試被繪制了,否則就會(huì)被丟棄。但是
glStencilFunc只描述了OpenGL對(duì)模板緩沖做什么,而不是描述我們?nèi)绾胃戮彌_。這就需要glStencilOp登場(chǎng)了。sfail: 如果模板測(cè)試失敗將采取的動(dòng)作。dpfail: 如果模板測(cè)試通過,但是深度測(cè)試失敗時(shí)采取的動(dòng)作。dppass: 如果深度測(cè)試和模板測(cè)試都通過,將采取的動(dòng)作。
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)函數(shù)包含三個(gè)選項(xiàng),我們可以指定每個(gè)選項(xiàng)的動(dòng)作:每個(gè)選項(xiàng)都可以使用下列任何一個(gè)動(dòng)作。
操作 描述GL_KEEP 保持現(xiàn)有的模板值GL_ZERO 將模板值置為0GL_REPLACE 將模板值設(shè)置為用glStencilFunc函數(shù)設(shè)置的ref值GL_INCR 如果模板值不是最大值就將模板值+1GL_INCR_WRAP 與GL_INCR一樣將模板值+1,如果模板值已經(jīng)是最大值則設(shè)為0GL_DECR 如果模板值不是最小值就將模板值-1GL_DECR_WRAP 與GL_DECR一樣將模板值-1,如果模板值已經(jīng)是最小值則設(shè)為最大值
glStencilOp函數(shù)默認(rèn)設(shè)置為 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何測(cè)試的任何結(jié)果,模板緩沖都會(huì)保留它的值。默認(rèn)行為不會(huì)更新模板緩沖,所以如果你想寫入模板緩沖的話,你必須像任意選項(xiàng)指定至少一個(gè)不同的動(dòng)作。使用
學(xué)習(xí)了模版以后,如何在開發(fā)中使用,這個(gè)是我們最關(guān)心的,下面展示一個(gè)用模板測(cè)試實(shí)現(xiàn)的一個(gè)特別的和有用的功能,叫做物體輪廓(Object Outlining)。效果如下圖所示:glStencilFunc和glStencilOp,我們就可以指定在什么時(shí)候以及我們打算怎么樣去更新模板緩沖了,我們也可以指定何時(shí)讓測(cè)試通過或不通過。什么時(shí)候片段會(huì)被拋棄。
物體輪廓就像它的名字所描述的那樣,它能夠給每個(gè)(或一個(gè))物體創(chuàng)建一個(gè)有顏色的邊。在策略游戲中當(dāng)你打算選擇一個(gè)單位的時(shí)候它特別有用。給物體加上輪廓的步驟如下:
在繪制物體前,把模板方程設(shè)置為GL_ALWAYS,用1更新物體將被渲染的片段。渲染物體,寫入模板緩沖。關(guān)閉模板寫入和深度測(cè)試。每個(gè)物體放大一點(diǎn)點(diǎn)。使用一個(gè)不同的片段著色器用來(lái)輸出一個(gè)純顏色。再次繪制物體,但只是當(dāng)它們的片段的模板值不為1時(shí)才進(jìn)行。開啟模板寫入和深度測(cè)試。這個(gè)過程將每個(gè)物體的片段模板緩沖設(shè)置為1,當(dāng)我們繪制邊框的時(shí)候,我們基本上繪制的是放大版本的物體的通過測(cè)試的地方,放大的版本繪制后物體就會(huì)有一個(gè)邊。我們基本會(huì)使用模板緩沖丟棄所有的不是原來(lái)物體的片段的放大的版本內(nèi)容。
我們先來(lái)創(chuàng)建一個(gè)非常基本的片段著色器,它輸出一個(gè)邊框顏色。我們簡(jiǎn)單地設(shè)置一個(gè)固定的顏色值,把這個(gè)著色器命名為shaderSingleColor:
void main(){ outColor = vec4(0.04, 0.28, 0.26, 1.0);}我們只打算給兩個(gè)箱子加上邊框,所以我們不會(huì)對(duì)地面做什么。這樣我們要先繪制地面,然后再繪制兩個(gè)箱子(同時(shí)寫入模板緩沖),接著我們繪制放大的箱子(同時(shí)丟棄前面已經(jīng)繪制的箱子的那部分片段)。
我們先開啟模板測(cè)試,設(shè)置模板、深度測(cè)試通過或失敗時(shí)才采取動(dòng)作:
glEnable(GL_DEPTH_TEST);glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);如果任何測(cè)試失敗我們都什么也不做,我們簡(jiǎn)單地保持深度緩沖中當(dāng)前所儲(chǔ)存著的值。如果模板測(cè)試和深度測(cè)試都成功了,我們就將儲(chǔ)存著的模板值替換為
1,我們要用glStencilFunc來(lái)做這件事。我們清空模板緩沖為0,為箱子的所有繪制的片段的模板緩沖更新為1,實(shí)現(xiàn)代碼片段如下所示:
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要寫入模板緩沖glStencilMask(0xFF); // 設(shè)置模板緩沖為可寫狀態(tài)normalShader.Use();DrawTwoContainers();使用
GL_ALWAYS模板測(cè)試函數(shù),我們確保箱子的每個(gè)片段用模板值1更新模板緩沖。因?yàn)槠慰倳?huì)通過模板測(cè)試,在我們繪制它們的地方,模板緩沖用引用值更新。現(xiàn)在箱子繪制之處,模板緩沖更新為1了,我們將要繪制放大的箱子,但是這次關(guān)閉模板緩沖的寫入:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);glStencilMask(0x00); // 禁止修改模板緩沖glDisable(GL_DEPTH_TEST);shaderSingleColor.Use();DrawTwoScaledUpContainers();我們把模板方程設(shè)置為
GL_NOTEQUAL,它保證我們只箱子上不等于1的部分,這樣只繪制前面繪制的箱子外圍的那部分。注意,我們也要關(guān)閉深度測(cè)試,這樣放大的的箱子也就是邊框才不會(huì)被地面覆蓋。做完之后還要保證再次開啟深度緩沖。
場(chǎng)景中的物體邊框的繪制方法最后看起來(lái)像這樣:
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); // 繪制地板時(shí)確保關(guān)閉模板緩沖的寫入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);實(shí)現(xiàn)的效果圖如下所示:最后把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);}接下來(lái)就是通過OpenGL的C++代碼進(jìn)行處理Shader了,下面把核心代碼拿出來(lái)展示:// 定義視口尺寸 glViewport(0, 0, screenWidth, screenHeight); // 深度測(cè)試 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");下面是對(duì)場(chǎng)景中的物體具體處理:// 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;這樣模版測(cè)試的核心技術(shù)給讀者就介紹完了,希望對(duì)大家有所幫助。。。。。。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注