three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
300 lines (204 loc) • 7.35 kB
JavaScript
/** @import { Raycaster, Intersection } from 'three' */
/** @import { GeometryBVH } from '../core/GeometryBVH.js' */
import { Mesh, Points, Line, LineLoop, LineSegments, Sphere, BatchedMesh, REVISION } from 'three';
import { MeshBVH } from '../core/MeshBVH.js';
const IS_REVISION_166 = parseInt( REVISION ) >= 166;
// TODO: how can we expand these raycast functions?
const _raycastFunctions = {
'Mesh': Mesh.prototype.raycast,
'Line': Line.prototype.raycast,
'LineSegments': LineSegments.prototype.raycast,
'LineLoop': LineLoop.prototype.raycast,
'Points': Points.prototype.raycast,
'BatchedMesh': BatchedMesh.prototype.raycast,
};
const _mesh = /* @__PURE__ */ new Mesh();
const _batchIntersects = [];
/**
* An accelerated raycast function with the same signature as `THREE.Mesh.raycast`. Uses the BVH
* for raycasting if it's available otherwise it falls back to the built-in approach. The results
* of the function are designed to be identical to the results of the conventional
* `THREE.Mesh.raycast` results.
*
* If the raycaster object being used has a property `firstHitOnly` set to `true`, then the
* raycasting will terminate as soon as it finds the closest intersection to the ray's origin and
* return only that intersection. This is typically several times faster than searching for all
* intersections.
*
* @section Extension Utilities
* @param {Raycaster} raycaster
* @param {Array<Intersection>} intersects
* @returns {void}
*/
export function acceleratedRaycast( raycaster, intersects ) {
if ( this.isBatchedMesh ) {
acceleratedBatchedMeshRaycast.call( this, raycaster, intersects );
} else {
const { geometry } = this;
if ( geometry.boundsTree ) {
geometry.boundsTree.raycastObject3D( this, raycaster, intersects );
} else {
let raycastFunction;
if ( this instanceof Mesh ) {
raycastFunction = _raycastFunctions.Mesh;
} else if ( this instanceof LineSegments ) {
raycastFunction = _raycastFunctions.LineSegments;
} else if ( this instanceof LineLoop ) {
raycastFunction = _raycastFunctions.LineLoop;
} else if ( this instanceof Line ) {
raycastFunction = _raycastFunctions.Line;
} else if ( this instanceof Points ) {
raycastFunction = _raycastFunctions.Points;
} else {
throw new Error( 'BVH: Fallback raycast function not found.' );
}
raycastFunction.call( this, raycaster, intersects );
}
}
}
function acceleratedBatchedMeshRaycast( raycaster, intersects ) {
if ( this.boundsTrees ) {
// TODO: remove use of geometry info, instance info when r170 is minimum version
const boundsTrees = this.boundsTrees;
const drawInfo = this._drawInfo || this._instanceInfo;
const drawRanges = this._drawRanges || this._geometryInfo;
const matrixWorld = this.matrixWorld;
_mesh.material = this.material;
_mesh.geometry = this.geometry;
const oldBoundsTree = _mesh.geometry.boundsTree;
const oldDrawRange = _mesh.geometry.drawRange;
if ( _mesh.geometry.boundingSphere === null ) {
_mesh.geometry.boundingSphere = new Sphere();
}
// TODO: provide new method to get instances count instead of 'drawInfo.length'
for ( let i = 0, l = drawInfo.length; i < l; i ++ ) {
if ( ! this.getVisibleAt( i ) ) {
continue;
}
// TODO: use getGeometryIndex
const geometryId = drawInfo[ i ].geometryIndex;
_mesh.geometry.boundsTree = boundsTrees[ geometryId ];
this.getMatrixAt( i, _mesh.matrixWorld ).premultiply( matrixWorld );
if ( ! _mesh.geometry.boundsTree ) {
this.getBoundingBoxAt( geometryId, _mesh.geometry.boundingBox );
this.getBoundingSphereAt( geometryId, _mesh.geometry.boundingSphere );
const drawRange = drawRanges[ geometryId ];
_mesh.geometry.setDrawRange( drawRange.start, drawRange.count );
}
_mesh.raycast( raycaster, _batchIntersects );
for ( let j = 0, l = _batchIntersects.length; j < l; j ++ ) {
const intersect = _batchIntersects[ j ];
intersect.object = this;
intersect.batchId = i;
intersects.push( intersect );
}
_batchIntersects.length = 0;
}
_mesh.geometry.boundsTree = oldBoundsTree;
_mesh.geometry.drawRange = oldDrawRange;
_mesh.material = null;
_mesh.geometry = null;
} else {
_raycastFunctions.BatchedMesh.call( this, raycaster, intersects );
}
}
/**
* A pre-made BufferGeometry extension function that builds a new BVH, assigns it to `boundsTree`
* for BufferGeometry, and applies the new index buffer to the geometry. Comparable to
* `computeBoundingBox` and `computeBoundingSphere`.
*
* ```js
* THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
* ```
*
* @section Extension Utilities
* @param {Object} [options]
* @returns {GeometryBVH}
*/
export function computeBoundsTree( options = {} ) {
const { type = MeshBVH } = options;
this.boundsTree = new type( this, options );
return this.boundsTree;
}
/**
* A BufferGeometry extension function that disposes of the BVH.
*
* ```js
* THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
* ```
*
* @section Extension Utilities
* @returns {void}
*/
export function disposeBoundsTree() {
this.boundsTree = null;
}
/**
* Equivalent of `computeBoundsTree` for `BatchedMesh`. Creates the
* `BatchedMesh.boundsTrees` array if it does not exist. If `index` is `-1`
* BVHs for all available geometries are generated and the full array is
* returned; otherwise only the BVH at that geometry index is generated and
* returned.
*
* ```js
* THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
* ```
*
* @section Extension Utilities
* @param {number} [index=-1]
* @param {Object} [options]
* @returns {GeometryBVH | Array<GeometryBVH> | null}
*/
export function computeBatchedBoundsTree( index = - 1, options = {} ) {
if ( ! IS_REVISION_166 ) {
throw new Error( 'BatchedMesh: Three r166+ is required to compute bounds trees.' );
}
options = {
...options,
range: null
};
const drawRanges = this._drawRanges || this._geometryInfo;
const geometryCount = this._geometryCount;
if ( ! this.boundsTrees ) {
this.boundsTrees = new Array( geometryCount ).fill( null );
}
const boundsTrees = this.boundsTrees;
while ( boundsTrees.length < geometryCount ) {
boundsTrees.push( null );
}
if ( index < 0 ) {
for ( let i = 0; i < geometryCount; i ++ ) {
options.range = drawRanges[ i ];
boundsTrees[ i ] = new MeshBVH( this.geometry, options );
}
return boundsTrees;
} else {
if ( index < drawRanges.length ) {
options.range = drawRanges[ index ];
boundsTrees[ index ] = new MeshBVH( this.geometry, options );
}
return boundsTrees[ index ] || null;
}
}
/**
* Equivalent of `disposeBoundsTree` for `BatchedMesh`. Sets entries in
* `BatchedMesh.boundsTrees` to `null`. If `index` is `-1` all BVHs are
* disposed; otherwise only the BVH at that geometry index is disposed.
*
* ```js
* THREE.BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
* ```
*
* @section Extension Utilities
* @param {number} [index=-1]
* @returns {void}
*/
export function disposeBatchedBoundsTree( index = - 1 ) {
if ( index < 0 ) {
this.boundsTrees.fill( null );
} else {
if ( index < this.boundsTrees.length ) {
this.boundsTrees[ index ] = null;
}
}
}