模板测试 STENCIL_TEST
参考深度测试这章,我们对物体的深度进行测试,那么模板测试自然是对模板进行测试了。
在深度测试中,我们可以为顶点定义它的深度,深度缓冲区自带默认值,自然能进行比较测试。 但模板是什么呢,我们可以简单的将其认为是一个蒙版,同时我们可以任意的创建一个蒙版,而模板缓冲区就是默认模板。
如下代码我们就可以开启它,后面我们来一起研究各个参数对模板的影响。
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0xff);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.INVERT);
首先,stencilFunc(func, ref, mask)接收3个参数:
-
func 用来指定
参考值与模板缓冲区的比较方式,可以有以下值:gl.NEVERgl.LESSgl.EQUALgl.LEQUALgl.GREATERgl.NOTEQUALgl.GEQUALgl.ALWAYS
-
ref 就是我们用来和模板缓冲区进行对比的参考值,取值范围是0-2^n - 1,n为模板缓冲区位深度,可以通过gl.getParameter(gl.STENCIL_BITS)查询
-
mask 是位掩码值,用来指定参考值与模板缓冲区中的值在比较的时候使用哪些位。它是一个16进制的数,其取值范围是 0x00 - 0xFF,换算为二进制正好为 00000000 - 11111111。假设我们设置掩码值为0x0F,其二进制为00001111,也就是说参考值与模板缓冲区比较的时候,将参考值与目标值都转化为二进制,只有后四位参与比较,前四位忽略。这里我们后面会在代码中详说。
再一个就是 stencilOp(fail, zfail, zpass),这个函数的作用是,在上面的stencilFunc通过或者失败以后,如何操作模板缓存区,同样接收3个参数:
- fail 当模板测试失败的时候执行的操作
- zfail 当深度测试失败的时候执行的操作
- zpass 当模板测试和深度测试都成功的时候执行的操作
这三个参数的取值都一样,如下:gl.KEEPgl.ZEROgl.REPLACEgl.INCRgl.INCR_WRAPgl.DECRgl.DECR_WRAPINVERT
下面我们来仔细研究一下它们的具体使用。
先来创建一个简单的三角形
虽然说只是一个简单的红色三角形,但仔细看以下两行代码:
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.EQUAL, 0x00, 0xFF,);
第一行开启模板测试。
第二行设置模板比较函数,当模板值等于0x00的时候通过测试,掩码为0xff,即每一位都参与比较。
同学们可以调一下参考值的值,可以发现,只有在0x00的时候才可以绘制出三角形,其他任何值都不可以。由此可见,模板缓冲区的默认值就是0,或者说是0xff。
我们修改比较函数如下:
gl.stencilFunc(gl.EQUAL, 0xF0, 0x0F,)
仍然可以绘制出三角形。
我们设置的是模板值等于0xf0的时候通过测试,但模板值默认为0x00,显然不相等。
这是因为我们设置了掩码值为0x0f,也就是00001111,前四位不参与比较。
默认值为00000000,参考值为11110000,后四位一致,通过测试,这就是掩码值的作用。
一个简单的模板
让我们看看如下代码:
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.GREATER, 0x0F, 0xFF);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
let triangleA = [
0, 0, -0.5,
0, 0.5, -0.5,
0.5, 0, -0.5,
];
drawTrangle(triangleA, [1, 0, 0])
gl.clear(gl.COLOR_BUFFER_BIT);
// 以上为第一次绘制
gl.stencilFunc(gl.NOTEQUAL, 0x0f, 0xFF);
let triangleB = [
-0.1, -0.1, -0.6,
-0.1, 0.6, -0.6,
0.6, -0.1, -0.6,
];
drawTrangle(triangleB, [0, 1, 0])
结果如下
我们是如何得到这个结果的呢?
首先打开模板测试,设置如果参考值0x0F如果比模板值更大,则通过测试。然后加上了stencilOp函数,这里三个值的含义分别是:
- 当模板测试失败时,保持模板值不变
- 当深度测试失败时,保持模板值不变
- 当模板测试和深度测试都通过时,将模板值设为参考值(也就是设为0x0F)
然后我们其实已经得到了一个我们自定义的模板,在模板缓冲区中,三角形A的区域,它的值被我们设为了0x0F。
之后我们清空了颜色缓冲区,其实如果这里不清空的话,会有一个红色三角形,某种意义上来说,这个三角形就是我们的模板。
接下来是第二次绘制,我们先将模板测试函数改为:当参考值不等于模板值的时候,通过测试。
从三角形B的坐标可以看出,它的面积是比三角形A更大的,所以在两个三角形的重叠部分,参考值0x0f与模板值0x0f相等,无法通过模板测试,就不会绘制。得到的结果就像是使用三角形A把三角形B扣掉了一部分。这是最简单的模板的作用。
到这里有一点非常重要,必须要提:实现任何一种效果,都不一定只有一套与之对应的stencilFunc,stencilOp。我在这里这么设置,是想尽量多的展示使用不同参数对缓冲区造成的变化,希望大家在学习的时候可以举一反三的理解不同参数的含义
来一个描边效果试试吧
function draw(positions, color, scale)
我们修改了绘制的函数,现在可以传入一系列的顶点,颜色,以及一个缩放因子。
然后是主角:
let rectangle = [
-0.5, -0.5, 0,
-0.5, 0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0,
0.5, -0.5, 0,
];
gl.enable(gl.STENCIL_TEST);
gl.stencilFunc(gl.ALWAYS, 1, 0xFF);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
draw(rectangle, [1, 0, 0], [1, 1, 1, 1])
gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);
draw(rectangle, [0, 0, 1], [1.05, 1.05, 1, 1])
首先我们定义了一个矩形,然后打开模板测试,设置总是通过测试,并把参考值设为新的模板值。然后绘制出我们的主体红色矩形。
之后设置新的模板函数当参考值不等于模板值的时候通过测试,然后绘制一个矩形,这时我们换了个颜色,也顺便把x方向和y方向都放大了一下。
于是乎,我们得到了一个描边的效果,可以通过缩放因子的大小控制描边的大小。
描边在世界坐标系下的一个问题
let triangle = [
-0.5, -0.5, 0,
-0.5, 0.5, 0,
0.5, -0.5, 0,
];
我们更改绘制物体,这次绘制一个红色的三角形并进行描边,可以看到,斜边并没有描边效果。
其实这并不是模板测试的问题,仅仅是因为我们在描边时,使用的缩放因子,其在计算后得到的坐标并不是我们想要的效果,并不是将物体往四面八方等比放大。
在这里不多啰嗦这个问题,因为在实际应用中,物体的顶点位置都是相对于自身定义的,其中心基本是在物体内部,缩放因子自然可以往四面八方进行缩放。
现在来试试切面效果
你可以拖动右上方的Slider来改变切面的位置。
先来讲讲这段代码的流程吧:
首先创建了两个着色程序,第一个着色程序中传入了一个切面uPlane,它会将对我们的立方体进行切割。第二个着色程序我是绘制整个屏幕,下面的代码中我们会让它只在对应模板值的地方显示,也就是我们要的切面效果。
然后的代码就是初始化界面右上角slider,定义了一个drawBox方法,它是我们绘制立方体的主要代码。drawStencil的代码很简单,就是全屏绘制红色,但配合上模板测试,就只会在切面绘制了。
一切准备就绪,让我们关注这篇文章的重点:mainDraw()
function mainDraw() {
gl.enable(gl.STENCIL_TEST)
gl.stencilFunc(gl.ALWAYS, 0x0f, 0xff);
gl.stencilOp(gl.KEEP, gl.REPLACE, gl.INVERT);
drawBox(programBox, boxMesh, boxColor)
gl.disable(gl.DEPTH_TEST);
// gl.clear(gl.COLOR_BUFFER_BIT);
// gl.clear(gl.DEPTH_BUFFER_BIT);
gl.stencilFunc(gl.EQUAL, 0xff, 0xff);
drawStecil()
}
首先我们打开模板测试,设置模板测试总是通过,参考值设为0x0f。
设置模板测试失败时KEEP,模板测试通过但深度测试失败时REPLACE(即将模板值设置为我们上面设置的参考值),两个都通过时将模板值置反。
然后绘制立方体。我们可以停在这里,将后面的代码忽略,得到如下的结果:
可以看到立方体被开了一个斜角,裸露出了绿色的底面,紫色的背面以及橘色的左面。
然后我们关闭深度测试,这是因为我们绘制切面传入的值是z值为0的坐标,我们要保证切面不会被遮挡。
然后我们设置模板测试函数为当模板值等于参考值0xff的时候通过测试。(思考为什么是0xff)
最后绘制切面,得到一块完美的红色切面。
我们可以打开被我注释的代码 // gl.clear(gl.COLOR_BUFFER_BIT); 得到以下结果
没错我们只显示了切面,正面我们的模板值是正确的。
但是切面真的仅仅如此吗?切面只能通过上面的方式得到吗?答案是否定的。
还记得我们第一次设置的参考值0x0f吗,其实你可以试试将他设为任何值(除了0xff),然后将stencilOp也设为任何值,你会发现我们仍然得到同样正确的切面。
这是为什么?让我们捋一捋模板测试吧。
- 每当我们在画布上进行绘制的时候,就会进行一次模板测试,也就是说:
对于三维物体,画布上同一个点,不同深度的情况下,会绘制两次,也就会进行两次模板测试 - 首先对于
切面来说:我们在fs里面进行了一次discard丢弃,绘制出来的是一个裸露的效果,虽然这部分顶点是在后面,但前方的已被丢弃。进行模板参数的时候,模板测试通过,深度测试通过,进行置反,模板值0x00置反为0xff,这也就是为什么我们第二次绘制的时候设置当模板值等于0xff时通过测试。 - 对于没有被切除的部分,以前面为例:按照绘制顺序(物体顶点的传入顺序),
前面的顶点通过模板测试,通过深度测试,置反设为0xff,欸嘿你要是那完蛋了,这里是0xff是不是也要绘制了,错,因为还有重叠的后面进行模板测试,模板测试通过,深度测试失败,将模板值设为0x0f!!!
这里我们可以将第二次的模板函数改为如下
gl.stencilFunc(gl.EQUAL, 0x0f, 0xff);
发现了吗!我们将所有的重叠部分标红了!
小贴士
你以为到此为止了吗,在上面我提到了一句话:
按照绘制顺序(物体顶点的传入顺序)
思考一下,如果我们先绘制后面:模板测试通过,深度测试通过,置反为0xff。然后绘制前面:模板测试通过,深度测试通过,置反为0x00。嗯????发现不对了吗,我把后面的传入顺序放到前面来看一下(boxmesh里面的顶点顺序),第二次绘制的模板函数为:gl.stencilFunc(gl.EQUAL, 0x0f, 0xff);因为我们要看其他部分,结果是:
你会说:what fxxk!一塌糊涂。然后可以将第二次绘制的模板函数设为
gl.stencilFunc(gl.EQUAL, 0x00, 0xff);
可以看到重叠部分的模板值确实变成0x00了
结语
好了到此为止,其实如果只是为了实现切面效果的话,我们大可不必思考后面的内容,但不思考怎么能说是完全理解模板测试了呢。
还是和上一篇的深度测试一样,希望大家可以乱七八糟的改下两次模板测试函数的参数以及通过后的操作,甚至是传入不同的物体,如果你能正确推导出最后的模板值,以及得到与预期一致的显示结果。恭喜你,区区模板测试,斩于马下!