一、简介
Three.js的demo中有一个案例是利用模板(stencil)实现模型的剖切,地址为three.js examples clip stencil,如图所示。本文将详细讲解其实现原理。
关于模板的原理这里不深入展开,具体可以参考模板测试 - LearnOpenGL CN (learnopengl-cn.github.io),大致可以认为模板就是在渲染时标记的一块区域,比如标记为1,可以根据情况来设置模板值等于1时绘制或不绘制,这样就可以利用模板来标记绘制指定区域,并实现很多实用有趣的效果,比如物体边框的高亮显示就可以利用模板实现。而本例中的剖切也是利用模板来实现的。
二、通过模板标记渲染剖面的实现过程
2.1 流程思路及实现原理
- 首先根据剖面裁减掉前面的像素,以如流程图第一张图所示的立方体为例,裁剪后如流程图第二幅图所示。
- 然后正面剔除,只渲染背面,并设置背面渲染时模板值加1,渲染后结果如流程中第三张图所示;
- 然后背面剔除,只渲染正面,这时设置模板值减1,渲染结果如流程第四张图所示。
- 两次渲染后最终的模板值如流程最后一张图所示,可以看到,模板值不为0的区域即为剖面区域,这样就可以将剖面标记出来。
2.2 具体实现过程
2.2.1 模板值标记
Threejs示例demo中实现模板标记的代码如下
js
function createPlaneStencilGroup(geometry, plane, renderOrder) {
const group = new THREE.Group();
const baseMat = new THREE.MeshBasicMaterial();
baseMat.depthWrite = false;
baseMat.depthTest = false;
baseMat.colorWrite = false;
baseMat.stencilWrite = true;
baseMat.stencilFunc = THREE.AlwaysStencilFunc;
// 后面
const mat0 = baseMat.clone();
mat0.side = THREE.BackSide;//只渲染背面
mat0.clippingPlanes = [plane];
// mat0.stencilFail:指定模板测试失败时的模板操作。在这个代码中,THREE.IncrementWrapStencilOp 表示在模板测试失败时,将模板缓冲区的值递增,并且在达到最大值时循环回零。
// mat0.stencilZFail:指定模板测试通过但深度测试失败时的模板操作。在这个代码中,THREE.IncrementWrapStencilOp 表示在深度测试失败时,将模板缓冲区的值递增,并且在达到最大值时循环回零。
// mat0.stencilZPass:指定模板测试和深度测试都通过时的模板操作。在这个代码中,THREE.IncrementWrapStencilOp 表示在模板测试和深度测试都通过时,将模板缓冲区的值递增,并且在达到最大值时循环回零。
mat0.stencilFail = THREE.IncrementWrapStencilOp;
mat0.stencilZFail = THREE.IncrementWrapStencilOp;
mat0.stencilZPass = THREE.IncrementWrapStencilOp;
const mesh0 = new THREE.Mesh(geometry, mat0);
mesh0.renderOrder = renderOrder;
group.add(mesh0);
// 前面
const mat1 = baseMat.clone();
mat1.side = THREE.FrontSide;//只渲染正面
mat1.clippingPlanes = [plane];
mat1.stencilFail = THREE.DecrementWrapStencilOp;
mat1.stencilZFail = THREE.DecrementWrapStencilOp;
mat1.stencilZPass = THREE.DecrementWrapStencilOp;
const mesh1 = new THREE.Mesh(geometry, mat1);
mesh1.renderOrder = renderOrder;
group.add(mesh1);
return group;
}
这里传入的geometry为需要剖切的几何体的geometry,plane为定义的剖切面,renderOrder为渲染顺序。这个函数就是用来对需要剖切的几何体,根据剖切面,利用模板值标记出剖面区域。这里的具体实现方式就是针对需要剖切的几何体设置两个材质,分别用来完成背面渲染和正面渲染。其中每个材质都关闭深度写入和深度测试以及颜色写入,并开启模板值写入,设置模板测试为永远通过模板测试,就是只要渲染,就一定会通过模板测试。针对正面和背面分别设置材质属性side,使其只渲染正面或背面,然后设置对应的模板测试通过时的操作,是模板值加1还是减1。最后将由设置好的材质和geometry生成的两个mesh加入group返回,在后面会加入scene中,在render之后就会标记出剖面的模板值。
2.2.2 根据模板值完成剖面的渲染
Threejs示例demo中完成这一过程的主要代码如下
js
planeObjects = [];
const planeGeom = new THREE.PlaneGeometry(4, 4);
for (let i = 0; i < 3; i++) {
const poGroup = new THREE.Group();
const plane = planes[i];
const stencilGroup = createPlaneStencilGroup(geometry, plane, i + 1);
// plane is clipped by the other clipping planes
const planeMat =
new THREE.MeshStandardMaterial({
color: 0xE91E63,//剖面颜色
metalness: 0.1,
roughness: 0.75,
clippingPlanes: planes.filter(p => p !== plane),
stencilWrite: true,
stencilRef: 0,
//设置模板测试函数为不等于参考值的测试。即,如果模板缓冲区的值不等于参考值,则通过模板测试。
stencilFunc: THREE.NotEqualStencilFunc,
//设置模板测试失败时的模板操作为替换模板缓冲区的值。
stencilFail: THREE.ReplaceStencilOp,
//设置模板测试通过但深度测试失败时的模板操作为替换模板缓冲区的值。
stencilZFail: THREE.ReplaceStencilOp,
//置模板测试和深度测试都通过时的模板操作为替换模板缓冲区的值。
stencilZPass: THREE.ReplaceStencilOp,
});
const po = new THREE.Mesh(planeGeom, planeMat);
po.onAfterRender = function (renderer) {
renderer.clearStencil();
};
po.renderOrder = i + 1.1;
object.add(stencilGroup);
poGroup.add(po);
planeObjects.push(po);
scene.add(poGroup);
在完成剖面区域模板值的标记之后,就可以根据剖面区域的模板值设置模板测试函数,完成剖面区域的渲染了。首先定义一个平面几何体PlaneGeometry,其作用是用来渲染剖面,然后给平面几何体设置材质,材质属性中模板测试函数设置为不等于,即当对应位置像素缓存中模板值不等于参考值0的时候才渲染,否则不进行渲染绘制。这样就可以只绘制模板值不等于0的剖面区域。因为示例demo中设置三个剖面,所以要设置渲染顺序,并且在每一个剖面标记和渲染完之后要清空模板缓存。