three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
481 lines (307 loc) • 8.72 kB
JavaScript
import { LineBasicMaterial, BufferAttribute, Box3, Group, MeshBasicMaterial, Object3D, BufferGeometry, Mesh, Matrix4, Vector3 } from 'three';
import { arrayToBox } from '../utils/ArrayBoxUtilities.js';
import { MeshBVH } from '../core/MeshBVH.js';
const boundingBox = /* @__PURE__ */ new Box3();
const matrix = /* @__PURE__ */ new Matrix4();
const vec = /* @__PURE__ */ new Vector3();
class BVHRootHelper extends Object3D {
get isMesh() {
return ! this.displayEdges;
}
get isLineSegments() {
return this.displayEdges;
}
get isLine() {
return this.displayEdges;
}
getVertexPosition( ...args ) {
// implement this function so it works with Box3.setFromObject
return Mesh.prototype.getVertexPosition.call( this, ...args );
}
constructor( bvh, material, depth = 10, group = 0 ) {
super();
this.material = material;
this.geometry = new BufferGeometry();
this.name = 'BVHRootHelper';
this.depth = depth;
this.displayParents = false;
this.bvh = bvh;
this.displayEdges = true;
this._group = group;
}
raycast() {}
update() {
const boundsTree = this.bvh;
this.geometry.dispose();
this.visible = false;
if ( boundsTree ) {
this.geometry = this.getGeometry( boundsTree );
this.visible = true;
}
}
getGeometry( boundsTree ) {
const group = this._group;
// fill in the position buffer with the bounds corners
let positionArray = null;
if ( group !== - 1 ) {
positionArray = this.getBVHBoundPositions( boundsTree, group );
} else {
const positionArrays = boundsTree._roots.map( ( r, i ) => this.getBVHBoundPositions( boundsTree, i ) );
const total = positionArrays.reduce( ( v, arr ) => v + arr.length, 0 );
positionArray = new Float32Array( total );
let offset = 0;
positionArrays.forEach( arr => {
positionArray.set( arr, offset );
offset += arr.length;
} );
}
const indexArray = this.getBVHBoundIndices( positionArray );
// update the geometry
const geometry = new BufferGeometry();
geometry.setIndex( new BufferAttribute( indexArray, 1, false ) );
geometry.setAttribute( 'position', new BufferAttribute( positionArray, 3, false ) );
return geometry;
}
getBVHBoundIndices( positionArray ) {
const boundsCount = positionArray.length / ( 8 * 3 );
let indexArray;
let indices;
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,
] );
}
if ( positionArray.length > 65535 ) {
indexArray = new Uint32Array( indices.length * boundsCount );
} else {
indexArray = new Uint16Array( indices.length * boundsCount );
}
const indexLength = indices.length;
for ( let i = 0; i < boundsCount; i ++ ) {
const posOffset = i * 8;
const indexOffset = i * indexLength;
for ( let j = 0; j < indexLength; j ++ ) {
indexArray[ indexOffset + j ] = posOffset + indices[ j ];
}
}
return indexArray;
}
getBVHBoundPositions( bvh, group = 0, matrix = null ) {
// count the number of bounds required
const targetDepth = this.depth - 1;
const displayParents = this.displayParents;
let boundsCount = 0;
bvh.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 );
bvh.traverse( ( depth, isLeaf, boundingData ) => {
const terminate = depth >= targetDepth || isLeaf;
if ( terminate || displayParents ) {
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;
vec.set( xVal, yVal, zVal );
if ( matrix ) {
vec.applyMatrix4( matrix );
}
vec.toArray( positionArray, posIndex );
posIndex += 3;
}
}
}
return terminate;
}
}, group );
return positionArray;
}
}
export class BVHHelper extends Group {
get color() {
return this.edgeMaterial.color;
}
get opacity() {
return this.edgeMaterial.opacity;
}
set opacity( v ) {
this.edgeMaterial.opacity = v;
this.meshMaterial.opacity = v;
}
get objectIndex() {
console.warn( 'BVHHelper: "objectIndex" has been renamed "instanceId".' );
return this.instanceId;
}
set objectIndex( v ) {
console.warn( 'BVHHelper: "objectIndex" has been renamed "instanceId".' );
this.instanceId = v;
}
constructor( mesh = null, bvh = null, depth = 10 ) {
// handle bvh, depth signature
if ( mesh instanceof MeshBVH ) {
depth = bvh || 10;
bvh = mesh;
mesh = null;
}
// handle mesh, depth signature
if ( typeof bvh === 'number' ) {
depth = bvh;
bvh = null;
}
super();
this.name = 'BVHHelper';
this.depth = depth;
this.mesh = mesh;
this.bvh = bvh;
this.displayParents = false;
this.displayEdges = true;
this.instanceId = 0;
this._roots = [];
const edgeMaterial = new LineBasicMaterial( {
color: 0x00FF88,
transparent: true,
opacity: 0.3,
depthWrite: false,
} );
const meshMaterial = new MeshBasicMaterial( {
color: 0x00FF88,
transparent: true,
opacity: 0.3,
depthWrite: false,
} );
meshMaterial.color = edgeMaterial.color;
this.edgeMaterial = edgeMaterial;
this.meshMaterial = meshMaterial;
this.update();
}
update() {
const mesh = this.mesh;
const instanceId = this.instanceId;
let bvh = this.bvh || mesh.boundsTree || mesh.geometry && mesh.geometry.boundsTree || null;
if ( mesh && mesh.isBatchedMesh && mesh.boundsTrees && ! bvh && instanceId >= 0 ) {
// get the bvh from a batchedMesh if not provided
// TODO: we should have an official way to get the geometry index cleanly
const drawInfo = mesh._drawInfo[ instanceId ];
if ( drawInfo ) {
bvh = mesh.boundsTrees[ drawInfo.geometryIndex ] || bvh;
}
}
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 ) {
const root = new BVHRootHelper( 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;
const instanceId = this.instanceId;
if ( mesh !== null ) {
mesh.updateWorldMatrix( true, false );
if ( parent ) {
this.matrix
.copy( parent.matrixWorld )
.invert()
.multiply( mesh.matrixWorld );
} else {
this.matrix
.copy( mesh.matrixWorld );
}
// handle batched and instanced mesh bvhs
if ( ( mesh.isInstancedMesh || mesh.isBatchedMesh ) && instanceId >= 0 ) {
mesh.getMatrixAt( instanceId, matrix );
this.matrix.multiply( matrix );
}
this.matrix.decompose(
this.position,
this.quaternion,
this.scale,
);
}
super.updateMatrixWorld( ...args );
}
copy( source ) {
this.depth = source.depth;
this.mesh = source.mesh;
this.bvh = source.bvh;
this.opacity = source.opacity;
this.color.copy( source.color );
}
clone() {
return new BVHHelper().copy( this );
}
dispose() {
this.edgeMaterial.dispose();
this.meshMaterial.dispose();
const children = this.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
children[ i ].geometry.dispose();
}
}
}
export class MeshBVHHelper extends BVHHelper {
constructor( ...args ) {
console.warn( 'MeshBVHHelper: Class has been deprecated. Use BVHHelper instead.' );
super( ...args );
}
}