1. 流程
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。
一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。
利用模板测试,我们可以限制渲染区域、渲染阴影,渲染物体粗轮廓等,下面是一个限制区域的例子:
1.1 模板测试
同样的,我们可以启用GL_STENCIL_TEST来启用模板测试:
glEnable(GL_STENCIL_TEST);
而且我们也需要在每次迭代之前清除模板缓冲(为0)。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
和深度测试的glDepthMask函数一样,模板缓冲也有一个类似的函数。glStencilMask允许我们设置一个位掩码(Bitmask),它会与将要写入缓冲的模板值进行与(AND)运算。默认情况下设置的位掩码所有位都为1,不影响输出,但如果我们将它设置为0x00,写入缓冲的所有模板值最后都会变成0.这与深度测试中的glDepthMask(GL_FALSE)是等价的。
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
1.2 模板函数
和深度测试的glDepthfunc函数一样,模板缓冲也有两个类似的函数。
glStencilFunc(GLenum func, GLint ref, GLuint mask)一共包含三个参数:
func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。
ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。
mask:设置一个掩码,它将会与参考值和储存的模板值都进行进行与(AND)运算,两个的结果再进行比较。初始情况下所有位都为1。
但是glStencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,而不是我们应该如何更新缓冲。这就需要glStencilOp这个函数了。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项,我们能够设定每个选项应该采取的行为:
sfail:模板测试失败时采取的行为。
dpfail:模板测试通过,但深度测试失败时采取的行为。
dppass:模板测试和深度测试都通过时采取的行为。
每个选项都可以选用以下的其中一种行为:
1.3 物体轮廓
接下来利用模板测试做一个实用的功能,那就是描绘出物体的轮廓,当你想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。
详细步骤如下:
1.首先清除模板缓存
glEnable(GL_DEPTH_TEST);
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
2.在没有开启模板测试前先将其他物体(比如灯)先渲染出来,保证不受影响
// 渲染指令
float timeValue = glfwGetTime();
deltaTime = timeValue - lastFrame;
lastFrame = timeValue;
view = projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
view = camera.GetViewMatrix();
light.view = view;
light.projection = projection;
light.Draw(lightShader);
3.开启模板测试,在同时通过模板和深度测试以后就把箱子渲染出来,并且把通过测试的区域的模板缓存设为参考值1
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
box.view = view;
box.projection = projection;
box.Draw(ourShader, camera, timeValue);
4.改变模板测试的规则,使它只有在缓存值不为1的时候通过测试,而且通过以后不更新缓存(这里的例子后面不渲染物体了,所以更不更新都无所谓),然后用单色着色器画出放大1.05倍的箱子(那0.05倍的突出处缓存为0,所以单色着色器只会渲染这部分,它就成了边框)
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
box.Draw(singleShader, camera, timeValue, 1.05f);
5.最后关闭模板测试,进行下一轮渲染
glDisable(GL_STENCIL_TEST);
glfwSwapBuffers(window);
glfwPollEvents();
}
效果:
那么进一步的,我们可不可以让这个边框即使被遮挡住了也能被显示出来呢?当然可以:
我们稍作修改,让箱子即使没通过深度测试也能更新模板,这样,被灯遮住的箱子部分的模板缓存也被更新为1了,然后我们在用单色着色器渲染边框的时候关闭深度测试,这样我们就能即透过灯看到边框,也不用担心被灯遮住的箱子部分全部被渲染为红色了
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
box.view = view;
box.projection = projection;
box.Draw(ourShader, camera, timeValue);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
//glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
box.Draw(singleShader, camera, timeValue, 1.05f);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
效果: