• 首页
  • 技术
  • 旅行
  • 车模
  • 关于

JZ's Blog

{技术, 绘画, 旅行, 摄影, 多肉}
  • 首页
  • 技术
  • 旅行
  • 车模
  • 关于
首页  /  技术  /  WebGL绘制详解之五:Stencil Buffer
05 四月 2016

WebGL绘制详解之五:Stencil Buffer

作者:jz1108
技术 WebGL 8 条留言

看完本篇文章,你会了解到:

  • 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

最左侧是原始渲染效果,中间是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,最后把三角形画出来。

你会在浏览器中看到如下效果:

stencil buffer 模板缓冲区demo

如果想得到相反的结果要怎么写呢?很简单,修改一下stencilFunc方法,把条件改为当buffer不等于1时通过:

gl.stencilFunc(gl.NOTEQUAL, 1, 0xFF);

你会得到下面相反的效果:

使用图片作为模板

有人可能会问,我能使用一张图片当做模板吗?就像Photoshop里面那样给一个图层增加一个蒙板(mask layer)。答案是肯定的,但需要注意的是stencil test只能是二元的,即通过还是不通过,如果你想实现一个具有256级灰度的蒙板就不能依赖于stencil了。

Photoshop中的蒙板

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

包含透明信息的PNG8格式图片(注意灰白格子表示透明区域)

完整的例子请看这里。

最终结果如下图所示:

模板缓冲区使用过程概览

在实际使用中,我发现经常会忘记模板缓冲区的使用流程和机制,于是这里对整个流程做一个简要概括,方便随时查阅。

  1. 首先将模板形状绘制到模板缓冲区,这个过程中通常会禁止写入颜色,模板检测设置为总通过,设置好之后会调用一次绘制。
  2. 重新调整模板检测方法,指名什么情况下通过测试,禁止写入模板缓冲区,恢复写入颜色缓冲区,再进行一次绘制。

补充渲染管线

我们把Stencil Test过程补充到WebGL渲染管线中:

WebGL Rendering Pipeline WebGL 渲染管线

 Previous Article 再游新加坡——最后一天
Next Article   WebGL摄像机详解之二:实现一个透视摄像机

相关文章

  • iOS 13 Safari 的一些变化

    2019年11月13日
  • WebGL 文章汇总

    2019年9月12日
  • 高性能 WebGL —— 使用 ImageBitmap 提升纹理性能

    2019年3月24日

8 条留言

  1. kangfu Reply to kangfu
    2016年7月10日 at 下午4:58

    gl.REPLACE表示通过测试时用stencilFunc的ref的值来进行替代,结果就是绘制这个模板三角形的时候stencil buffer中三角形的有效区域对应的值都是1(这里就是我没太明白的地方,假设第三个参数给KEEP,那么stencil buffer里对应的值该是多少呢?)

    设置keep常量,所有点的模板值是0吧,也就是默认值。

    • jz1108 Reply to jz1108
      2016年7月15日 at 下午3:08

      被你发现了,我更新一下内容。

      • cz Reply to cz
        2016年8月11日 at 下午7:18

        然而并没有。。更新

        • jz1108 Reply to jz1108
          2016年8月16日 at 上午10:45

          好了。

  2. 老蒋 Reply to 老蒋
    2019年10月8日 at 上午11:46

    写得很好 怎么不继续写呢

  3. sixsir Reply to sixsir
    2020年5月3日 at 上午11:14

    写的非常好,通俗易懂,支持下!

  4. 谢磊 Reply to 谢磊
    2020年11月29日 at 下午3:06

    汗,越是强调的东西也容易忽略,检查半天查不出错误原因。获取gl时,忘记加{ stencil: true }

  5. lew Reply to lew
    2022年1月21日 at 下午3:39

    这是我看过写最好的

留言

取消回复

近期文章

  • 记录一次解决 no buffer is bound to enabled attribute 报错的过程
  • TypeScript 中的装饰器
  • iOS 13 Safari 的一些变化
  • WebGL 文章汇总
  • 高性能 WebGL —— 使用 ImageBitmap 提升纹理性能

近期评论

  • jz1108发表在《记录一次解决 no buffer is bound to enabled attribute 报错的过程》
  • xiangnan发表在《记录一次解决 no buffer is bound to enabled attribute 报错的过程》
  • lew发表在《WebGL绘制详解之五:Stencil Buffer》
  • jz1108发表在《WebGL基础绘制之一:绘制一个点》
  • jz1108发表在《WebGL 文章汇总》

文章归档

  • 2021年三月
  • 2019年十一月
  • 2019年九月
  • 2019年三月
  • 2018年十二月
  • 2018年十一月
  • 2018年十月
  • 2017年十一月
  • 2017年十月
  • 2017年九月
  • 2017年五月
  • 2017年四月
  • 2017年三月
  • 2017年二月
  • 2017年一月
  • 2016年十一月
  • 2016年十月
  • 2016年四月
  • 2016年三月
  • 2016年二月
  • 2016年一月
  • 2015年十二月
  • 2015年十一月
  • 2015年十月
  • 2015年九月
  • 2015年八月
  • 2015年七月
  • 2015年六月
  • 2015年三月
  • 2014年十二月
  • 2014年十一月
  • 2014年十月
  • 2014年八月
  • 2014年七月
  • 2014年六月
  • 2014年五月
  • 2014年四月
  • 2014年三月
  • 2014年二月
  • 2014年一月
  • 2013年十二月
  • 2013年十一月
  • 2013年十月
  • 2013年九月
  • 2013年八月
  • 2013年七月
  • 2013年六月
  • 2013年五月
  • 2013年四月
  • 2013年三月
  • 2013年二月
  • 2013年一月
  • 2012年十二月
  • 2012年十月
  • 2012年九月
  • 2012年七月
  • 2012年六月
  • 2012年五月
  • 2012年一月
  • 2011年十一月
  • 2011年十月
  • 2011年九月
  • 2011年八月
  • 2011年七月
  • 2011年六月
  • 2011年五月
  • 2011年四月
  • 2011年二月
  • 2011年一月
  • 2010年十二月
  • 2010年十月
  • 2010年九月
  • 2010年六月
  • 2010年五月
  • 2010年四月
  • 2010年三月
  • 2010年二月
  • 2010年一月
  • 2009年十二月
  • 2009年十一月
  • 2009年九月
  • 2009年八月
  • 2009年六月
  • 2009年三月
  • 2009年二月
  • 2009年一月

分类目录

  • 地图API
  • 多肉
  • 开发
  • 技术
  • 摄影
  • 旅行
  • 未分类
  • 汽车
  • 绘画
  • 美食
  • 车模
  • 随笔

功能

  • 登录
  • 文章RSS
  • 评论RSS
  • WordPress.org
© Copyright 2015. Theme by BloomPixel.