前言
源码阅读系列鸽了一段时间了,这次接着往下,刨根问底地搞明白如何优雅地把创建的 BVH节点数据 给可视化出来。
之前的两篇难度较大,过程复杂,本篇内容比较少,可以轻松地阅读😁。
涉及技术
这次的文章涉及到 继承实现 Object3D
的 遍历(traverse) 接口,TypedArray 的数据循环遍历,如何构造 bufferData 来构建 线 和 面 。还有如何通过继承 Group
类的方式来实现节点的管理和更新。
解析
继承关系
看下代码的位置:
最后这个 MeshBVHHelper
就是用来可视化 BVH 节点的类,使用时作为单独的对象引入,。由于我写文章的时候,pull
了最新的代码,版本为 "version": "0.7.3"
,使用 MeshBVHHelper
来替代了原来名为 MeshBVHVisualizer
的类。这样做的一个重要原因是为了和 three.js
保持风格统一, three.js
的各种帮助器,比如 CameraHelper
, LightHelper
都是这样的命名风格。这个库如此强大,未来如果作为 addon
添加到 three.js
正式发布里也是极有可能。
把关系图简单画一下:
然后把项目跑起来,继续使用之前的 SkinnedMesh.html 例子 ,来看看它是如何使用的:
meshHelper
的 geometry
是运用过动画数据并合并的 geometry
,这个在这个系列的第一篇有详细说过。
看代码:
看下 meshHelper
的内部方法和 three.js
的命名保持一致,这里面重点是 update
函数,在节点数据更新后调用,节点可视化更新,这个在后面的篇幅里分析:
显然,都继承自 Object3D
这个基础类,Group
提供了对多个 Object3D
的管理,因为 geometry
有分组的情况下,构建后会有多个根节点。 在上一篇里,最后的构建成的 BVH
节点树被扁平化写入到了一段 buffer
里,接下来为了可视化地构建出来是需要去遍历这段数据的,之前的文章里已经熟悉了buffer
的操作,接下来就看下注释的 traverse
函数。
MeshBVH 遍历:
js
// 传入的 callback 函数每次递归运行,当返回值为 false,则停止递归
traverse( callback, rootIndex = 0 ) {
const buffer = this._roots[ rootIndex ];
// Viewers
const uint32Array = new Uint32Array( buffer );
const uint16Array = new Uint16Array( buffer );
_traverse( 0 );
// 闭包函数里有深度选项,传入到 callback 进行判断
function _traverse( node32Index, depth = 0 ) {
const node16Index = node32Index * 2;
// 这段如果不明白看下上一篇,从 buffer 里读出 isLeaf,offset,count,left,right,splitAxis 这几个参数
const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), offset, count );
} else {
// TODO: use node functions here
const left = node32Index + BYTES_PER_NODE / 4;
const right = uint32Array[ node32Index + 6 ];
const splitAxis = uint32Array[ node32Index + 7 ];
const stopTraversal = callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), splitAxis );
// 如上面所说,false 则递归终止
if ( ! stopTraversal ) {
_traverse( left, depth + 1 );
_traverse( right, depth + 1 );
}
}
}
}
MeshBVHHelper 更新
js
...
update() {
const bvh = this.bvh || this.mesh.geometry.boundsTree;
const totalRoots = bvh ? bvh._roots.length : 0;
while ( this._roots.length > totalRoots ) {
const root = this._roots.pop();
root.geometry.dispose();
this.remove( root );
}
for ( let i = 0; i < totalRoots; i ++ ) {
const { depth, edgeMaterial, meshMaterial, displayParents, displayEdges } = this;
if ( i >= this._roots.length ) {
// 每个 BVH 根节点
const root = new MeshBVHRootHelper( bvh, edgeMaterial, depth, i );
this.add( root );
this._roots.push( root );
}
const root = this._roots[ i ];
root.bvh = bvh;
root.depth = depth;
root.displayParents = displayParents;
root.displayEdges = displayEdges;
root.material = displayEdges ? edgeMaterial : meshMaterial;
root.update();
}
}
updateMatrixWorld( ...args ) {
const mesh = this.mesh;
const parent = this.parent;
if ( mesh !== null ) {
mesh.updateWorldMatrix( true, false );
if ( parent ) {
this.matrix
.copy( parent.matrixWorld )
.invert()
.multiply( mesh.matrixWorld );
} else {
this.matrix
.copy( mesh.matrixWorld );
}
this.matrix.decompose(
this.position,
this.quaternion,
this.scale,
);
}
super.updateMatrixWorld( ...args );
}
...
这个更新就是 MeshBVHHelper
作为 Group
来管理根节点可视化类 MeshBVHRootHelper
, 这其中 update
函数是更新各个节点的 顶点数据 ,updateMatrixWorld
这个接口也要实现,顶点数据要和整个模型的 世界矩阵 相乘才能贴合。
最后才是 BVH节点 顶点数据最终的处理类 MeshBVHRootHelper
。
MeshBVHRootHelper
最终的目的就是把节点的 AABB 包围盒可视化出来,有两种方式,渲染盒子的各个顶点的连线或者渲染各个盒子的三角面,如果不熟悉可以查看下 WebGL 或者 OpenGL 相关内容,看下 vertexBuffer
和 indexBuffer
。既然都看到这了,读者可以拿个笔在笔记本上画下立刻就明白了,提示下,三角面的顶点顺序是逆时针的。
VertexBuffer
js
update() {
...
if ( boundsTree ) {
// count the number of bounds required
const targetDepth = this.depth - 1;
const displayParents = this.displayParents;
let boundsCount = 0;
boundsTree.traverse( ( depth, isLeaf ) => {
if ( depth >= targetDepth || isLeaf ) {
boundsCount ++;
return true;
} else if ( displayParents ) {
boundsCount ++;
}
}, group );
// fill in the position buffer with the bounds corners
let posIndex = 0;
const positionArray = new Float32Array(8 * 3 * boundsCount);
boundsTree.traverse((depth, isLeaf, boundingData) => {
const terminate = depth === targetDepth || isLeaf;
if (terminate || displayParents) {
// 把 buffer 里的 boundingData 解析成 { min, max } 的 box 数据
arrayToBox(0, boundingData, boundingBox);
const { min, max } = boundingBox;
for (let x = - 1; x <= 1; x += 2) {
const xVal = x < 0 ? min.x : max.x;
for (let y = - 1; y <= 1; y += 2) {
const yVal = y < 0 ? min.y : max.y;
for (let z = - 1; z <= 1; z += 2) {
const zVal = z < 0 ? min.z : max.z;
positionArray[posIndex + 0] = xVal;
positionArray[posIndex + 1] = yVal;
positionArray[posIndex + 2] = zVal;
posIndex += 3;
}
}
}
return terminate;
}
}, group);
...
}
}
...
其中 boundsTree
是 MeshBVH
实例,前面已经分析过它的 traverse
函数,这里调用遍历函数,去把最新的顶点数据写入到几何体里,由于 geometry
都是索引过的,更新 buffer
操作需要在下一步和索引数据一起准备好了再写入。
Index Buffer
js
update() {
...
// 线模式
if (this.displayEdges) {
// fill in the index buffer to point to the corner points
indices = new Uint8Array([
// x axis
0, 4,
1, 5,
2, 6,
3, 7,
// y axis
0, 2,
1, 3,
4, 6,
5, 7,
// z axis
0, 1,
2, 3,
4, 5,
6, 7,
]);
} else {
// 面模式
indices = new Uint8Array([
// X-, X+
0, 1, 2,
2, 1, 3,
4, 6, 5,
6, 7, 5,
// Y-, Y+
1, 4, 5,
0, 4, 1,
2, 3, 6,
3, 7, 6,
// Z-, Z+
0, 2, 4,
2, 6, 4,
1, 5, 3,
3, 5, 7,
]);
}
...
}
线模式:
面模式:
update
至此,最新的 顶点数据 和 索引数据 已经获取,接下来写入就行了。
js
update() {
...
// vertex data
...
// index data
...
// update the geometry
geometry.setIndex(
new BufferAttribute( indexArray, 1, false ),
);
geometry.setAttribute(
'position',
new BufferAttribute( positionArray, 3, false ),
);
this.visible = true;
}
结语
继承 three.js
的 Object3D
类来实现对应的接口,使用 bufferGeometry
来构建几何体,一颗 BVH 树 的 drawCall 只需要一次,性能和体验上都很友好。
本篇篇幅较短,一个是内容比较少,一个是前面的文章有提到的部分就不再重复了,如果有不清楚的可以看下前面的系列文章:
写文章不易,点赞收藏是最好的支持!