OpenGL学习笔记22-Stencil testing

Stencil testing 模版测试

Advanced-OpenGL/Stencil-testing

一旦片段着色器处理了片段,一个所谓的模板测试被执行,就像深度测试一样,有放弃片段的选项。之后,剩余的片段被传递给深度测试,OpenGL可能会丢弃更多的片段。模板测试基于另一个名为模板缓冲区的缓冲区的内容,我们允许在呈现期间更新该缓冲区以实现有趣的效果。

模具缓冲区(通常)每个模具值包含8位,相当于每个像素共有256个不同的模具值。我们可以将这些模板值设置为我们喜欢的值,并且可以在特定片段具有特定的模板值时丢弃或保留片段。

每个窗口库都需要为您设置一个模板缓冲区。GLFW会自动做这个,所以我们不必告诉GLFW创建一个,但是其他窗口库可能不会在默认情况下创建模板缓冲区,所以一定要检查你的库的文档。

一个简单的模板缓冲区的例子如下所示(像素不按比例):

模板缓冲区首先用零清除,然后在模板缓冲区中存储一个开放的矩形1。场景的片段只在片段的模板值包含1的地方被渲染(其他的被丢弃)。

模板缓冲区操作允许我们在呈现片段的任何地方将模板缓冲区设置为特定值。通过在渲染时更改模板缓冲区的内容,我们正在向模板缓冲区写入内容。在同一个(或后面的)框架中,我们可以读取这些值来丢弃或传递某些片段。当使用模板缓冲区,你可以得到疯狂,但一般轮廓通常如下:

  • 允许写入模具缓冲区。
  • 呈现对象,更新模板缓冲区的内容。
  • 禁用对模具缓冲区的写入。
  • 渲染(其他)对象,这一次根据模板缓冲区的内容丢弃某些片段。

通过使用模板缓冲区,我们可以基于场景中其他绘制对象的片段来丢弃某些片段。

可以通过启用GL_STENCIL_TEST来启用模具测试。从那时起,所有呈现调用都将以某种方式影响模具缓冲区。


glEnable(GL_STENCIL_TEST);    

请注意,您还需要清除模具缓冲区每次迭代,就像颜色和深度缓冲区:r:


glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

另外,就像深度测试的glDepthMask函数一样,模板缓冲区也有一个等效的函数。函数glStencilMask允许我们设置一个位掩码,该位掩码与即将写入缓冲区的模板值进行磨合。默认情况下,它被设置为所有1的位掩码,不会影响输出,但是如果我们将其设置为0x00,写入缓冲区的所有模板值将以0结束。这相当于深度测试的glDepthMask(GL_FALSE):


glStencilMask(0xFF); // each bit is written to the stencil buffer as is
glStencilMask(0x00); // each bit ends up as 0 in the stencil buffer (disabling writes)

在大多数情况下,您将只使用0x00或0xFF作为模板掩码,但是最好知道有一些设置自定义位掩码的选项。

Stencil functions

与深度测试类似,我们可以在一定程度上控制钢网测试何时通过或失败,以及它如何影响钢网缓冲区。我们总共可以使用两个函数来配置模具测试:glStencilFunc和glStencilOp。

glStencilFunc(GLenum func, GLint ref, GLuint mask)有三个参数:

  • func:设置模板测试函数,确定片段是通过还是丢弃。这个测试函数应用于存储的模板值和glStencilFunc的ref值。可能的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。这些函数的语义类似于深度缓冲区的函数。
  • ref:指定模板测试的参考值。将模板缓冲区的内容与此值进行比较。
  • mask:指定一个掩码,该掩码在测试比较引用值和存储的模板值之前对它们进行沙漏处理。最初都设置为1。

所以在我们在开始展示的简单模板示例的情况下,函数将被设置为:


glStencilFunc(GL_EQUAL, 1, 0xFF)

这告诉OpenGL,每当片段的模板值等于引用值1时,片段就会通过测试并被绘制,否则就会被丢弃。

但是glStencilFunc只描述了OpenGL是否应该基于模板缓冲区的内容传递或丢弃片段,而不是我们如何实际更新缓冲区。这就是glStencilOp的作用所在。

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)包含三个选项,我们可以为每个选项指定要采取的动作:

  • sfail: 模板测试失败时要采取的动作。
  • dpfail:当模板测试通过,但深度测试失败时采取的动作。
  • dppass:当模板和深度测试都通过时所采取的动作。

然后,对于每个选项,您可以采取以下任何行动:

ActionDescription
GL_KEEPThe currently stored stencil value is kept.
GL_ZEROThe stencil value is set to 0.
GL_REPLACEThe stencil value is replaced with the reference value set with glStencilFunc.
GL_INCRThe stencil value is increased by 1 if it is lower than the maximum value.
GL_INCR_WRAPSame as GL_INCR, but wraps it back to 0 as soon as the maximum value is exceeded.
GL_DECRThe stencil value is decreased by 1 if it is higher than the minimum value.
GL_DECR_WRAPSame as GL_DECR, but wraps it to the maximum value if it ends up lower than 0.
GL_INVERTBitwise inverts the current stencil buffer value.

