WebGL模板缓冲区

课程目标

  • 理解Stencil Buffer 的概念
  • 掌握Stencil Buffer 的用法

1-stencil buffer简介

Stencil的本意是模板,即有镂空图案的板子。

stencil buffer 就是模板缓冲区的意思,这是一个存储着模板数据的缓冲区。

WebGL模板缓冲区

在上图中,如果没有模板,喷枪中的颜料会直接喷到画布上。

有了镂空的模板后,便可以对颜料进行过滤,从而喷出特定的图案。

Stencil buffer会为每个fragment提供8位的存储空间,在其中可以写入[0,255]的数字。

其实要实现上图那样的简单效果,1位0和1就够了。

我们通过下面的示意图具体说一下这个逻辑。

WebGL模板缓冲区

  • A:要喷绘的颜色
  • B:模板
  • C:模板过滤出的图像

由此我们可以思考一下用模板绘图的逻辑。

1.准备一张模板。这张模板上需要有一堆参考数据reference。

2.喷涂颜色,喷涂之前要指定参考值reference和模板测试方法,这里的reference会与模板的reference做模板测试。

2-stencil buffer的使用

在stencil buffer相关操作中,有两个很重要的方法:gl.stencilFunc()、gl.stencilOp()。

stencilFunc(func, ref, mask) 设置模板测试的前后函数和参考值。

  • func 测试函数,默认gl.ALWAYS。

    下面是其可以取的值,拷贝自MDN,英语很简单,不翻译了。

    • gl.NEVER: Never pass.
    • gl.LESS: Pass if (ref & mask) < (stencil & mask).
    • gl.EQUAL: Pass if (ref & mask) = (stencil & mask).
    • gl.LEQUAL: Pass if (ref & mask) <= (stencil & mask).
    • gl.GREATER: Pass if (ref & mask) > (stencil & mask).
    • gl.NOTEQUAL: Pass if (ref & mask) !== (stencil & mask).
    • gl.GEQUAL: Pass if (ref & mask) >= (stencil & mask).
    • gl.ALWAYS: Always pass.
  • ref 用于指定模板测试的参考值。默认值为0。

  • mask 指定一个逐位掩码,用于在测试完成时对参考值和存储的模板值进行AND运算。默认值为1。

stencilOp(fail, zfail, zpass):指定通过测试和未通过测试时要怎么处理。

  • fail 模板测试失败时要使用的函数。默认值为gl.KEEP。
  • zfail 模板测试通过但深度测试失败时要使用的函数。默认值为gl.KEEP。
  • zpass 模板测试和深度测试都通过时,或者模板测试通过且深度缓冲区无效时要使用的函数。默认值为gl.KEEP。

上面参数可以写入以下值:

  • gl.KEEP 保持当前值。
  • gl.ZERO 将模板缓冲区值设置为0。
  • gl.REPLACE 将模板缓冲区值设置为WebGLRenderingContext.stencilFunc()里的reference。
  • gl.INCR 增加当前模板缓冲区值。最大为unsigned 值。
  • gl.INCR_WRAP 增加当前模板缓冲区值。 增加到unsigned 值时,模板缓冲区的值为零。
  • gl.DECR 减小当前模板缓冲区的值。最小为0。
  • gl.DECR_WRAP 减小当前模板缓冲区的值。减小到0时,模板缓冲区的值为unsigned 值。
  • gl.INVERT 逐位反转当前模板缓冲区值。

接下来我们举个简单的例子。

我要透过一个圆形的遮罩画一个圆,效果如下:

WebGL模板缓冲区

  • A:要喷绘的颜色
  • B:模板
  • C:模板过滤出的图像

其整体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>颜色合成</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
//点位
gl_Position=a_Position;
//尺寸
gl_PointSize=300.0;
}
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
void main(){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=vec4(0,0.7,0.9,1);
}else{
discard;
}
}
</script>
<script>
const canvas = document.querySelector('#canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
//三维画笔
const gl = canvas.getContext('webgl', { stencil: true })
gl.enable(gl.STENCIL_TEST)
// 获取着色器文本
const vsSource = document.querySelector('#vertexShader').innerText
const fsSource = document.querySelector('#fragmentShader').innerText
//初始化着色器
initShaders(gl, vsSource, fsSource)
// 定义背景色,默认为(0,0,0,0)
gl.clearColor(1, 0.95, 0.9, 1.0)
// 定义模板缓冲区的背景值,默认为0,这不是颜色,就是一个模板参考值
gl.clearStencil(0)
// 用定义好的背景色理缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
//获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
//缓冲对象
const vertexBuffer = gl.createBuffer()
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//修改attribute 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
//赋能-批处理
gl.enableVertexAttribArray(a_Position)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)
/* 模板 */
// ALWAYS永远通过测试,1&0xff=1
gl.stencilFunc(gl.ALWAYS, 1, 0xff)
// 当模板测试或深度测试失败时,保留模板当前值,即gl.clearStencil(0)中的0;
// 否则测试都通过,或者模板测试通过且深度缓冲区无效时,取stencilFunc()里的reference,即1。
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE)
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0]), gl.STATIC_DRAW)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)
//不需要绘制模板
gl.colorMask(false, false, false, false)
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 画完后复原colorMask
gl.colorMask(true, true, true, true)
/* 绘图 */
// 指定接下来要绘制的图形与之前模板之间测试方法,以及参考值
gl.stencilFunc(gl.EQUAL, 1, 0xff)
// 反向遮罩
// gl.stencilFunc(gl.NOTEQUAL, 1, 0xff)
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//写入数据
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0.2, 0.2]),
gl.STATIC_DRAW
)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 初始化着色器
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function createProgram(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
return program
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
</script>
</body>
</html>

效果如下:

WebGL模板缓冲区

解释一下其原理。

1.圆形是用一点加上距离算法实现的。

<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
//点位
gl_Position=a_Position;
//尺寸
gl_PointSize=300.0;
}
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
void main(){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=vec4(0,0.7,0.9,1);
}else{
discard;
}
}
</script>

