three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
360 lines (235 loc) • 9.03 kB
JavaScript
import { Box3 } from 'three';
import { BYTES_PER_NODE, UINT32_PER_NODE, DEFAULT_OPTIONS, FLOAT32_EPSILON } from './Constants.js';
import { arrayToBox } from '../utils/ArrayBoxUtilities.js';
import { IS_LEAF, LEFT_NODE, RIGHT_NODE, SPLIT_AXIS, COUNT, OFFSET } from './utils/nodeBufferUtils.js';
import { buildPackedTree } from './build/buildTree.js';
import { shapecast as shapecastFunc } from './cast/shapecast.js';
import { bvhcast } from './cast/bvhcast.js';
const _tempBox = /* @__PURE__ */ new Box3();
const _tempBuffer = /* @__PURE__ */ new Float32Array( 6 );
export class BVH {
constructor() {
this._roots = null;
this.primitiveBuffer = null;
this.primitiveBufferStride = null;
}
init( options ) {
options = {
...DEFAULT_OPTIONS,
...options,
};
buildPackedTree( this, options );
}
getRootRanges( /* range */ ) {
// TODO: can we avoid passing range in here?
throw new Error( 'BVH: getRootRanges() not implemented' );
}
// write the i-th primitive bounds in a 6-value min / max format to the buffer
// starting at the given "writeOffset"
writePrimitiveBounds( /* i, buffer, writeOffset */ ) {
throw new Error( 'BVH: writePrimitiveBounds() not implemented' );
}
// writes the union bounds of all primitives in the given range in a min / max format
// to the buffer
writePrimitiveRangeBounds( offset, count, targetBuffer, baseIndex ) {
// Initialize bounds
let minX = Infinity;
let minY = Infinity;
let minZ = Infinity;
let maxX = - Infinity;
let maxY = - Infinity;
let maxZ = - Infinity;
// compute union of all bounds
for ( let i = offset, end = offset + count; i < end; i ++ ) {
this.writePrimitiveBounds( i, _tempBuffer, 0 );
// compute union
const [ lx, ly, lz, rx, ry, rz ] = _tempBuffer;
if ( lx < minX ) minX = lx;
if ( rx > maxX ) maxX = rx;
if ( ly < minY ) minY = ly;
if ( ry > maxY ) maxY = ry;
if ( lz < minZ ) minZ = lz;
if ( rz > maxZ ) maxZ = rz;
}
// write bounds
targetBuffer[ baseIndex + 0 ] = minX;
targetBuffer[ baseIndex + 1 ] = minY;
targetBuffer[ baseIndex + 2 ] = minZ;
targetBuffer[ baseIndex + 3 ] = maxX;
targetBuffer[ baseIndex + 4 ] = maxY;
targetBuffer[ baseIndex + 5 ] = maxZ;
return targetBuffer;
}
computePrimitiveBounds( offset, count, targetBuffer ) {
const boundsOffset = targetBuffer.offset || 0;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
this.writePrimitiveBounds( i, _tempBuffer, 0 );
// construction primitive bounds requires a center + half extents format
const [ lx, ly, lz, rx, ry, rz ] = _tempBuffer;
const cx = ( lx + rx ) / 2;
const cy = ( ly + ry ) / 2;
const cz = ( lz + rz ) / 2;
const hx = ( rx - lx ) / 2;
const hy = ( ry - ly ) / 2;
const hz = ( rz - lz ) / 2;
const baseIndex = ( i - boundsOffset ) * 6;
targetBuffer[ baseIndex + 0 ] = cx;
targetBuffer[ baseIndex + 1 ] = hx + ( Math.abs( cx ) + hx ) * FLOAT32_EPSILON;
targetBuffer[ baseIndex + 2 ] = cy;
targetBuffer[ baseIndex + 3 ] = hy + ( Math.abs( cy ) + hy ) * FLOAT32_EPSILON;
targetBuffer[ baseIndex + 4 ] = cz;
targetBuffer[ baseIndex + 5 ] = hz + ( Math.abs( cz ) + hz ) * FLOAT32_EPSILON;
}
return targetBuffer;
}
shiftPrimitiveOffsets( offset ) {
const indirectBuffer = this._indirectBuffer;
if ( indirectBuffer ) {
// the offsets are embedded in the indirect buffer
for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
indirectBuffer[ i ] += offset;
}
} else {
// offsets are embedded in the leaf nodes
const roots = this._roots;
for ( let rootIndex = 0; rootIndex < roots.length; rootIndex ++ ) {
const root = roots[ rootIndex ];
const uint32Array = new Uint32Array( root );
const uint16Array = new Uint16Array( root );
const totalNodes = root.byteLength / BYTES_PER_NODE;
for ( let node = 0; node < totalNodes; node ++ ) {
const node32Index = UINT32_PER_NODE * node;
const node16Index = 2 * node32Index;
if ( IS_LEAF( node16Index, uint16Array ) ) {
// offset value
uint32Array[ node32Index + 6 ] += offset;
}
}
}
}
}
traverse( callback, rootIndex = 0 ) {
const buffer = this._roots[ rootIndex ];
const uint32Array = new Uint32Array( buffer );
const uint16Array = new Uint16Array( buffer );
_traverse( 0 );
function _traverse( node32Index, depth = 0 ) {
const node16Index = node32Index * 2;
const isLeaf = IS_LEAF( node16Index, uint16Array );
if ( isLeaf ) {
const offset = uint32Array[ node32Index + 6 ];
const count = uint16Array[ node16Index + 14 ];
callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), offset, count );
} else {
const left = LEFT_NODE( node32Index );
const right = RIGHT_NODE( node32Index, uint32Array );
const splitAxis = SPLIT_AXIS( node32Index, uint32Array );
const stopTraversal = callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), splitAxis );
if ( ! stopTraversal ) {
_traverse( left, depth + 1 );
_traverse( right, depth + 1 );
}
}
}
}
refit( /* nodeIndices = null */ ) {
// TODO: add support for "nodeIndices"
// if ( nodeIndices && Array.isArray( nodeIndices ) ) {
// nodeIndices = new Set( nodeIndices );
// }
const roots = this._roots;
for ( let rootIndex = 0, rootCount = roots.length; rootIndex < rootCount; rootIndex ++ ) {
const buffer = roots[ rootIndex ];
const uint32Array = new Uint32Array( buffer );
const uint16Array = new Uint16Array( buffer );
const float32Array = new Float32Array( buffer );
const totalNodes = buffer.byteLength / BYTES_PER_NODE;
// Traverse nodes from right to left so children are updated before parents
for ( let nodeIndex = totalNodes - 1; nodeIndex >= 0; nodeIndex -- ) {
const nodeIndex32 = nodeIndex * UINT32_PER_NODE;
const nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
// get the bounds
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
this.writePrimitiveRangeBounds( offset, count, _tempBuffer, 0 );
// write directly to node bounds (already in min/max format)
float32Array.set( _tempBuffer, nodeIndex32 );
} else {
const left = LEFT_NODE( nodeIndex32 );
const right = RIGHT_NODE( nodeIndex32, uint32Array );
// Union the bounds of left and right children
for ( let i = 0; i < 3; i ++ ) {
const leftMin = float32Array[ left + i ];
const leftMax = float32Array[ left + i + 3 ];
const rightMin = float32Array[ right + i ];
const rightMax = float32Array[ right + i + 3 ];
float32Array[ nodeIndex32 + i ] = leftMin < rightMin ? leftMin : rightMin;
float32Array[ nodeIndex32 + i + 3 ] = leftMax > rightMax ? leftMax : rightMax;
}
}
}
}
}
getBoundingBox( target ) {
target.makeEmpty();
const roots = this._roots;
roots.forEach( buffer => {
arrayToBox( 0, new Float32Array( buffer ), _tempBox );
target.union( _tempBox );
} );
return target;
}
// Base shapecast implementation that can be used by subclasses
// TODO: see if we can get rid of "iterateFunc" here as well as the primitive so the function
// API aligns with the "shapecast" implementation
shapecast( callbacks ) {
// TODO: can we get rid of "scratchPrimitive" and / or "iterate"? Or merge them somehow
let {
boundsTraverseOrder,
intersectsBounds,
intersectsRange,
intersectsPrimitive,
scratchPrimitive,
iterate,
} = callbacks;
// wrap the intersectsRange function
if ( intersectsRange && intersectsPrimitive ) {
const originalIntersectsRange = intersectsRange;
intersectsRange = ( offset, count, contained, depth, nodeIndex ) => {
if ( ! originalIntersectsRange( offset, count, contained, depth, nodeIndex ) ) {
return iterate( offset, count, this, intersectsPrimitive, contained, depth, scratchPrimitive );
}
return true;
};
} else if ( ! intersectsRange ) {
if ( intersectsPrimitive ) {
intersectsRange = ( offset, count, contained, depth ) => {
return iterate( offset, count, this, intersectsPrimitive, contained, depth, scratchPrimitive );
};
} else {
intersectsRange = ( offset, count, contained ) => {
return contained;
};
}
}
// run shapecast
let result = false;
let nodeOffset = 0;
const roots = this._roots;
for ( let i = 0, l = roots.length; i < l; i ++ ) {
const root = roots[ i ];
result = shapecastFunc( this, i, intersectsBounds, intersectsRange, boundsTraverseOrder, nodeOffset );
if ( result ) {
break;
}
nodeOffset += root.byteLength / BYTES_PER_NODE;
}
return result;
}
bvhcast( otherBvh, matrixToLocal, callbacks ) {
let { intersectsRanges } = callbacks;
return bvhcast( this, otherBvh, matrixToLocal, intersectsRanges );
}
}