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

前言

源码阅读系列鸽了一段时间了,这次接着往下,刨根问底地搞明白如何优雅地把创建的 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 例子 ,来看看它是如何使用的:

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

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

其中 boundsTreeMeshBVH 实例,前面已经分析过它的 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.jsObject3D 类来实现对应的接口,使用 bufferGeometry 来构建几何体,一颗 BVH 树drawCall 只需要一次,性能和体验上都很友好。

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

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

相关推荐
燃先生._.32 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235242 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v4 小时前
webpack最基础的配置
前端·webpack·node.js