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 只需要一次,性能和体验上都很友好。

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

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

相关推荐
NiNg_1_2342 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河5 分钟前
CSS总结
前端·css
BigYe程普26 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H44 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink2 小时前
从底层看 path.resolve 实现
前端·node.js