看完本篇文章,你会了解到:
- Stencil Buffer是什么,可以用来干什么,Stencil Test是怎么回事儿
- 如何使用Stencil Buffer
Stencil Buffer 概念
Stencil的本意是模板,是指刻有镂空图案的板子。拿着这样的板子就可以方便快速的画出某些特定形状,我记得在幼儿园就做过这样的东西,类似下面这张图:在一张纸上事先刻好图案,即模板,接着把它覆盖在要喷涂的地方,喷涂完成之后拿开纸板就能看到图案了。
图片来自:http://gomedia.com/zine/tutorials/how-to-design-occult-looking-band-logo/
WebGL中的stencil buffer(模板缓冲区)的作用也是类似的,可以在Buffer中指定一个形状作为模板,接着通过stencil test(模板测试)过程让位于形状内部的物体显示,而外部不显示,类似遮罩的效果。当然也可以反过来,让形状内部不显示物体,而外部显示。Stencil buffer为每个fragment提供8位的存储空间,即可以存储256个不同的数值,但是如果要实现一个简单的模板剪裁效果,其实1位(0和1)就够用了。Stencil buffer的作用如下图所示:
最左侧是原始渲染效果,中间是Stencil Buffer的内容,右侧是根据Buffer剪裁之后的效果。
如何使用
在stencil buffer相关操作中,有两个很重要的方法:gl.stencilFunc
和 gl.stencilOp
。前者用来控制stencil的测试方式,即怎样才算通过测试;后者用来指定通过测试和未通过测试时要怎么处理。
stencilFunc的原型如下:
void stencilFunc(GLenum func, GLint ref, GLuint mask);
参数含义如下:
- func:指定测试函数,允许的值为NEVER、LESS、LEQUAL、GREATER、GEQUAL、EQUAL、NOTEQUAL、ALWAYS。这些名字含义很明确,不再详述。
- ref:用来做stencil测试的参考值。
- mask:指定操作掩码,在测试的时候会先将ref与mask进行与运算,再将ref与buffer中的值进行与运算,最后将两个结果进行比较,比较的方法由func参数所指定。
单独看这些参数定义我估计你还是有点儿晕,来看一个实际的例子,这是本文例子当中所出现的:
gl.stencilFunc(gl.EQUAL, 1, 0xFF);
这句话的意思就是说首先将参考值1与0xFF进行与运算,这里仍然得到是1,再将对应的stencil buffer中的值也与上1,实际上就是获取stencil buffer所有的有效位。mask在默认情况下为0xFF,即表示ref参数与stencil buffer所有位都要参与比较。func在这里是EQUAL,它表示buffer中等于1的通过测试,不等于1的则不通过。如果用伪代码形式描述上面的操作,则可表示为:
if (1 & 0xFF == stencil & 0xFF) { pass stencil; }
进而可简化为:
if (1 == stencil) { pass stencil; }
stencilOp的原型如下,它的作用是控制输出到模板缓冲区的内容到底是什么:
void stencilOp(GLenum fail, GLenum zfail, GLenum zpass);
参数含义如下:
- fail:指定当stencil测试未通过时的行为,允许的值为KEEP、ZERO、REPLACE、INCR、INCR_WRAP、DECR、DECR_WRAP、INVERT。
- zfail:指定当stencil测试通过但是depth测试未通过时的行为。允许的值同fail。
- zpass:指定当stencil测试通过且depth测试也通过时的行为,或者当stencil测试通过且当前没有depth buffer或者depth测试被关闭时的行为。允许的值同fail。
下面是每个行为的说明:
- KEEP:保持当前值不变。
- ZERO:设置为0。
- REPLACE:设置stencil buffer的值为ref,该ref是通过stencilFunc指定的。
- INCR:递增当前stencil buffer的值。如果超过buffer允许的最大值则保持为该值。
- INCR_WRAP:递增当前stencil buffer的值。如果递增前buffer的值已经是允许的最大值,则设置buffer值为0,wrap的含义即如此。
- DECR:递减当前stencil buffer的值。如果小于0,则设置buffer值为0。
- DECR_WRAP:递减当前stencil buffer的值。如果递减前buffer的值为0,则设置buffer值为允许的最大值。
- INVERT:进行按位取反操作。
还有一个有用的方法是:
void stencilMask(GLuint mask);
它通过mask来控制写入stencil buffer的有效位。
下面我们通过一个简单的例子来了解如何使用stencil buffer。完整的代码在这里。
在这里例子中我们实现一个最简单的模板剪裁效果:
左侧是原始的三角形渲染效果,这是在讲如何给物体添加颜色的那篇文章中出现的示例,中间是模板的形状,也是一个三角形,最后是剪裁之后的效果。
关键代码分析如下:
gl = canvas.getContext(glContextNames[i], { stencil: true });
这是最重要且最容易忽视的,WebGL默认是没有启用stencil test的,所以一定别忘了在获取context时候指定stencil为true。我在一开始学习的时候就忽略了它,结果不论怎么修改代码就是不生效。
接着看设置缓冲区和绘制部分。首先我们绘制模板到stencil buffer中,实际上和绘制一个普通的图形没什么区别,只不过是绘制到了模板缓冲区中:
// draw the first triangle as stencil var color = [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]; var colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(color), gl.STATIC_DRAW); var aColorPosition = gl.getAttribLocation(glProgram, 'aColor'); gl.vertexAttribPointer(aColorPosition, 4, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aColorPosition); // Always pass test gl.stencilFunc(gl.ALWAYS, 1, 0xff); gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); gl.stencilMask(0xff); gl.clear(gl.STENCIL_BUFFER_BIT); // No need to display the triangle gl.colorMask(0, 0, 0, 0); var vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(stencilVertex), gl.STATIC_DRAW); var aVertexPosition = gl.getAttribLocation(glProgram, 'aPos'); gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aVertexPosition); gl.drawArrays(gl.TRIANGLES, 0, stencilVertex.length / 3);
在这段代码中我们将一个三角形绘制到stencil buffer里,绘制模板不需要经过stencil test,或者说全部要通过stencil test,这样才能把模板绘制完整,只有在有了模板信息之后才需要判断哪些区域通过测试,哪些区域不通过。所以stencilFunc第一个参数设置为gl.ALWAYS表示总是通过测试,那么这时候第二个参数ref还有作用吗?它的作用我们会在后面看到,第三个参数mask将8位全设置为1,表示所有位都要用。
下面的stencilOp设定通过和没通过的行为,因为这里都是通过测试的,所以直接看第三个参数就好,前两个参数在这里是不起作用的。gl.REPLACE表示通过测试时用stencilFunc的ref的值来进行替代,结果就是绘制这个模板三角形的时候stencil buffer中三角形的有效区域对应的值都是1。
接下来stencilMask设置为0xff,并调用clear方法清除stencil buffer,此时stencil buffer中的内容全部为0。对于一个简单的模板来说,在模板内部是一个状态,在模板外部是另外一个状态,因此只需要两个值就可以加以区分。所以我们先将buffer置为0再像上面说的将有效区域设置为1。
由于模板不需要被真正的画出来,所以colorMask都给0,这样就不会向color buffer(颜色缓冲区)写入任何信息,接下来便是简单绘制过程。绘制结束也就意味着stencil buffer中有我们想要的模板数据。
绘制好模板之后,我们再来绘制真正需要显示出来的三角形。代码如下:
// Pass test if stencil value is 1 gl.stencilFunc(gl.EQUAL, 1, 0xFF); gl.stencilMask(0x00); gl.colorMask(1, 1, 1, 1); // draw the clipped triangle color = [ 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1 ]; gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(color), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW); gl.drawArrays(gl.TRIANGLES, 0, vertex.length / 3);
stencilFunc在前面已经讲到,含义是stencil buffer为1表示通过。由于现在不需要再向stencil buffer里写入内容,因此也没有必要设置stencilOp,同时stencilMask设置为0x00。这时再恢复写入color buffer,设置colorMask各个参数为1,最后把三角形画出来。
你会在浏览器中看到如下效果:
如果想得到相反的结果要怎么写呢?很简单,修改一下stencilFunc方法,把条件改为当buffer不等于1时通过:
gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);
你会得到下面相反的效果:
使用图片作为模板
有人可能会问,我能使用一张图片当做模板吗?就像Photoshop里面那样给一个图层增加一个蒙板(mask layer)。答案是肯定的,但需要注意的是stencil test只能是二元的,即通过还是不通过,如果你想实现一个具有256级灰度的蒙板就不能依赖于stencil了。