2.在通过canvas获取webgl上下文对象的时候,stencil需要设置为true,并且开启STENCIL_TEST 模板测试功能。

const canvas = document.querySelector('#canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
//三维画笔
const gl = canvas.getContext('webgl', { stencil: true })
gl.enable(gl.STENCIL_TEST)

3.初始化着色器。

// 获取着色器文本
const vsSource = document.querySelector('#vertexShader').innerText
const fsSource = document.querySelector('#fragmentShader').innerText
//初始化着色器
initShaders(gl, vsSource, fsSource)

4.定义背景色和模板值,清理缓冲区。

// 定义背景色,默认为(0,0,0,0)
gl.clearColor(1, 0.95, 0.9, 1.0)
// 定义模板缓冲区的背景值,默认为0,这不是颜色,就是一个模板参考值
gl.clearStencil(0)
// 用定义好的背景色理缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)

现在的模板缓冲区里都是0:

WebGL模板缓冲区

5.获取顶点的attribute 变量,并对其做一下初始化设置。

//获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
//缓冲对象
const vertexBuffer = gl.createBuffer()
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//修改attribute 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
//赋能-批处理
gl.enableVertexAttribArray(a_Position)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)

6.建立一个圆形的模板。

// ALWAYS永远通过测试,1&0xff=1
gl.stencilFunc(gl.ALWAYS, 1, 0xff)
// 当模板测试或深度测试失败时,保留模板当前值,即gl.clearStencil(0)中的0;
// 否则测试都通过,或者模板测试通过且深度缓冲区无效时,取stencilFunc()里的reference,即1。
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE)
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0]), gl.STATIC_DRAW)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)

我们可以画一下看看:

gl.drawArrays(gl.POINTS, 0, 1)

效果如下:

WebGL模板缓冲区

当前的模板缓冲区中,圆内为1,圆外为0:

WebGL模板缓冲区

7.我们只需要用gl.drawArrays()方法把模板数据写入模板缓冲区即可,但不需要显示出来,所以得这么做。

//不需要绘制模板
gl.colorMask(false, false, false, false)
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 画完后复原colorMask
gl.colorMask(true, true, true, true)

现在画布中除了浅黄色的背景,啥也木有了:

WebGL模板缓冲区

8.再绘制一个蓝色的圆,这个圆是要显示出来的,不再是模板了。

// 指定接下来要绘制的图形与之前模板之间测试方法,以及参考值
gl.stencilFunc(gl.EQUAL, 1, 0xff)
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
//写入数据
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0.2, 0.2]),
gl.STATIC_DRAW
)
//清空缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null)
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)

stencilFunc()不是模板专有的方法,因为你要画的图形与模板怎么合成,是两方面的事。

所以在正常画图时,也要通过stencilFunc()方法告诉程序对象,你希望它如何与模板打配合。

stencilFunc()中的gl.EQUAL表示当我这里面的reference与模板那里的reference相等时,代表模板测试通过了。

stencilFunc()中的1是要与之前模板里的reference做比较的。

后面没有执行stencilOp(),这是因我们现在不需要再向stencil buffer里写入内容。

现在stencil buffer的基本操作流程就说完了。

我们在画图时,也可以用stencilFunc()方法实现反向遮罩效果。

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

效果如下:

WebGL模板缓冲区

webgl中stencil buffer是可以精确到模型的哪一面的,比如正面、反面和双面。

要精确到模型的哪一面,将之前的stencilFunc(func, ref, mask)、stencilOp(fail, zfail, zpass)、stencilMask(mask)替换成stencilFuncSeparate(face, func, ref, mask)、stencilOpSeparate(face, fail, zfail, zpass)、stencilMaskSeparate(face, mask)。

其中的face就指定模型哪一面的,其值可以取 gl.FRONT、gl.BACK和gl.FRONT_AND_BACK。

具体我就不再演示了,很简单,大家可以自己试试,若有问题再跟我说。

总结

模板缓冲区使用过程:

1.将模板形状绘制到模板缓冲区,这个过程中通常会禁止写入颜色,模板检测设置为总通过,设置好之后会调用一次绘制,画完后恢复写入颜色缓冲区。

2.重新调整模板检测方法,指明什么情况下通过测试,不再写入模板缓冲区,再进行一次绘制。

我们可以在WebGL渲染管线中对Stencil Test有一个整体认知:

WebGL模板缓冲区

参考链接:

www.jiazhengblog.com/blog/2016/0…

developer.mozilla.org/en-US/docs/…

原文链接:https://juejin.cn/post/7228526570992894010 作者:李伟_Li慢慢

(0)
上一篇 2023年5月3日 上午10:15
下一篇 2023年5月3日 上午10:26

相关推荐

发表回复

登录后才能评论