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

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

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

相关推荐
Martin -Tang19 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发20 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端