当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它也可能会丢弃片段。接下来,被保留的片段会进入深度测试,它可能会丢弃更多的片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer),我们可以在渲染的时候更新它来实现一些很有意思的效果。
一个模板缓冲中,(通常)每个模板值(Stencil Value)是8位的。所以每个像素/片段一共能有256种不同的模板值。我们可以将这些模板值设置为我们想要的值,然后当某一个片段有某一个模板值的时候,我们就可以选择丢弃或是保留这个片段了。
在模板测试中有两个很重要的方法是stencilFunc和stencilOp,stencilFunc用来控制stencil的测试方式,得出测试结果。stencilOp根据结果决定要如何处理缓冲中的数据。
stencilFunc用来
模板缓冲的一个简单的例子如下:
模板缓冲首先会被清除为0,之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染(其它的都被丢弃了)。
每个窗口库都需要为你配置一个模板缓冲。GLFW自动做了这件事,所以我们不需要告诉GLFW来创建一个,但其它的窗口库可能不会默认给你创建一个模板库,所以记得要查看库的文档。
测试和混合发生在渲染流程中的最后一个阶段,在这个阶段里,GPU主要的工作是逐片元操作,将片元的颜色以某种形式合并,得到最终在屏幕上显示的像素颜色。
在webgl中的测试有裁剪测试、透明的测试、模板测试以及深度测试。这几个测试都是高度可配置的,测试流程如下图:
通常用户在启用模板缓冲的时候,会将整个模板缓冲中的所有片段模板值设置为0,从而丢弃所有的片段,然后再设置特定区域的模板值以及比较函数。GPU会读取用户设置的模板值,然后将该值和模板缓冲中该位置的模板值,按比较函数进行比较,最终决定是保留还是舍弃该片段,形成遮罩效果。
glStencilFunc(GLenum func, GLint ref, GLuint mask)一共包含三个参数:
func
:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref
值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。ref
:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。mask
:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。测试的时候,ref会先和mask做与运算,再将
模板缓冲中的值与mask做与运算,最后把这两个与运算的值,代入比较函数得出结果。
所有的比较函数如下:
举个例子:
glStencilFunc(gl.GEQUAL, 1, 0xFF)
先将参考值1与0xff做与运算得到运算结果:1,将运算结果再与"模板缓冲和0xff进行与运算的值"进行比较。判断是否满足前者大于后者,如果是的话模板测试成功,否则失败。
mask值设为0xff的时候,就等于直接拿参考值和模板缓冲值做比较。
想要禁用模板也可以将mask设置为0x00
经历了glStencilFunc之后,我们就知道模板测试是不是通过。接下来就要对模板缓冲进行操作,这就需要 glStencilOp这个函数了。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项,我们能够设定每个选项应该采取的行为:
sfail
:模板测试失败时采取的行为。dpfail
:模板测试通过,但深度测试失败时采取的行为。dppass
:模板测试和深度测试都通过时采取的行为。每个选项都可以选用以下的其中一种行为:
行为 | 描述 |
---|---|
gl.KEEP | 保持当前储存的模板值 |
gl.ZERO | 将模板缓冲值设置为0 |
gl.REPLACE | 将模板缓冲区值设置为glStencilFunc函数设置的ref 值 |
gl.INCR | 如果模板缓冲值小于最大值则将模板值加1 |
gl.INCR_WRAP | 与GL_INCR一样,但如果模板缓冲值超过了最大值则归零 |
gl.DECR | 如果模板缓冲值大于最小值则将模板值减1 |
gl.DECR_WRAP | 与GL_DECR一样,但如果模板缓冲值小于0则将其设置为最大值 |
gl.INVERT | 按位翻转当前的模板缓冲值 |
默认情况下glStencilOp是设置为(gl.KEEP, gl.KEEP, gl.KEEP)
的,所以不论测试的结果是什么,模板缓冲都会保留它的值。默认的行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你需要至少对其中一个选项设置不同的值。
通常我们这样设置:glStencilOp(gl
..KEEP, gl.KEEP,gl.REPLACE
),测试失败时保持原有值(KEEP
),测试通过的参考值替换模板缓冲值(REPLACE
)
在webgl中模板测试默认是处于禁用状态,使用时需要手动开启,我们采用gl.enable来开启:
gl.enable(gl.STENCIL_TEST);
注意,和颜色和深度缓冲一样,我们也需要在每帧绘制之前清除模板缓冲。
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
如果想自己在webgl上写模板测试,需要在获取gl上下文对象时传入stencil的请求:
const gl = canvas.getContext("webgl",{stencil:true});
深度测试可以帮助实现3D渲染的物体遮挡效果 ,
深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。
当深度测试被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。
深度缓冲是在片段着色器运行之后(以及模板测试运行之后)在屏幕空间中运行的。屏幕空间坐标与通过OpenGL的glViewport所定义的视口密切相关,并且可以直接使用GLSL内建变量gl_FragCoord从片段着色器中直接访问。gl_FragCoord的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)。gl_FragCoord中也包含了一个z分量,它包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值。
深度测试默认是禁用的,所以如果要启用深度测试的话,我们需要用GL_DEPTH_TEST选项来启用它:
glEnable(GL_DEPTH_TEST);
当它启用的时候,如果一个片段通过了深度测试的话,OpenGL会在深度缓冲中储存该片段的z值;如果没有通过深度缓冲,则会丢弃该片段。如果你启用了深度缓冲,你还应该在每个渲染迭代之前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲,否则你会仍在使用上一次渲染迭代中的写入的深度值:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
如果在某些情况下你会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。也就是你在使用一个只读的(Read-only)深度缓冲。OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE
就可以了:
glDepthMask(GL_FALSE);
注意这只在深度测试被启用的时候才有效果。
OpenGL允许我们修改深度测试中使用的比较运算符。这允许我们来控制OpenGL什么时候该通过或丢弃一个片段,什么时候去更新深度缓冲。我们可以调用glDepthFunc函数来设置比较运算符(或者说深度函数(Depth Function)):
glDepthFunc(GL_LESS);
这个函数接受下面表格中的比较运算符:
函数 | 描述 |
---|---|
GL_ALWAYS | 永远通过深度测试 |
GL_NEVER | 永远不通过深度测试 |
GL_LESS | 在片段深度值小于缓冲的深度值时通过测试 |
GL_EQUAL | 在片段深度值等于缓冲区的深度值时通过测试 |
GL_LEQUAL | 在片段深度值小于等于缓冲区的深度值时通过测试 |
GL_GREATER | 在片段深度值大于缓冲区的深度值时通过测试 |
GL_NOTEQUAL | 在片段深度值不等于缓冲区的深度值时通过测试 |
GL_GEQUAL | 在片段深度值大于等于缓冲区的深度值时通过测试 |
默认情况下使用的深度函数是GL_LESS,它将会丢弃深度值大于等于当前深度缓冲值的所有片段。
如果想自己在webgl上写深度测试,需要在获取gl上下文对象时传入depth的请求:
const gl = canvas.getContext("webgl",{stencil:true,depth:true});
打开TS引擎源码文件cocos\gfx\webgl\webgl-swapchain.ts(低版本打开cocos\gfx\webgl\webgl-device.ts)
有个initStates函数,引擎在这里初始化depth、stencil state:
function initStates (gl: WebGLRenderingContext) {
gl.activeTexture(gl.TEXTURE0);
gl.pixelStorei(gl.PACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// rasterizer state
gl.enable(gl.SCISSOR_TEST);
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK);
gl.frontFace(gl.CCW);
gl.disable(gl.POLYGON_OFFSET_FILL);
gl.polygonOffset(0.0, 0.0);
// depth stencil state
gl.enable(gl.DEPTH_TEST);
gl.depthMask(true);
gl.depthFunc(gl.LESS);
gl.depthRange(0.0, 1.0);
gl.stencilFuncSeparate(gl.FRONT, gl.ALWAYS, 1, 0xffff);
gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.KEEP);
gl.stencilMaskSeparate(gl.FRONT, 0xffff);
gl.stencilFuncSeparate(gl.BACK, gl.ALWAYS, 1, 0xffff);
gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.KEEP);
gl.stencilMaskSeparate(gl.BACK, 0xffff);
gl.disable(gl.STENCIL_TEST);
// blend state
gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
gl.disable(gl.BLEND);
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO);
gl.colorMask(true, true, true, true);
gl.blendColor(0.0, 0.0, 0.0, 0.0);
}
以上初始化的配置,主要针对3D对象,因为2D对象大多数包含透明像素,因此2D管线不需要进行深度测试,比如:对于builtin-sprite.effect这类2d shader中对应深度测试部分,都被默认关闭了:
除了可以在effect文件中初始化深度模板测试 , creator编辑器还可以在material文件中修改:
所有深度、模板测试相关的配置都可以在Pipeline States下修改。
下面我们用案例验证一下上面的深度测试知识:
如图,结合相机位置和右下角相机拍摄出的画面来看,由于长方体柱子的深度值比其他模型的深度值要小,(由于深度测试函数是LESS,深度值小的通过测试)使得长方体背后的模型的片段被剔除了,因此长方体遮挡住了其他物体(包括地面)。接下来我们对长方体的深度测试做一些配置修改,再看看显示效果:
1.关闭长方体模型材质中的的深度测试:
关闭长方体的深度测试表现的效果:
总结:在长方体开启深度测试情况下,由于长方体后方的物体深度值大于长方体,所以这部分片段都被剔除掉了,现在模型没有了深度,这部分片段的深度都被后方的物体模型填充。因此出现这种现象。
下面我们这样配置:
1.把长方体开启深度测试并且把深度测试函数改成GREATER
2.其他物体的深度测试函数还是LESS
这时候的表现是长方体消失了,其他物体正常显示,这是为什么呢?
我们每帧都会做清除深度缓冲的操作,清除的深度缓冲区默认是1.0,表示最大深度值 ,而长方体选择GREATER后深度值大的才可以通过测试写入深度缓冲,长方体模型的所有片段的深度值都不会超过1.0(最大值),所以长方体上片段测试失败,被全部剔除。而之前长方体没有开启深度测试的时候,它不会因为深度测试失败而剔除片段,所以还是有部分能够看的到。
我们用模板测试实现只显示模型的一部分:(下图中左边的模型)