通过本节可以了解到
- 什么是模板缓冲和模板测试?
- OpenGL中怎么使用模板测试?
- 模板测试可以实现的一些效果
1 Stencil testing 模板测试
首先我们了解什么是模板缓存/缓冲。模板缓存(stencil buffer)是一个用于专门用于制作特效的离屏(off-screen)缓存。
理论上:当片元着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,部分片元可能未通过模板测被丢弃。接下来,被保留的片段会进入深度测试。模板测试是根据模板缓冲来进行的,我们可以在渲染的时候更新它来获得一些很有意思的效果,比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。
模板缓冲为屏幕上的每一个像素进行保存一个8位的无符号整数,即范围 0~255 的整数。我们将某个片元的模板值与模板缓冲中的相比较,可以选择丢弃或保留该片元。过程示意如下:
模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
2 Stencil Buffer in OpenGL
了解了模板缓冲区的原理,下面我们看看在OpenGL中如何使用模板缓冲区进行测试。OpenGL中与模板缓冲区相关的函数有如下几个:
// 默认模板缓冲区并未开启,需要使用glEnable开启
glEnable(GL_STENCIL_TEST);
// 设置模板缓冲区的写入掩码:当为0x00时禁止在Stencil Buffer中写入(默认0xff)
glStencilMask(0xFF);
// 清除Stencil Buffer的值默认为0
glClearStencil(clearStencilValue);
// 默认值GL_ALWAYS, 0, 0xFF, 总是通过Stencil测试
glStencilFunc(func, ref, mask);
// 默认GL_KEEP, GL_KEEP, GL_KEEP, 不会改变Stencil Buffer
glStencilOp(fail, zfail, zpass);
// 清除StencilBuffer
glClear(GL_STENCIL_BUFFER_BIT);
模板测试只有存在模板缓冲区且模板缓冲区开启的情况下进行,模板测试把每个像素对应的模板缓冲区的值与一个参考值ref
进行比较glStencilFunc
,根据测试结果,对模板缓冲区的值进行相应的修改glStencilOp
。
// 通过的和不通过的都去glStencilOp进行下一步操作
void glStencilFunc (GLenum func, GLint ref, GLuint mask);
// func:
// GL_NEVER 从来不能通过
// GL_ALWAYS 永远可以通过(默认值)
// GL_LESS 小于参考值可以通过
// GL_LEQUAL 小于或者等于可以通过
// GL_EQUAL 等于通过
// GL_GEQUAL 大于等于通过
// GL_GREATER 大于通过
// GL_NOTEQUAL 不等于通过
// ref: 参考值
// mask: 掩码值(会与参考值和模板缓冲区内的值先作按位与操作,在参考func中的参数测试是否通过测试)
void glStencilOp (GLenum fail, GLenum zfail, GLenum zpass);
// fail模板测试未通过时该如何变化;zfail表示模板测试通过,但深度测试未通过时该如何变化;zpass表示模板测试和深度测试或者未执行深度测试均通过时该如何变化
// GL_KEEP(不改变,这也是默认值)
// GL_ZERO(回零)
// GL_REPLACE(使用测试条件中的设定值来代替当前模板值)
// GL_INCR(增加1,但如果已经是最大值,则保持不变),
// GL_INCR_WRAP(增加1,但如果已经是最大值,则从零重新开始)
// GL_DECR(减少1,但如果已经是零,则保持不变),
// GL_DECR_WRAP(减少1,但如果已经是零,则重新设置为最大值)
// GL_INVERT(按位取反)
也就是说对模板缓冲区有三种操作方式:分别是:
- 模板缓冲区测试失败了:那么执行
glStencilOp
中第一个参数fail
的操作方式; - 模板缓冲区测试通过了&深度测试未通过:那么执行
glStencilOp
中第二个参数zfail
的操作方式; - 模板缓冲区测试通过了&深度测试通过了:那么执行
glStencilOp
中第三个参数zpass
的操作方式;
注意三者的共同点都是对模板缓冲区进行操作,换句话说glStencilOp
才是真正对模板缓冲区进行操作的函数。
3 Possible Effect 可实现的效果
3.1 绘制矩形模板
// 清除颜色缓冲区 深度缓冲区 模板缓冲区
glClearColor(0.18f, 0.04f, 0.14f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// section1 绘制模板
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_FALSE);
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
// 在模板测试和深度测试都通过时更新模板缓冲区
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
stencilShader.use();
glBindVertexArray(stencilVAOId);
glDrawArrays(GL_TRIANGLES, 0, 6);
// section 2绘制实际场景
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
glStencilMask(0x00); // 禁止写入stencil
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
shader.use();
// 绘制第一个立方体
// 绘制第二个立方体
// 绘制平面
需要注意的是,绘制模板时使用:
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDisable(GL_DEPTH_TEST);
禁止写入颜色和深度缓冲区,因为矩形模板最终是不显示在屏幕上的。后面我们可以看到,有些时候模板也需要显示,注意在合适的时候调整这部分代码。
3.2 Object outlining 绘制物体轮廓
我们可以在每个物体的周围创建一个小的有色边框,比如在游戏中告诉玩家选中了哪个物体。实现的步骤如下:
- 在绘制物体之前,将模板函数设置为GL_ALWAYS,物体的片段被渲染时,将对应模板缓冲更新为1。
- 渲染原物体
- 禁用模板写入和深度测试
- 物体scale放大一点点
- 创建输出边框颜色的片元着色器
- 再次绘制物体,但仅在模板值不为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);
如果其中的一个测试失败了,我们什么都不做,我们仅仅保留当前储存在模板缓冲中的值。如果模板测试和深度测试都通过了,那么我们希望将储存的模板值设置为参考值,参考值能够通过glStencilFunc来设置,我们之后会设置为1。
我们将模板缓冲清除为0,对箱子中所有绘制的片段,将模板值更新为1:
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲
glStencilMask(0xFF); // 启用模板缓冲写入
normalShader.use();
DrawTwoContainers();
通过使用GL_ALWAYS模板测试函数,我们保证了箱子的每个片段都会将模板缓冲的模板值更新为1。因为片段永远会通过模板测试,在绘制片段的地方,模板缓冲会被更新为参考值。
现在模板缓冲在箱子被绘制的地方都更新为1了,我们将要绘制放大的箱子,但这次要禁用模板缓冲的写入:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止模板缓冲的写入
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
我们将模板函数设置为GL_NOTEQUAL,它会保证我们只绘制箱子上模板值不为1的部分,即只绘制箱子在之前绘制的箱子之外的部分。注意我们也禁用了深度测试,让放大的箱子,即边框,不会被地板所覆盖。
记得要在完成之后重新启用深度缓冲。
场景中物体轮廓的完整步骤会看起来像这样:
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); // 记得保证我们在绘制地板的时候不会更新模板缓冲
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);
轮廓算法的结果如下所示:
3.3 Planar reflections 镜面效果
未完!会用到Blend等效果
4 Keypoint
模板测试使用的思路很简单,先通过绘制物体写入模板缓冲,这是一个建立的过程;第二步是利用模板缓冲中的值选择性地丢弃或者保留片元,从而制造特效。不同的效果,调弄这些模板函数的方式各不相同。在使用过程中,尤其需要注意的是何时开始缓冲区,以及缓冲区写入的模式设置问题。
模板测试使用模板参考值、模板掩码、模板比较函数和当前像素在模板缓冲区中的模板值作为参数,判断某个像素是否将被写入到后台缓冲区中。模板测试的表达式是这样的:
\[result=(ref\And mask_{ref})OP(value\And mask_{write})\]
其中的ref表示模板参考值,mask表示模板掩码,value表示模板缓冲中的值,OP表示模板比较函数,而符号“&”则表示模板值或模板参考值与模板掩码进行按位的与计算。
5 Reference
- OpenGL模板缓冲区—StencilBuffer
- OpenGL学习脚印:模板测试(stencil testing)
- learnopengl-Stencil-testing
- 【Visual C++】游戏开发笔记四十六 浅墨DirectX教程十四 模板测试与镜面特效专场
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付

comments powered by Disqus