OpenGL 模板测试(Stencil testing)

Reading time ~1 minute

模板缓存:

当片段着色器处理完片段之后,模板测试(stencil test) 就开始执行了,它能丢弃一些片段。模板测试基于一个缓冲,叫做模板缓冲(stencil buffer),我们被允许在渲染时更新它来获取有意思的效果。 模板缓冲的用途之一,就是将绘图范围限制在屏幕的特定区域,用来进行复杂的掩模(masking)操作。一个复杂的形状可以存储在模板缓存里,然后绘制子序列操作可以使用模板缓存里的内容来决定是否更新象素。

OpenGL在模板缓冲区中为每个像素保存了一个模板值,当像素需要进行模板测试时,将设定的模板参考值与该像素的模板值进行比较,符合条件的通过测试,不符合条件的则被丢弃。 模板缓冲中的模板值(stencil value)通常是8位的,因此每个片段(像素)共有256种不同的模板值。

注:只有在创建窗口过程中预先请求一个模板缓存区,才能够使用模板测试(如果没有模板缓存,模板测试总是通过的)。如果使用GLFW,它会自动做了这件事。

无论我们在渲染哪里的片段,模板缓冲操作都允许我们把模板缓冲设置为一个特定值。改变模板缓冲的内容实际上就是对模板缓冲进行写入。在同一次(或接下来的)渲染迭代我们可以读取这些值来决定丢弃还是保留这些片段。当使用模板缓冲的时候,你可以随心所欲,但是需要遵守下面的原则:

  • 开启模板缓冲写入。
  • 渲染物体,更新模板缓冲。
  • 关闭模板缓冲写入。
  • 渲染(其他)物体,这次基于模板缓冲内容丢弃特定片段。

使用模板缓冲我们可以基于场景中已经绘制的片段,来决定是否丢弃特定的片段。

启用模板测试:

使用GL_STENCIL_TEST来开启模板测试: glEnable(GL_STENCIL_TEST);

要注意的是,像颜色和深度缓冲一样,在每次循环,你也得清空模板缓冲: glClear(GL_STENCIL_BUFFER_BIT);

同时,glStencilMask允许我们给模板值设置一个位遮罩(bitmask),它与模板值进行按位与(and)决定缓冲是否可写。默认设置的位遮罩都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0,此时模板缓冲不可写,使用的是只读的模板缓存,这与深度缓冲的glDepthMask(GL_FALSE)很类似。

模板函数(stencil functions):

我们有几个不同控制权,决定何时模板测试通过或失败以及它怎样影响模板缓冲。一共有两种函数可供我们使用去配置模板测试:glStencilFunc 和 glStencilOp。

1
void glStencilFunc(GLenum func, GLint ref, GLuint mask)

函数有三个参数:

  • func:设置模板测试操作。这个操作设置模板缓存中的值和ref值如何比较,可用的选项是:GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS。
  • ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
  • mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。

比如:glStencilFunc(GL_EQUAL, 1, 0xFF)。它表示,一个片段模板值等于(GL_EQUAL)引用值1,片段就能通过测试被绘制了,否则就会被丢弃。

但是glStencilFunc只描述了OpenGL对模板缓冲做什么,而不能描述我们如何更新缓冲。这就需要glStencilOp登场了。

1
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)

函数包含三个选项:

  • sfail: 如果模板测试失败将采取的动作。
  • dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。
  • dppass: 如果深度测试和模板测试都通过,将采取的动作。

每个选项都可以使用下列任何一个动作:

glStencilOp选项

glStencilOp函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须向任意选项指定至少一个不同的动作。

使用glStencilFunc和glStencilOp,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候片段会被抛弃。

绘制物体轮廓

我们展示一个用模板测试实现的一个特别的和有用的功能,叫做物体轮廓(object outlining)。它能够给每个(或一个)物体创建一个有颜色的边,步骤如下:

  1. 清空所有模板缓存值为0。
  2. 在绘制物体前,把模板方程设置为GL_ALWAYS,用1更新物体所有绘制片段的模板缓存。
  3. 渲染物体。
  4. 关闭模板写入和深度测试。
  5. 每个物体放大一点点。
  6. 使用一个不同的片段着色器用来输出一个纯颜色。
  7. 再次绘制物体,但只是当它们的片段的模板值不为1时才进行。
  8. 开启模板写入和深度测试。

这个过程对于每个物体片段都将模板缓存值设为1,当我们绘制边框的时候,我们基本上绘制的是放大版物体的通过测试的地方,这样放大版绘制后物体就会有一个边框。我们基本会使用模板缓冲丢弃所有的不是原来物体的片段的放大的版本内容。

我们需要创建一个基本的片段着色器,它输出一个边框颜色。

然后先用glClear设置所有像素的模板值为0。 接着开启模板测试,设置模板、深度测试通过或失败时才采取动作:

1
2
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

如果任何测试失败我们都什么也不做。如果片段的模板测试和深度测试都成功了,我们就将储存着的模板值替换为1,我们要用glStencilFunc来做这件事:

1
2
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲
glStencilMask(0xFF); // 设置模板缓冲为可写状态

使用GL_ALWAYS模板测试函数,这样片段总会通过测试,在我们绘制它们的地方,模板缓存就会被替换为参考值1。

现在箱子绘制之处,模板缓冲更新为1了,我们将要绘制放大的箱子,但是这次关闭模板缓冲的写入:

1
2
3
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止修改模板缓冲
glDisable(GL_DEPTH_TEST);

我们把模板方程设置为GL_NOTEQUAL,它保证我们只绘制模板缓存不等于1的部分,即只绘制之前箱子外围的那部分。注意,我们也要关闭深度测试,这样放大的的箱子也就是边框才不会被地面覆盖。

做完之后还要保证再次开启深度缓冲。


参考 LearnOpenGL-CN 模板测试

Scriptable Objects 及 游戏架构

Scriptable Objects 相关介绍,及基于其的游戏架构技术 Continue reading

AssetBundle 最佳实践

Published on January 29, 2019

AssetBundle 基础总结

Published on January 27, 2019