LearnOpenGL Note - Advanced OpenGL - Stencil testing

高级OpenGL-模板测试

Posted by Tao on Thursday, April 7, 2022

通过本节可以了解到

  • 什么是模板缓冲和模板测试?
  • OpenGL中怎么使用模板测试?
  • 模板测试可以实现的一些效果

1 Stencil testing 模板测试

首先我们了解什么是模板缓存/缓冲。模板缓存(stencil buffer)是一个用于专门用于制作特效的离屏(off-screen)缓存。

理论上:当片元着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,部分片元可能未通过模板测被丢弃。接下来,被保留的片段会进入深度测试。模板测试是根据模板缓冲来进行的,我们可以在渲染的时候更新它来获得一些很有意思的效果,比如图形的合成、镜面特效、消融、淡入淡出、轮廓的显示、侧影和实时阴影等等特效。

模板缓冲为屏幕上的每一个像素进行保存一个8位的无符号整数,即范围 0~255 的整数。我们将某个片元的模板值与模板缓冲中的相比较,可以选择丢弃或保留该片元。过程示意如下:

stencil_buffer.png

模板缓冲首先会被清除为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(按位取反)

也就是说对模板缓冲区有三种操作方式:分别是:

  1. 模板缓冲区测试失败了:那么执行glStencilOp中第一个参数fail的操作方式;
  2. 模板缓冲区测试通过了&深度测试未通过:那么执行glStencilOp中第二个参数zfail的操作方式;
  3. 模板缓冲区测试通过了&深度测试通过了:那么执行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 绘制物体轮廓

我们可以在每个物体的周围创建一个小的有色边框,比如在游戏中告诉玩家选中了哪个物体。实现的步骤如下:

  1. 在绘制物体之前,将模板函数设置为GL_ALWAYS,物体的片段被渲染时,将对应模板缓冲更新为1。
  2. 渲染原物体
  3. 禁用模板写入和深度测试
  4. 物体scale放大一点点
  5. 创建输出边框颜色的片元着色器
  6. 再次绘制物体,但仅在模板值不为1时绘制
  7. 再次启用模板写入和深度测试

所以我们首先来创建一个很简单的片段着色器,它会输出一个边框颜色。我们简单地给它设置一个硬编码的颜色值,将这个着色器命名为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);  

轮廓算法的结果如下所示: outline_result.png

3.3 Planar reflections 镜面效果

未完!会用到Blend等效果

4 Keypoint

模板测试使用的思路很简单,先通过绘制物体写入模板缓冲,这是一个建立的过程;第二步是利用模板缓冲中的值选择性地丢弃或者保留片元,从而制造特效。不同的效果,调弄这些模板函数的方式各不相同。在使用过程中,尤其需要注意的是何时开始缓冲区,以及缓冲区写入的模式设置问题。

模板测试使用模板参考值、模板掩码、模板比较函数和当前像素在模板缓冲区中的模板值作为参数,判断某个像素是否将被写入到后台缓冲区中。模板测试的表达式是这样的:

\[result=(ref\And mask_{ref})OP(value\And mask_{write})\]

其中的ref表示模板参考值,mask表示模板掩码,value表示模板缓冲中的值,OP表示模板比较函数,而符号“&”则表示模板值或模板参考值与模板掩码进行按位的与计算。

5 Reference

「如果这篇文章对你有用,请随意打赏」

Heisenberg Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付


comments powered by Disqus