阅读 578

WebGL模板测试中的“视觉欺骗”

希沃ENOW大前端

公司官网:CVTE(广州视源股份)

团队:CVTE旗下未来教育希沃软件平台中心enow团队

本文作者:

image.png

前言

我们在工作中或多或少都会接触到三维可视化的相关内容,神秘而酷炫它可以更直观地描述物质和更清晰地传递讯息,给产品带来更高的可玩性及更多创造力。

GPU逐片元处理的一些骚操作也可以改变模型的显示效果而实现特定需求,如下图,你看到的绿色轮廓并不是线条拼接而成的线框,橙色的填充面也并不是一个不规则的面片。

本篇文章主要介绍WebGL模板测试的原理,并使用Three.js引擎做了一些简单的应用讲解。如果你对渲染管线的流程已经有了一定了解,接下来就让我们在计算机图形学的海洋中一起🐶 爬吧。

image.png

边框.png

image.png

填充.png

一、WebGL缓冲区

WebGL渲染管线中有多个缓冲区,这些缓冲区其实可以看做是片元不同规则的剔除流程,我们先简要了解一下各个缓冲区的作用。

1.模板缓冲

模板缓冲可以设定掩码,与当前模板缓冲值进行比对,借助设定的测试函数和操作函数,以一定标准丢弃片元,可以设定模型缓冲值是否写入。

2.深度缓冲

深度缓冲存储每个像素的深度值,进行深度测试,从而确定像素的遮挡关系,保证渲染顺序的正确性,可以设定模型深度是否写入。

3.颜色缓冲

颜色缓冲也称为帧缓冲区,存储片元的颜色信息,可以设定模型颜色是否写入。

二、模板测试

模板测试是通过将某像素在模板缓冲区的存储值与设定的参考值比较,以设定的测试方法(glStencilFunc),对通过及未通过测试的像素点进行不同的更新操作(glStencilOp)

glStencilFunc的计算方法为(ref & mask) func (currentValue & mask)。用来将“设定参考值”与“当前模板值”进行比较,得到一个布尔值,表示该像素是否通过模板测试。比较函数由第一个参数指定,它是一个枚举值,可选的值有 GL_GREATER ,GL_LESS 等;第二个是自由设定的参考值;第三个是一个位掩码值,为了方便计算我们一般取0x00或者0xFF

例如某像素的模板缓冲值为A,第二个参数指定的是B,那么比较会先将Amask进行AND操作,并将Bmask进行AND操作,最后用操作函数对二者进行比较。

1.WebGL API

glStencilFunc (func, ref, mask)glStencilOp (fail, zfail, zpass)
func:
GL_NEVER 从来不能通过
GL_ALWAYS 永远可以通过(默认值)
GL_LESS 小于参考值可以通过
GL_LEQUAL 小于或者等于可以通过
GL_EQUAL 等于通过
GL_GEQUAL 大于等于通过
GL_GREATER 大于通过
GL_NOTEQUAL 不等于通过
**fail:**模板测试失败时执行的操作。
GL_KEEP(不改变,这也是默认值)
GL_ZERO(将模板值设为零)
GL_REPLACE(使用测试条件中的设定值来代替当前模板值)
GL_INCR(增加1,如果已为最大值,则保持不变),
GL_INCR_WRAP(增加1,如果已为最大值,则从零重新开始)
GL_DECR(减少1,如果已为零,则保持不变),
GL_DECR_WRAP(减少1,如果已为零,则重新设为最大值)
GL_INVERT(按位取反)
**ref:**参考值。**zfail:**当模板测试成功但深度测试失败时执行的操作。
mask:位掩码,设置为1才会进行测试。** zpass:**当模板测试及深度测试都成功时执行的操作。

2.Three API

Three.jsWebGLRenderer进行了封装,较早版本是在renderer中对模板缓冲进行操作,现有版本已经将其挂在了material下。参数含义与WebGL API相差不大,具体参数可参见官网threejs.org/docs/index.…

THREE.material
.stencilWrite是否开启缓冲测试。默认为false。
.stencilWriteMask写入模板缓冲区时使用的位掩码。默认值为0xFF。
stencilFunc使用的测试函数。默认为AlwaysStencilFunc。
stencilRef比较时使用的参考值。默认值为0。
stencilFuncMask模板值比较时使用的位掩码。默认值为0xFF。
stencilFail模板测试失败时执行的操作。默认为KeepStencilOp。
stencilZFail当模板测试成功但深度测试失败时执行的操作。默认为KeepStencilOp。
stencilZPass当模板测试及深度测试都成功时执行的操作。默认为KeepStencilOp。

三、应用示例

