2 - 模板测试
在片段着色器处理完一个片段后,模板测试(Stencil Test)就会开始运行,它也会丢弃部分片段,随后剩下的片段会进入深度测试。
模板测试
使用模板缓冲
和深度测试有对应的深度缓冲一样,模板测试也有它对应的模板缓冲。一个模板缓冲中,通常每个模板值是8位的,因此每个像素/片段一共能有\(2^8=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()
允许我们设置一个自定义的位掩码,它与将要写入的模板缓冲进行AND运算。通常用到0xFF
和0x00
:
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)
模板函数
glStencilFunc()
和glStencilOp()
两个函数决定如何进行模板测试,以及如何更新模板缓冲。
glStencilFunc()
首先看看 glStencilFunc(GLenum func, GLint ref, GLuint mask)
,它会告诉OpenGL应该对模板缓冲内容做什么:
func
:设置模板测试函数。可用的选项有:GL_NEVER
、GL_LESS
、GL_LEQUAL
、GL_GREATER
、GL_GEQUAL
、GL_EQUAL
、GL_NOTEQUAL
和GL_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);
}
然后开始正式实现物体轮廓:
启用模板测试,并只更新通过两个测试的模板缓冲值:
glEnable(GL_STENCIL_TEST); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
启用模板缓冲写入,将模型所有片段的模板缓冲值更新为1,然后渲染原模型:
glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilMask(0xFF); // 使用ObjShader渲染模型...
禁用模板缓冲写入,稍微放大模型,使用
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)