three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
180 lines (122 loc) • 5.35 kB
JavaScript
import { ensureIndex, getFullGeometryRange, getRootIndexRanges, getTriCount, hasGroupGaps, } from './geometryUtils.js';
import { getBounds, computeTriangleBounds } from './computeBoundsUtils.js';
import { getOptimalSplit } from './splitUtils.js';
import { MeshBVHNode } from '../MeshBVHNode.js';
import { BYTES_PER_NODE } from '../Constants.js';
import { partition } from './sortUtils.generated.js';
import { partition_indirect } from './sortUtils_indirect.generated.js';
import { countNodes, populateBuffer } from './buildUtils.js';
export function generateIndirectBuffer( geometry, useSharedArrayBuffer ) {
const triCount = ( geometry.index ? geometry.index.count : geometry.attributes.position.count ) / 3;
const useUint32 = triCount > 2 ** 16;
const byteCount = useUint32 ? 4 : 2;
const buffer = useSharedArrayBuffer ? new SharedArrayBuffer( triCount * byteCount ) : new ArrayBuffer( triCount * byteCount );
const indirectBuffer = useUint32 ? new Uint32Array( buffer ) : new Uint16Array( buffer );
for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
indirectBuffer[ i ] = i;
}
return indirectBuffer;
}
export function buildTree( bvh, triangleBounds, offset, count, options ) {
// epxand variables
const {
maxDepth,
verbose,
maxLeafTris,
strategy,
onProgress,
indirect,
} = options;
const indirectBuffer = bvh._indirectBuffer;
const geometry = bvh.geometry;
const indexArray = geometry.index ? geometry.index.array : null;
const partionFunc = indirect ? partition_indirect : partition;
// generate intermediate variables
const totalTriangles = getTriCount( geometry );
const cacheCentroidBoundingData = new Float32Array( 6 );
let reachedMaxDepth = false;
const root = new MeshBVHNode();
getBounds( triangleBounds, offset, count, root.boundingData, cacheCentroidBoundingData );
splitNode( root, offset, count, cacheCentroidBoundingData );
return root;
function triggerProgress( trianglesProcessed ) {
if ( onProgress ) {
onProgress( trianglesProcessed / totalTriangles );
}
}
// either recursively splits the given node, creating left and right subtrees for it, or makes it a leaf node,
// recording the offset and count of its triangles and writing them into the reordered geometry index.
function splitNode( node, offset, count, centroidBoundingData = null, depth = 0 ) {
if ( ! reachedMaxDepth && depth >= maxDepth ) {
reachedMaxDepth = true;
if ( verbose ) {
console.warn( `MeshBVH: Max depth of ${ maxDepth } reached when generating BVH. Consider increasing maxDepth.` );
console.warn( geometry );
}
}
// early out if we've met our capacity
if ( count <= maxLeafTris || depth >= maxDepth ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
return node;
}
// Find where to split the volume
const split = getOptimalSplit( node.boundingData, centroidBoundingData, triangleBounds, offset, count, strategy );
if ( split.axis === - 1 ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
return node;
}
const splitOffset = partionFunc( indirectBuffer, indexArray, triangleBounds, offset, count, split );
// create the two new child nodes
if ( splitOffset === offset || splitOffset === offset + count ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
} else {
node.splitAxis = split.axis;
// create the left child and compute its bounding box
const left = new MeshBVHNode();
const lstart = offset;
const lcount = splitOffset - offset;
node.left = left;
getBounds( triangleBounds, lstart, lcount, left.boundingData, cacheCentroidBoundingData );
splitNode( left, lstart, lcount, cacheCentroidBoundingData, depth + 1 );
// repeat for right
const right = new MeshBVHNode();
const rstart = splitOffset;
const rcount = count - lcount;
node.right = right;
getBounds( triangleBounds, rstart, rcount, right.boundingData, cacheCentroidBoundingData );
splitNode( right, rstart, rcount, cacheCentroidBoundingData, depth + 1 );
}
return node;
}
}
export function buildPackedTree( bvh, options ) {
const geometry = bvh.geometry;
if ( options.indirect ) {
bvh._indirectBuffer = generateIndirectBuffer( geometry, options.useSharedArrayBuffer );
if ( hasGroupGaps( geometry, options.range ) && ! options.verbose ) {
console.warn(
'MeshBVH: Provided geometry contains groups or a range that do not fully span the vertex contents while using the "indirect" option. ' +
'BVH may incorrectly report intersections on unrendered portions of the geometry.'
);
}
}
if ( ! bvh._indirectBuffer ) {
ensureIndex( geometry, options );
}
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
const triangleBounds = computeTriangleBounds( geometry );
const geometryRanges = options.indirect ? getFullGeometryRange( geometry, options.range ) : getRootIndexRanges( geometry, options.range );
bvh._roots = geometryRanges.map( range => {
const root = buildTree( bvh, triangleBounds, range.offset, range.count, options );
const nodeCount = countNodes( root );
const buffer = new BufferConstructor( BYTES_PER_NODE * nodeCount );
populateBuffer( 0, root, buffer );
return buffer;
} );
}