本文主要解决两个问题:
1、模板测试的原理是什么?
2、如何利用模板缓存显示盒子的边框?
引言
前面的章节中我们介绍了深度测试,深度测试为我们呈现了物体前后遮挡的效果。不过,光有深度测试还不够,我们还需要别的东西来实现一些特殊的效果,例如水面上映照出天空,战争迷雾效果,汽车后视镜效果。于是,我们引入一个新的测试:模板测试。这个测试可以帮助我们实现上述的这些效果。
模板测试
和深度测试一样,模板测试也会决定片元是可以被渲染还是被丢弃。从渲染流程来说,模板测试在深度测试之前,通过模板测试之后才能进入深度测试。模板测试是基于另一个名叫模板缓存的东西来实现的,通过更新模板缓存,我们可以实现一些非常有趣的效果。
本质上,模板缓存不像深度缓存那样有实际意义(深度缓存表示物体的前后关系),它只是我们创造出来用于实现一些特殊效果的缓存。所以,你可能用不到模板缓存,但你肯定要用到深度缓存。模板缓存中的模板值是一个8位的整型数,所以,每个模板值就有256种不同的状态。通常我们不会用到那么多,用的最频繁的就是0和0xFF两种状态。
每个GUI库都需要设置一个模板缓存。GLFW已经自动创建好了,不过不是每个库都会自己创建,使用的时候记得查看用户手册确保模板缓存已经创建。
一个简单的模板缓存的示例:
模板缓存先被全部清0,然后在需要绘制的地方设置成1,然后场景中只有模板值为1的片元绘制出来了,其余全被丢弃。
模板缓存操作允许我们设置将模板缓存设置成特定的值。在渲染的时候,我们先写入模板值,然后在当前帧中读取这些值来测试片元是否丢弃。你可以尽情的开发模板缓存的使用方法,不过基本的流程是这样的:
- 启用写入模板缓存功能
- 渲染物体,更新模板缓存
- 禁止写入模板缓存
- 渲染物体,根据之前模板缓存中的内容进行渲染或丢弃
由此可见,使用模板缓存,我们可以根据已经渲染显示的物体去决定其他的物体时候显示。
启用模板测试的方法与启用深度测试的方法类似,只需要调用glEnable就行了,而且也需要在清屏的时候把模板缓存也清空了:
glEnable(GL_STENCIL_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
模板缓存提供一个glStencilMask函数来调整可以写入到缓存中的模板值。
glStencilMask(0xFF);
glStencilMask(0x00);
所有的模板值都会和glStencilMask中的参数进行与操作,这样就能起到对模板值的过滤作用。默认的参数为0xFF,一般也只需要用到0xFF和0x00就可以了。
模板函数
和深度测试一样,模板测试也有一些函数来控制如何进行测试以及测试之后进行什么操作。控制模板测试的函数是:glStencilFunc和glStencilOp。前面的函数用来控制如何进行测试,后面的函数用来控制测试之后进行什么操作。
glStencilFunc的函数原型是:glStencilFunc(GLenum func, GLint ref, GLuint mask)
,分别介绍一下参数的含义:
- func:设置模板测试函数。允许的参数有:GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER,GL_GEQUAL,GL_EQUAL,GL_NOTEQUAL和GL_ALWAYS。这些参数的含义和深度测试函数中参数含义一致。
- ref:设置模板测试的引用值。模板缓存中的内容会和这个值比较。
- mask:指定一个掩码值,这个值会引用值进行与运算,也会和模板缓存中的模板值进行与运算,这些运算操作是在模板测试之前完成的。默认会把掩码的所有位都置1.
在本章中我们会设置ref的值为1,mask的值为0xFF,像这样:
glStencilFunc(GL_EQUAL, 1, 0xFF);
如此调用的意思是,如果模板缓冲中的模板值等于1,则通过模板测试,否则就不通过。
glStencilFunc仅仅说明了OpenGL如何进行测试,并没有说测试完成之后该如何做。这个时候另一个函数就来了:glStencilOp。
glStencilOp(GLenum sFail, GLenum dpfail, GLenum dppass)
包含了三个参数,这些参数的含义是:
- sfail: 模板测试失败后进行的操作
- dpfail: 模板测试成功,深度测试失败后进行的操作
- dppass:模板测试和深度测试都成功后进行的操作
操作 | 描述 |
---|---|
GL_KEEP | 保留当前保存的模板值 |
GL_ZERO | 模板值置0 |
GL_REPLACE | 模板值设置成glStencilFunc函数中设置的引用值 |
GL_INCR | 如果模板值小于最大值,将模板值加1 |
GL_INCR_WRAP | 类似GL_INCR,当模板值超过最大值的时候设置成0 |
GL_DECR | 如果模板值大于最小值,将模板值减1 |
GL_DECR_WRAP | 类似GL_DECR,当模板值低于最小值时设置成最大值 |
GL_INVERT | 对当前的模板值按位取反 |
每一个参数都可以设定以下这些值:
操作 | 描述 |
---|---|
GL_KEEP | 保留当前保存的模板值 |
GL_ZERO | 模板值置0 |
GL_REPLACE | 模板值设置成glStencilFunc函数中设置的引用值 |
GL_INCR | 如果模板值小于最大值,将模板值加1 |
GL_INCR_WRAP | 类似GL_INCR,当模板值超过最大值的时候设置成0 |
GL_DECR | 如果模板值大于最小值,将模板值减1 |
GL_DECR_WRAP | 类似GL_DECR,当模板值低于最小值时设置成最大值 |
GL_INVERT | 对当前的模板值按位取反 |
glStencilOp的默认设置值为(GL_KEEP, GL_KEEP, GL_KEEP),所以不管测试结果如何,模板缓存都会保存当前的值。所以,如果你想要改变模板缓存中的值,你就需要调用glStencilOp来改变其默认值。
有了glStencilFunc和glStencilOp函数的帮助,我们就可以利用模板缓存来实现一些效果了。
物体边框
光是解释原理和函数的使用方式不足以真正了解其使用方式,所以接下来的部分,我们会用模板缓存来实现物体边框的效果,就像下面这张图一样:
根据上面学到的内容,我们大致可以理出一个如何实现的思路:
- 将模板测试方法设置成GL_ALWAYS,将物体片元位置的模板缓存设置为1
- 渲染物体
- 禁用模板写入和深度测试
- 将所有的物体都放大一点
- 使用一个输出边框颜色的片元着色器
- 绘制物体,但是只绘制模板值不为1的地方
- 启用模板写入和深度测试
在这个流程中,首先设置物体的每个片元位置的模板值为1,当我们想绘制边框的时候,只需要绘制边框的时候,只需要将物体都放大一点,然后在通过模板测试的位置绘制就可以,这样就只会绘制一个物体的框框了。
所以,我们先来创建绘制放大物体的片元着色器。这一步非常简单,只需输出一个固定颜色就行了。这个着色器我们命名为shaderSingleColor:
void main() {
FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}
我们只想绘制物体的边框,所以地板的绘制还是老样子。我们要先绘制地板,然后绘制两个箱子(写入模板缓存),最后绘制大一点的箱子(根据之前的模板缓存值来丢弃片元)。
先启用模板测试,然后设置测试后的操作:
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
如果模板测试和深度测试有不成功的,我们保留原有的值,如果测试都成功了,我们就用设置的引用值替换,我们会把引用值设置为1.在清空模板缓存值之后,我们将其设置为1:
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
normalShader.use();
//绘制两个正常的盒子
...
使用GL_ALWAYS参数确保盒子的片元位置会设置成1.因为片元总会通过模板测试,而我们之前设置通过模板测试和深度测试后会把模板值替换成引用值。接下来,我们采用另一种测试的方式来绘制大一点的盒子,并且我们不希望模板值写入到缓存中:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);;
shaderSingleColor.use();
//绘制两个稍大的盒子
...
代码非常简单,如果之前对模板缓存理解透彻的话,完全可以理解代码的意义。有一点你可能会觉得奇怪,就是为什么要禁用深度测试。因为我们的盒子是放在地板上的,有一部分的框框会延伸到地板下面,如果不禁用,你就只能到地板下面去看到框框了。像这样:
好,整个过程就像是这样子:
glEnable(GL_STRENCIL_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();
//绘制地板
...
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
//绘制两个正常盒子
...
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
//绘制两个稍大的盒子
...
//恢复模板测试和深度测试
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
按照这个思路补充完代码之后,编译运行,你会看到类似下面的效果:
如果实现有困难,请参考这里的源码。
怎么样,效果很赞吧?
这种边框的效果在很多游戏中都可以使用,比如,一些策略游戏需要选中某些东西就可以用这种方法。当然这种直接用一个颜色作为边框的显示方式有点生硬,你可以做一些过滤让边框效果柔和点,比如说高斯模糊(Gaussian Blur)。
总结
本文中,我们深入的理解了模板测试的原理,学到了如何使用glEnable函数启用/禁用模板测试,如何使用glStencilFunc函数设置模板测试的方式,如何使用glStencilOp函数设置模板测试后要进行的操作。并且,通过使用这些知识,我们绘制了两个盒子边框作为实践。成效斐然~
参考资料
www.learnopengl.com(非常好的网站,建议学习)