默认情况下,glStencilOp函数被设置为(GL_KEEP、GL_KEEP、GL_KEEP),因此无论任何测试的结果是什么,模板缓冲区都会保留它的值。默认行为不会更新模板缓冲区,因此如果希望写入模板缓冲区,则需要为任何选项指定至少一个不同的操作。

因此,使用glStencilFunc和glStencilOp,我们可以精确地指定何时以及如何更新模板缓冲区,以及何时根据其内容传递或丢弃片段。

Object outlining 对象描述

如果您仅从前面几节中就完全理解了模板测试的工作原理,则不太可能实现此功能,因此我们将演示一个可以仅通过模板测试实现的特别有用的特性,即对象轮廓。

对象概述完全按照它所说的去做。对于每个对象(或者只有一个),我们在(合并的)对象周围创建一个小的彩色边框。例如,当你想在战略游戏中选择单位,并需要向用户展示哪些单位被选中时,这是一个特别有用的效果。列出对象的程序如下:

  1. 使模板写作。
  2. 在绘制(要绘制的)对象之前,将模板操作设置为GL_ALWAYS,在呈现对象片段的地方将模板缓冲区更新为1。
  3. 呈现的对象。
  4. 禁用模具写入和深度测试。
  5. 按小比例缩放每个物体。
  6. 使用不同的片段着色器输出单一(边界)颜色。
  7. 再次绘制对象,但仅当其片段的模板值不等于1。
  8. 再次启用深度测试并将模板函数恢复到GL_KEEP。

此过程将模板缓冲区的每个对象片段的内容设置为1,当需要绘制边框时,我们仅在模板测试通过的地方绘制对象的放大版本。使用模板缓冲区,我们有效地丢弃了所有放大版本的片段,这些片段是原始对象片段的一部分。

所以我们首先要创建一个非常基本的片段着色器,它输出一个边界颜色。我们简单地设置一个硬编码的颜色值,并调用着色器shaderSingleColor:


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

使用前一章的场景,我们将向两个容器添加outline对象,因此我们将不使用floor。我们想首先绘制地板,然后是两个容器(在写入模具缓冲区时),然后绘制放大的容器(在丢弃写入之前绘制的容器片段的片段)。

我们首先需要启用模具测试:


glEnable(GL_STENCIL_TEST);

然后在每一帧中,我们要指定当模板测试成功或失败时要采取的动作:


glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

如果任何一个测试失败,我们什么都不做;我们只需保持模板缓冲区中当前存储的值。但是,如果模板测试和深度测试都成功,我们希望用通过glStencilFunc设置的引用值替换存储的模板值,我们稍后将其设置为1。

在帧开始时,我们将模板缓冲区清除为0,对于容器,我们将模板缓冲区更新为1,对于绘制的每个片段:


glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  
glStencilFunc(GL_ALWAYS, 1, 0xFF); // all fragments should pass the stencil test
glStencilMask(0xFF); // enable writing to the stencil buffer
normalShader.use();
DrawTwoContainers();

通过使用GL_REPLACE作为模板操作函数,我们确保每个容器的片段都用模板值1更新模板缓冲区。因为片段总是通过模板测试,所以模板缓冲区在绘制它们的任何地方都会使用引用值进行更新。

现在模板缓冲区更新为1,在这里绘制了容器,我们将绘制放大的容器,但这一次使用适当的测试函数和禁用写模板缓冲区:


glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // disable writing to the stencil buffer
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); // make sure we don't update the stencil buffer while drawing the floor
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);
glStencilFunc(GL_ALWAYS, 1, 0xFF);   
glEnable(GL_DEPTH_TEST);  

只要你理解模板测试背后的基本思想,这应该不会太难理解。否则,请再次仔细阅读前面的部分,并尝试完全理解每个函数的功能,现在您已经看到了一个可以使用它的示例。

概述算法的结果是这样的:

请查看这里here 的源代码,查看对象概述算法的完整代码。

你可以看到两个容器之间的边界重叠,这通常是我们想要的效果(想想我们想要选择10个单位的策略游戏;合并边框通常是首选)。如果你想要每个对象的完整边框,你必须清除每个对象的模板缓冲区,并在深度缓冲区上有一点创意。

您所看到的描述算法的对象通常用于游戏中可视化所选对象(想想策略游戏),这样的算法可以很容易地在模型类中实现。您可以在模型类中设置一个布尔标记来绘制有边框或无边框。如果你想要更有创意,你甚至可以在后期处理滤镜(如高斯模糊)的帮助下让边框看起来更自然。

模具测试还有更多的用途(除了勾线对象之外),比如在后视镜中绘制纹理,以便它能整齐地贴合镜面形状,或者使用名为阴影体积的模具缓冲技术绘制实时阴影。模板缓冲区在我们已经扩展的OpenGL工具包中为我们提供了另一个很好的工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值