2 - 模板测试

在片段着色器处理完一个片段后,模板测试(Stencil Test)就会开始运行,它也会丢弃部分片段,随后剩下的片段会进入深度测试。

模板测试

使用模板缓冲

和深度测试有对应的深度缓冲一样,模板测试也有它对应的模板缓冲。一个模板缓冲中,通常每个模板值是8位的,因此每个像素/片段一共能有\(2^8=256\)种模板值。

上图体现了模板缓冲是怎么被使用的,它首先被清除为0,然后用1填充了个空心矩形,之后的渲染结果就只是一个空心矩形。因为 只有模板值为1的片段值才会被渲染

使用模板缓冲的大体步骤如下:

  1. 启用模板缓冲的写入
  2. 渲染物体,更新模板缓冲的内容
  3. 禁用模板缓冲的写入
  4. 渲染(其他)物体,这次根据模板缓冲的内容丢弃特定的片段。

通过使用模板缓冲,我们可以根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。

可以用GL_STENCIL_TEST来启用模板测试,此后所有的渲染调用都会以某种方式影响着模板缓冲:

glEnable(GL_STENCIL_TEST);
// 别忘了清除模板缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);  

glDepthMask()同理,glStencilMask()允许我们设置一个自定义的位掩码,它与将要写入的模板缓冲进行AND运算。通常用到0xFF0x00:

glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

模板函数

glStencilFunc()glStencilOp()两个函数决定如何进行模板测试,以及如何更新模板缓冲。

glStencilFunc()

首先看看 glStencilFunc(GLenum func, GLint ref, GLuint mask),它会告诉OpenGL应该对模板缓冲内容做什么:

  • func:设置模板测试函数。可用的选项有:GL_NEVERGL_LESSGL_LEQUALGL_GREATERGL_GEQUALGL_EQUALGL_NOTEQUALGL_ALWAYS。它们的语义和深度缓冲的函数类似。
  • ref:设置模板测试的参考值。模板缓冲的内容将于这个值进行比较。
  • mask:设置一个模板掩码。

例如上图的那个简单例子,它的glStencilFunc()应该被设置为:

glStencilFunc(GL_EQUAL, 1, 0xFF);

glStencilOp()

然后是glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass),他会告诉OpenGL如何更新缓冲:

  • sfail:模板测试失败时采取的行为
  • dpfail:模板测试成功,但深度测试失败时采取的行为
  • dppass:两个测试成功时采取的行为

这些行为列表如下:

行为描述
GL_KEEP保持当前储存的模板值
GL_ZERO将模板值设置为0
GL_REPLACE将模板值设置为glStencilFunc函数设置的ref
GL_INCR如果模板值小于最大值则将模板值加1
GL_INCR_WRAP与GL_INCR一样,但如果模板值超过了最大值则归零
GL_DECR如果模板值大于最小值则将模板值减1
GL_DECR_WRAP与GL_DECR一样,但如果模板值小于0则将其设置为最大值
GL_INVERT按位翻转当前的模板缓冲值

默认情况下glStencilOp是设置为glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)的,不会更新模板缓冲,所以要想写入模板缓冲,需要至少对其中一个选项设置不同的值。

实践:物体轮廓

我们将会用模板测试完成RTS游戏中通常有的特性:物体轮廓(Object Outlining),效果如下:

首先先创建一个片段着色器shaderSingleColor.frag

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}

然后开始正式实现物体轮廓:

  1. 启用模板测试,并只更新通过两个测试的模板缓冲值:

    glEnable(GL_STENCIL_TEST);
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
  2. 启用模板缓冲写入,将模型所有片段的模板缓冲值更新为1,然后渲染原模型:

    glStencilFunc(GL_ALWAYS, 1, 0xFF);
    glStencilMask(0xFF);
    // 使用ObjShader渲染模型...
  3. 禁用模板缓冲写入,稍微放大模型,使用GL_NOTEQUAL进行模板测试,只渲染模型放大的那一部分(即边框):

    // 设置模板缓冲规则为NOTEQUAL
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilMask(0x00);
    // 使用singleColorShader渲染放大一点的模型...
    // 开启模板缓冲写入
    glStencilMask(0xFF);

对于正方体,最终结果就像上图那样。但是如果是稍微复杂点的模型,就会出现如下图的状况,轮廓往上移动了:

复杂模型的变换“基准点”在两脚之间,导致缩放时出现此现象。解决此问题的方法可以是给轮廓单独应用一个顶点着色器,将轮廓的每个顶点,都沿着模型的法线方向移动一点:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat3 normalMat;

out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords;

void main()
{
    Normal = normalMat * aNormal;
    gl_Position = projection * view * model * vec4(aPos, 1.0) + vec4(0.01*Normal, 0);
    FragPos = vec3(model * vec4(aPos, 1.0)) + 0.01*Normal;
    TexCoords = aTexCoords;
}

可见效果很好。

除此之外,模板测试还能在后视镜中绘制纹理,让它能够绘制到镜子形状中;或者能用一个叫做阴影体积(Shadow Volume)的模板缓冲技术来实时渲染阴影。

参考资料

  • 模板测试 - LearnOpenGL CN (learnopengl-cn.github.io)
  • OpenGL 模块测试实现物体轮廓时轮廓上移的解决方案 - 知乎 (zhihu.com)