首先准备好当做模板的图片,我们准备通过图片的透明信息来控制模板样子,完全透明的地方认为是非有效区域,属于这部分的内容将不显示,不透明的地方则显示。由于stencil test只可以是二元的,图片保存为含有透明信息的PNG8格式即可。

完整的例子请看这里。
最终结果如下图所示:
模板缓冲区使用过程概览
在实际使用中,我发现经常会忘记模板缓冲区的使用流程和机制,于是这里对整个流程做一个简要概括,方便随时查阅。
- 首先将模板形状绘制到模板缓冲区,这个过程中通常会禁止写入颜色,模板检测设置为总通过,设置好之后会调用一次绘制。
- 重新调整模板检测方法,指名什么情况下通过测试,禁止写入模板缓冲区,恢复写入颜色缓冲区,再进行一次绘制。
补充渲染管线
我们把Stencil Test过程补充到WebGL渲染管线中:
gl.REPLACE表示通过测试时用stencilFunc的ref的值来进行替代,结果就是绘制这个模板三角形的时候stencil buffer中三角形的有效区域对应的值都是1(这里就是我没太明白的地方,假设第三个参数给KEEP,那么stencil buffer里对应的值该是多少呢?)
设置keep常量,所有点的模板值是0吧,也就是默认值。
被你发现了,我更新一下内容。
然而并没有。。更新
好了。
写得很好 怎么不继续写呢
写的非常好,通俗易懂,支持下!
汗,越是强调的东西也容易忽略,检查半天查不出错误原因。获取gl时,忘记加{ stencil: true }
这是我看过写最好的