通过模板测试我们可以实现多种特效,例如蒙板裁剪、轮廓高亮、反射效果,剖面填充等。本篇主要介绍两种:轮廓高亮和剖面填充。

1.轮廓

  轮廓.gif

实现轮廓的主要思想是:先渲染一个模型,使每个像素点都通过模板测试并替换成指定初始值,用于记录原模型的模板信息;再渲染一个稍大比例的模型,使得模板值不为1的部分通过测试并显示出来,达到模拟轮廓的目的。关键代码如下所示。

objLoader.load("model.obj", function (object) {
  // 模型
  const model = object.clone();
  model.children.forEach(function (child) {
    const mat = child.material.clone();
    mat.stencilWrite = true;
    mat.stencilFunc = THREE.AlwaysStencilFunc;
    mat.stencilRef = 1;
    mat.stencilZPass = THREE.ReplaceStencilOp;
    child.material = mat;
  });
  scene.add(model);
  // 边框
  const outline = object.clone();
  outline.children.forEach(function (child) {
    child.material = new THREE.MeshBasicMaterial({
      color: "#31B404",
      stencilWrite: true,
      stencilFunc: THREE.NotEqualStencilFunc,
      stencilRef: 1,
    });
  });
  outline.scale.set(1.05, 1.05, 1.05);
  scene.add(outline);
});
复制代码

2.填充

未填充.gif填充.gif

要想正确实现截面的填充,这对模型制作时的面法线有一定要求。一般一个闭合的多面体,外部为正面,闭合的内部为背面。此时摄像机到闭合物体的任何一条不外切于表面的视线,都会穿越n个面向摄像机的正面和n个面向摄像机的背面,而视线穿过非闭合物体时面向摄像机的正面与背面的数量相差1。由此我们可以使背面模板值累增、正面模板值递减的方法来确定一个暴露出来的背面,达到“修补”的目的。关键代码如下所示。

  1. 开启局部裁切功能,渲染一个被裁切的模型。
// 开启局部裁切功能
renderer.localClippingEnabled = true;

// 渲染模型并添加裁切面
const model = object.clone();
model.children.forEach(function (child) {
  child.material.stencilWrite = false;
  child.material.side = THREE.DoubleSide;
  child.material.clippingPlanes = planes;
});
scene.add(model);
复制代码
  1. 使用只开启正面和只开启背面渲染的模型用于确定暴露面的位置。
// 设置用于获取模板值标记的基础材质,只开启模板测试,不写入深度和颜色,不显示
const baseMat = new THREE.MeshBasicMaterial();
baseMat.depthWrite = false;
baseMat.depthTest = false;
baseMat.colorWrite = false;
baseMat.stencilWrite = true;
baseMat.stencilFunc = THREE.AlwaysStencilFunc;

// 获取背面标记,背面累增
const modelBackSide = object.clone();
modelBackSide.children.forEach(function (child) {
  const mat = baseMat.clone();
  (mat.clippingPlanes = planes), (mat.side = THREE.BackSide);
  mat.stencilFail = THREE.IncrementWrapStencilOp;
  mat.stencilZFail = THREE.IncrementWrapStencilOp;
  mat.stencilZPass = THREE.IncrementWrapStencilOp;
  child.material = mat;
});
scene.add(modelBackSide);

// 获取正面标记,正面递减
const modelFrontSide = object.clone();
modelFrontSide.children.forEach(function (child) {
  const mat = baseMat.clone();
  (mat.clippingPlanes = planes), (mat.side = THREE.FrontSide);
  mat.stencilFail = THREE.DecrementWrapStencilOp;
  mat.stencilZFail = THREE.DecrementWrapStencilOp;
  mat.stencilZPass = THREE.DecrementWrapStencilOp;
  child.material = mat;
});
scene.add(modelFrontSide);
复制代码
  1. 填充面进行模板测试,只在暴露面位置渲染。
// 设置填充面的材质,等于1则显示
const planeMat = new THREE.MeshStandardMaterial({
  color: 0xfe9a2e,
  stencilWrite: true,
  stencilRef: 1,
  stencilFunc: THREE.EqualStencilFunc,
  stencilFail: THREE.ReplaceStencilOp,
  stencilZFail: THREE.ReplaceStencilOp,
  stencilZPass: THREE.ReplaceStencilOp,
});
const planeGeometry = new THREE.PlaneGeometry(30, 30);
const plane = new THREE.Mesh(planeGeometry, planeMat);
scene.add(plane);
复制代码

总结

模板测试的作用、使用方法、及应用示例就简要介绍到这里,模板测试配合着色器材质剔除指定位置的片元还可以做出更神奇的效果,感兴趣的小伙伴可以一起尝试一起探索。本文若有理解不当之处可以留言指正。

文章分类
前端