three-mesh-bvh 源码阅读(3) 可视化BVH节点-MeshBVHHelpler

775 阅读5分钟

前言

源码阅读系列鸽了一段时间了,这次接着往下,刨根问底地搞明白如何优雅地把创建的 BVH节点数据 给可视化出来。

之前的两篇篇幅较长,过程复杂,本篇内容比较少,可以轻松地阅读😁。

这次的文章涉及到 继承实现 Object3D遍历(traverse) 接口,TypedArray 的数据循环遍历,如何构造 bufferData 来构建 线。还有如何通过继承 Group 类的方式来实现节点的管理和更新。

解析

继承关系

看下代码的位置:

Index.png

最后这个 MeshBVHHelper 就是用来可视化 BVH 节点的类,使用时作为单独的对象引入,。由于我写文章的时候,pull 了最新的代码,版本为   "version": "0.7.3",使用 MeshBVHHelper 来替代了原来名为 MeshBVHVisualizer 的类。这样做的一个重要原因是为了和 three.js 保持风格统一, three.js 的各种帮助器,比如 CameraHelper, LightHelper 都是这样的命名风格。这个库如此强大,未来如果作为 addon 添加到 three.js 正式发布里也是极有可能。

把关系图简单画一下:

Objects_Chain.png

然后把项目跑起来,继续使用之前的 SkinnedMesh.html 例子 ,来看看它是如何使用的:

Use.png

meshHelpergeometry 是运用过动画数据并合并的 geometry,这个在这个系列的第一篇有详细说过。

看代码:

Genrate.png

看下 meshHelper 的内部方法和 three.js 的命名保持一致,这里面重点是 update 函数,在节点数据更新后调用,节点可视化更新,这个在后面的篇幅里分析:

MeshHelper.png

显然,都继承自 Object3D 这个基础类,Group 提供了对多个 Object3D 的管理,因为 geometry 有分组的情况下,构建后会有多个根节点。 在上一篇里,最后的构建成的 BVH 节点树被扁平化写入到了一段 buffer 里,接下来为了可视化地构建出来是需要去遍历这段数据的,之前的文章里已经熟悉了buffer 的操作,接下来就看下注释的 traverse 函数。

MeshBVH 遍历:

// 传入的 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 更新

...
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 相关内容,看下 vertexBufferindexBuffer 。既然都看到这了,读者可以拿个笔在笔记本上画下立刻就明白了,提示下,三角面的顶点顺序是逆时针的。

VertexBuffer


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);
		...
	}
}
...

其中 boundsTreeMeshBVH 实例,前面已经分析过它的 traverse 函数,这里调用遍历函数,去把最新的顶点数据写入到几何体里,由于 geometry 都是索引过的,更新 buffer 操作需要在下一步和索引数据一起准备好了再写入。

Index Buffer

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,
	
		]);
	
	}
...
}

线模式:

DisplayLines.png

面模式:

DisplayEdges.png

update

至此,最新的 顶点数据索引数据 已经获取,接下来写入就行了。

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.jsObject3D 类来实现对应的接口,使用 bufferGeometry 来构建几何体,一颗 BVH 树drawCall 只需要一次,性能和体验上都很友好。

本篇篇幅较短,一个是内容比较少,一个是前面的文章有提到的部分就不再重复了,如果有不清楚的可以看下前面的系列文章:

写文章不易,点赞收藏是最好的支持!