three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
2,017 lines (1,352 loc) • 245 kB
JavaScript
import { Box3, Matrix4, BufferAttribute, Vector3, Vector2, Plane, Line3, Triangle, REVISION, BackSide, DoubleSide, Ray, FrontSide, BufferGeometry, Sphere, Mesh, Object3D, Group, LineBasicMaterial, MeshBasicMaterial, Line, LineSegments, LineLoop, Points, BatchedMesh, RGBAFormat, RGFormat, RedFormat, RGBAIntegerFormat, RGIntegerFormat, RedIntegerFormat, DataTexture, NearestFilter, IntType, UnsignedIntType, FloatType, UnsignedByteType, UnsignedShortType, ByteType, ShortType, Vector4, Matrix3 } from 'three';
import { BVH as BVH$1, NOT_INTERSECTED as NOT_INTERSECTED$1, INTERSECTED as INTERSECTED$1, GeometryBVH as GeometryBVH$1, SKIP_GENERATION as SKIP_GENERATION$1, ExtendedTriangle as ExtendedTriangle$1 } from 'three-mesh-bvh';
// Split strategy constants
const CENTER = 0;
const AVERAGE = 1;
const SAH = 2;
// Traversal constants
const NOT_INTERSECTED = 0;
const INTERSECTED = 1;
const CONTAINED = 2;
// SAH cost constants
// TODO: hone these costs more. The relative difference between them should be the
// difference in measured time to perform a primitive intersection vs traversing
// bounds.
// TODO: could be tuned per primitive type (triangles vs lines vs points)
const PRIMITIVE_INTERSECT_COST = 1.25;
const TRAVERSAL_COST = 1;
// Build constants
const BYTES_PER_NODE = 6 * 4 + 4 + 4;
const UINT32_PER_NODE = BYTES_PER_NODE / 4;
const IS_LEAFNODE_FLAG = 0xFFFF;
// Bit masks for 32 bit node data
const LEAFNODE_MASK_32 = IS_LEAFNODE_FLAG << 16;
// EPSILON for computing floating point error during build
// https://en.wikipedia.org/wiki/Machine_epsilon#Values_for_standard_hardware_floating_point_arithmetics
const FLOAT32_EPSILON = Math.pow( 2, - 24 );
const SKIP_GENERATION = Symbol( 'SKIP_GENERATION' );
const DEFAULT_OPTIONS = {
strategy: CENTER,
maxDepth: 40,
maxLeafSize: 10,
useSharedArrayBuffer: false,
setBoundingBox: true,
onProgress: null,
indirect: false,
verbose: true,
range: null,
[ SKIP_GENERATION ]: false,
};
function arrayToBox( nodeIndex32, array, target ) {
target.min.x = array[ nodeIndex32 ];
target.min.y = array[ nodeIndex32 + 1 ];
target.min.z = array[ nodeIndex32 + 2 ];
target.max.x = array[ nodeIndex32 + 3 ];
target.max.y = array[ nodeIndex32 + 4 ];
target.max.z = array[ nodeIndex32 + 5 ];
return target;
}
function makeEmptyBounds( target ) {
target[ 0 ] = target[ 1 ] = target[ 2 ] = Infinity;
target[ 3 ] = target[ 4 ] = target[ 5 ] = - Infinity;
}
function getLongestEdgeIndex( bounds ) {
let splitDimIdx = - 1;
let splitDist = - Infinity;
for ( let i = 0; i < 3; i ++ ) {
const dist = bounds[ i + 3 ] - bounds[ i ];
if ( dist > splitDist ) {
splitDist = dist;
splitDimIdx = i;
}
}
return splitDimIdx;
}
// copies bounds a into bounds b
function copyBounds( source, target ) {
target.set( source );
}
// sets bounds target to the union of bounds a and b
function unionBounds( a, b, target ) {
let aVal, bVal;
for ( let d = 0; d < 3; d ++ ) {
const d3 = d + 3;
// set the minimum values
aVal = a[ d ];
bVal = b[ d ];
target[ d ] = aVal < bVal ? aVal : bVal;
// set the max values
aVal = a[ d3 ];
bVal = b[ d3 ];
target[ d3 ] = aVal > bVal ? aVal : bVal;
}
}
// expands the given bounds by the provided primitive bounds
function expandByPrimitiveBounds( startIndex, primitiveBounds, bounds ) {
for ( let d = 0; d < 3; d ++ ) {
const tCenter = primitiveBounds[ startIndex + 2 * d ];
const tHalf = primitiveBounds[ startIndex + 2 * d + 1 ];
const tMin = tCenter - tHalf;
const tMax = tCenter + tHalf;
if ( tMin < bounds[ d ] ) {
bounds[ d ] = tMin;
}
if ( tMax > bounds[ d + 3 ] ) {
bounds[ d + 3 ] = tMax;
}
}
}
// compute bounds surface area
function computeSurfaceArea( bounds ) {
const d0 = bounds[ 3 ] - bounds[ 0 ];
const d1 = bounds[ 4 ] - bounds[ 1 ];
const d2 = bounds[ 5 ] - bounds[ 2 ];
return 2 * ( d0 * d1 + d1 * d2 + d2 * d0 );
}
function IS_LEAF( n16, uint16Array ) {
return uint16Array[ n16 + 15 ] === IS_LEAFNODE_FLAG;
}
function OFFSET( n32, uint32Array ) {
return uint32Array[ n32 + 6 ];
}
function COUNT( n16, uint16Array ) {
return uint16Array[ n16 + 14 ];
}
// Returns the uint32-aligned offset of the left child node for performance
function LEFT_NODE( n32 ) {
return n32 + UINT32_PER_NODE;
}
// Returns the uint32-aligned offset of the right child node for performance
function RIGHT_NODE( n32, uint32Array ) {
// stored value is relative offset from parent, convert to absolute uint32 index
const relativeOffset = uint32Array[ n32 + 6 ];
return n32 + relativeOffset * UINT32_PER_NODE;
}
function SPLIT_AXIS( n32, uint32Array ) {
return uint32Array[ n32 + 7 ];
}
function BOUNDING_DATA_INDEX( n32 ) {
return n32;
}
// computes the union of the bounds of all of the given primitives and puts the resulting box in "target".
// A bounding box is computed for the centroids of the primitives, as well, and placed in "centroidTarget".
// These are computed together to avoid redundant accesses to bounds array.
function getBounds( primitiveBounds, offset, count, target, centroidTarget ) {
let minx = Infinity;
let miny = Infinity;
let minz = Infinity;
let maxx = - Infinity;
let maxy = - Infinity;
let maxz = - Infinity;
let cminx = Infinity;
let cminy = Infinity;
let cminz = Infinity;
let cmaxx = - Infinity;
let cmaxy = - Infinity;
let cmaxz = - Infinity;
const boundsOffset = primitiveBounds.offset || 0;
for ( let i = ( offset - boundsOffset ) * 6, end = ( offset + count - boundsOffset ) * 6; i < end; i += 6 ) {
const cx = primitiveBounds[ i + 0 ];
const hx = primitiveBounds[ i + 1 ];
const lx = cx - hx;
const rx = cx + hx;
if ( lx < minx ) minx = lx;
if ( rx > maxx ) maxx = rx;
if ( cx < cminx ) cminx = cx;
if ( cx > cmaxx ) cmaxx = cx;
const cy = primitiveBounds[ i + 2 ];
const hy = primitiveBounds[ i + 3 ];
const ly = cy - hy;
const ry = cy + hy;
if ( ly < miny ) miny = ly;
if ( ry > maxy ) maxy = ry;
if ( cy < cminy ) cminy = cy;
if ( cy > cmaxy ) cmaxy = cy;
const cz = primitiveBounds[ i + 4 ];
const hz = primitiveBounds[ i + 5 ];
const lz = cz - hz;
const rz = cz + hz;
if ( lz < minz ) minz = lz;
if ( rz > maxz ) maxz = rz;
if ( cz < cminz ) cminz = cz;
if ( cz > cmaxz ) cmaxz = cz;
}
target[ 0 ] = minx;
target[ 1 ] = miny;
target[ 2 ] = minz;
target[ 3 ] = maxx;
target[ 4 ] = maxy;
target[ 5 ] = maxz;
centroidTarget[ 0 ] = cminx;
centroidTarget[ 1 ] = cminy;
centroidTarget[ 2 ] = cminz;
centroidTarget[ 3 ] = cmaxx;
centroidTarget[ 4 ] = cmaxy;
centroidTarget[ 5 ] = cmaxz;
}
const BIN_COUNT = 32;
const binsSort = ( a, b ) => a.candidate - b.candidate;
const sahBins = /* @__PURE__ */ new Array( BIN_COUNT ).fill().map( () => {
return {
count: 0,
bounds: new Float32Array( 6 ),
rightCacheBounds: new Float32Array( 6 ),
leftCacheBounds: new Float32Array( 6 ),
candidate: 0,
};
} );
const leftBounds = /* @__PURE__ */ new Float32Array( 6 );
function getOptimalSplit( nodeBoundingData, centroidBoundingData, primitiveBounds, offset, count, strategy ) {
let axis = - 1;
let pos = 0;
// Center
if ( strategy === CENTER ) {
axis = getLongestEdgeIndex( centroidBoundingData );
if ( axis !== - 1 ) {
pos = ( centroidBoundingData[ axis ] + centroidBoundingData[ axis + 3 ] ) / 2;
}
} else if ( strategy === AVERAGE ) {
axis = getLongestEdgeIndex( nodeBoundingData );
if ( axis !== - 1 ) {
pos = getAverage( primitiveBounds, offset, count, axis );
}
} else if ( strategy === SAH ) {
const rootSurfaceArea = computeSurfaceArea( nodeBoundingData );
let bestCost = PRIMITIVE_INTERSECT_COST * count;
// iterate over all axes
const boundsOffset = primitiveBounds.offset || 0;
const cStart = ( offset - boundsOffset ) * 6;
const cEnd = ( offset + count - boundsOffset ) * 6;
for ( let a = 0; a < 3; a ++ ) {
const axisLeft = centroidBoundingData[ a ];
const axisRight = centroidBoundingData[ a + 3 ];
const axisLength = axisRight - axisLeft;
const binWidth = axisLength / BIN_COUNT;
// If we have fewer primitives than we're planning to split then just check all
// the primitive positions because it will be faster.
if ( count < BIN_COUNT / 4 ) {
// initialize the bin candidates
const truncatedBins = [ ...sahBins ];
truncatedBins.length = count;
// set the candidates
let b = 0;
for ( let c = cStart; c < cEnd; c += 6, b ++ ) {
const bin = truncatedBins[ b ];
bin.candidate = primitiveBounds[ c + 2 * a ];
bin.count = 0;
const {
bounds,
leftCacheBounds,
rightCacheBounds,
} = bin;
for ( let d = 0; d < 3; d ++ ) {
rightCacheBounds[ d ] = Infinity;
rightCacheBounds[ d + 3 ] = - Infinity;
leftCacheBounds[ d ] = Infinity;
leftCacheBounds[ d + 3 ] = - Infinity;
bounds[ d ] = Infinity;
bounds[ d + 3 ] = - Infinity;
}
expandByPrimitiveBounds( c, primitiveBounds, bounds );
}
truncatedBins.sort( binsSort );
// remove redundant splits
let splitCount = count;
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
while ( bi + 1 < splitCount && truncatedBins[ bi + 1 ].candidate === bin.candidate ) {
truncatedBins.splice( bi + 1, 1 );
splitCount --;
}
}
// find the appropriate bin for each primitive and expand the bounds.
for ( let c = cStart; c < cEnd; c += 6 ) {
const center = primitiveBounds[ c + 2 * a ];
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
if ( center >= bin.candidate ) {
expandByPrimitiveBounds( c, primitiveBounds, bin.rightCacheBounds );
} else {
expandByPrimitiveBounds( c, primitiveBounds, bin.leftCacheBounds );
bin.count ++;
}
}
}
// expand all the bounds
for ( let bi = 0; bi < splitCount; bi ++ ) {
const bin = truncatedBins[ bi ];
const leftCount = bin.count;
const rightCount = count - bin.count;
// check the cost of this split
const leftBounds = bin.leftCacheBounds;
const rightBounds = bin.rightCacheBounds;
let leftProb = 0;
if ( leftCount !== 0 ) {
leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea;
}
let rightProb = 0;
if ( rightCount !== 0 ) {
rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea;
}
const cost = TRAVERSAL_COST + PRIMITIVE_INTERSECT_COST * (
leftProb * leftCount + rightProb * rightCount
);
if ( cost < bestCost ) {
axis = a;
bestCost = cost;
pos = bin.candidate;
}
}
} else {
// reset the bins
for ( let i = 0; i < BIN_COUNT; i ++ ) {
const bin = sahBins[ i ];
bin.count = 0;
bin.candidate = axisLeft + binWidth + i * binWidth;
const bounds = bin.bounds;
for ( let d = 0; d < 3; d ++ ) {
bounds[ d ] = Infinity;
bounds[ d + 3 ] = - Infinity;
}
}
// iterate over all center positions
for ( let c = cStart; c < cEnd; c += 6 ) {
const triCenter = primitiveBounds[ c + 2 * a ];
const relativeCenter = triCenter - axisLeft;
// in the partition function if the centroid lies on the split plane then it is
// considered to be on the right side of the split
let binIndex = ~ ~ ( relativeCenter / binWidth );
if ( binIndex >= BIN_COUNT ) binIndex = BIN_COUNT - 1;
const bin = sahBins[ binIndex ];
bin.count ++;
expandByPrimitiveBounds( c, primitiveBounds, bin.bounds );
}
// cache the unioned bounds from right to left so we don't have to regenerate them each time
const lastBin = sahBins[ BIN_COUNT - 1 ];
copyBounds( lastBin.bounds, lastBin.rightCacheBounds );
for ( let i = BIN_COUNT - 2; i >= 0; i -- ) {
const bin = sahBins[ i ];
const nextBin = sahBins[ i + 1 ];
unionBounds( bin.bounds, nextBin.rightCacheBounds, bin.rightCacheBounds );
}
let leftCount = 0;
for ( let i = 0; i < BIN_COUNT - 1; i ++ ) {
const bin = sahBins[ i ];
const binCount = bin.count;
const bounds = bin.bounds;
const nextBin = sahBins[ i + 1 ];
const rightBounds = nextBin.rightCacheBounds;
// don't do anything with the bounds if the new bounds have no primitives
if ( binCount !== 0 ) {
if ( leftCount === 0 ) {
copyBounds( bounds, leftBounds );
} else {
unionBounds( bounds, leftBounds, leftBounds );
}
}
leftCount += binCount;
// check the cost of this split
let leftProb = 0;
let rightProb = 0;
if ( leftCount !== 0 ) {
leftProb = computeSurfaceArea( leftBounds ) / rootSurfaceArea;
}
const rightCount = count - leftCount;
if ( rightCount !== 0 ) {
rightProb = computeSurfaceArea( rightBounds ) / rootSurfaceArea;
}
const cost = TRAVERSAL_COST + PRIMITIVE_INTERSECT_COST * (
leftProb * leftCount + rightProb * rightCount
);
if ( cost < bestCost ) {
axis = a;
bestCost = cost;
pos = bin.candidate;
}
}
}
}
} else {
console.warn( `BVH: Invalid build strategy value ${ strategy } used.` );
}
return { axis, pos };
}
// returns the average coordinate on the specified axis of all the provided primitives
function getAverage( primitiveBounds, offset, count, axis ) {
let avg = 0;
const boundsOffset = primitiveBounds.offset;
for ( let i = offset, end = offset + count; i < end; i ++ ) {
avg += primitiveBounds[ ( i - boundsOffset ) * 6 + axis * 2 ];
}
return avg / count;
}
class BVHNode {
constructor() {
// internal nodes have boundingData, left, right, and splitAxis
// leaf nodes have offset and count (referring to primitives in the mesh geometry)
this.boundingData = new Float32Array( 6 );
}
}
// reorders the partition buffer such that for `count` elements after `offset`, elements on the left side of the split
// will be on the left and elements on the right side of the split will be on the right. returns the index
// of the first element on the right side, or offset + count if there are no elements on the right side.
function partition( buffer, stride, primitiveBounds, offset, count, split ) {
let left = offset;
let right = offset + count - 1;
const pos = split.pos;
const axisOffset = split.axis * 2;
const boundsOffset = primitiveBounds.offset || 0;
// hoare partitioning, see e.g. https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme
while ( true ) {
while ( left <= right && primitiveBounds[ ( left - boundsOffset ) * 6 + axisOffset ] < pos ) {
left ++;
}
// if a primitive center lies on the partition plane it is considered to be on the right side
while ( left <= right && primitiveBounds[ ( right - boundsOffset ) * 6 + axisOffset ] >= pos ) {
right --;
}
if ( left < right ) {
// we need to swap all of the information associated with the primitives at index
// left and right; that's the elements in the partition buffer and the bounds
for ( let i = 0; i < stride; i ++ ) {
let t0 = buffer[ left * stride + i ];
buffer[ left * stride + i ] = buffer[ right * stride + i ];
buffer[ right * stride + i ] = t0;
}
// swap bounds
for ( let i = 0; i < 6; i ++ ) {
const l = left - boundsOffset;
const r = right - boundsOffset;
const tb = primitiveBounds[ l * 6 + i ];
primitiveBounds[ l * 6 + i ] = primitiveBounds[ r * 6 + i ];
primitiveBounds[ r * 6 + i ] = tb;
}
left ++;
right --;
} else {
return left;
}
}
}
let float32Array, uint32Array, uint16Array, uint8Array;
const MAX_POINTER = Math.pow( 2, 32 );
function countNodes( node ) {
if ( 'count' in node ) {
return 1;
} else {
return 1 + countNodes( node.left ) + countNodes( node.right );
}
}
function populateBuffer( byteOffset, node, buffer ) {
float32Array = new Float32Array( buffer );
uint32Array = new Uint32Array( buffer );
uint16Array = new Uint16Array( buffer );
uint8Array = new Uint8Array( buffer );
return _populateBuffer( byteOffset, node );
}
// pack structure
// boundingData : 6 float32
// right / offset : 1 uint32
// splitAxis / isLeaf + count : 1 uint32 / 2 uint16
function _populateBuffer( byteOffset, node ) {
const node32Index = byteOffset / 4;
const node16Index = byteOffset / 2;
const isLeaf = 'count' in node;
const boundingData = node.boundingData;
for ( let i = 0; i < 6; i ++ ) {
float32Array[ node32Index + i ] = boundingData[ i ];
}
if ( isLeaf ) {
if ( node.buffer ) {
uint8Array.set( new Uint8Array( node.buffer ), byteOffset );
return byteOffset + node.buffer.byteLength;
} else {
uint32Array[ node32Index + 6 ] = node.offset;
uint16Array[ node16Index + 14 ] = node.count;
uint16Array[ node16Index + 15 ] = IS_LEAFNODE_FLAG;
return byteOffset + BYTES_PER_NODE;
}
} else {
const { left, right, splitAxis } = node;
// fill in the left node contents
const leftByteOffset = byteOffset + BYTES_PER_NODE;
let rightByteOffset = _populateBuffer( leftByteOffset, left );
// calculate relative offset from parent to right child
const currentNodeIndex = byteOffset / BYTES_PER_NODE;
const rightNodeIndex = rightByteOffset / BYTES_PER_NODE;
const relativeRightIndex = rightNodeIndex - currentNodeIndex;
// check if the relative offset is too high
if ( relativeRightIndex > MAX_POINTER ) {
throw new Error( 'MeshBVH: Cannot store relative child node offset greater than 32 bits.' );
}
// fill in the right node contents (store as relative offset)
uint32Array[ node32Index + 6 ] = relativeRightIndex;
uint32Array[ node32Index + 7 ] = splitAxis;
// return the next available buffer pointer
return _populateBuffer( rightByteOffset, right );
}
}
function buildTree( bvh, primitiveBounds, offset, count, options, loadRange ) {
// expand variables
const {
maxDepth,
verbose,
maxLeafSize,
strategy,
onProgress,
} = options;
const partitionBuffer = bvh.primitiveBuffer;
const partitionStride = bvh.primitiveBufferStride;
// generate intermediate variables
const cacheCentroidBoundingData = new Float32Array( 6 );
let reachedMaxDepth = false;
const root = new BVHNode();
getBounds( primitiveBounds, offset, count, root.boundingData, cacheCentroidBoundingData );
splitNode( root, offset, count, cacheCentroidBoundingData );
return root;
function triggerProgress( primitivesProcessed ) {
if ( onProgress ) {
onProgress( ( primitivesProcessed - loadRange.offset ) / loadRange.count );
}
}
// 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 primitives 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( `BVH: Max depth of ${ maxDepth } reached when generating BVH. Consider increasing maxDepth.` );
}
}
// early out if we've met our capacity
if ( count <= maxLeafSize || 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, primitiveBounds, offset, count, strategy );
if ( split.axis === - 1 ) {
triggerProgress( offset + count );
node.offset = offset;
node.count = count;
return node;
}
const splitOffset = partition( partitionBuffer, partitionStride, primitiveBounds, 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 BVHNode();
const lstart = offset;
const lcount = splitOffset - offset;
node.left = left;
getBounds( primitiveBounds, lstart, lcount, left.boundingData, cacheCentroidBoundingData );
splitNode( left, lstart, lcount, cacheCentroidBoundingData, depth + 1 );
// repeat for right
const right = new BVHNode();
const rstart = splitOffset;
const rcount = count - lcount;
node.right = right;
getBounds( primitiveBounds, rstart, rcount, right.boundingData, cacheCentroidBoundingData );
splitNode( right, rstart, rcount, cacheCentroidBoundingData, depth + 1 );
}
return node;
}
}
function buildPackedTree( bvh, options ) {
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
// get the range of buffer data to construct / arrange
const rootRanges = bvh.getRootRanges( options.range );
const firstRange = rootRanges[ 0 ];
const lastRange = rootRanges[ rootRanges.length - 1 ];
const fullRange = {
offset: firstRange.offset,
count: lastRange.offset + lastRange.count - firstRange.offset,
};
// construct the primitive bounds for sorting
const primitiveBounds = new Float32Array( 6 * fullRange.count );
primitiveBounds.offset = fullRange.offset;
bvh.computePrimitiveBounds( fullRange.offset, fullRange.count, primitiveBounds );
// Build BVH roots
bvh._roots = rootRanges.map( range => {
const root = buildTree( bvh, primitiveBounds, range.offset, range.count, options, fullRange );
const nodeCount = countNodes( root );
const buffer = new BufferConstructor( BYTES_PER_NODE * nodeCount );
populateBuffer( 0, root, buffer );
return buffer;
} );
}
class PrimitivePool {
constructor( getNewPrimitive ) {
this._getNewPrimitive = getNewPrimitive;
this._primitives = [];
}
getPrimitive() {
const primitives = this._primitives;
if ( primitives.length === 0 ) {
return this._getNewPrimitive();
} else {
return primitives.pop();
}
}
releasePrimitive( primitive ) {
this._primitives.push( primitive );
}
}
class _BufferStack {
constructor() {
this.float32Array = null;
this.uint16Array = null;
this.uint32Array = null;
const stack = [];
let prevBuffer = null;
this.setBuffer = buffer => {
if ( prevBuffer ) {
stack.push( prevBuffer );
}
prevBuffer = buffer;
this.float32Array = new Float32Array( buffer );
this.uint16Array = new Uint16Array( buffer );
this.uint32Array = new Uint32Array( buffer );
};
this.clearBuffer = () => {
prevBuffer = null;
this.float32Array = null;
this.uint16Array = null;
this.uint32Array = null;
if ( stack.length !== 0 ) {
this.setBuffer( stack.pop() );
}
};
}
}
const BufferStack = /* @__PURE__ */ new _BufferStack();
let _box1$1, _box2$1;
const boxStack = [];
const boxPool = /* @__PURE__ */ new PrimitivePool( () => new Box3() );
function shapecast( bvh, root, intersectsBounds, intersectsRange, boundsTraverseOrder, nodeOffset ) {
// setup
_box1$1 = boxPool.getPrimitive();
_box2$1 = boxPool.getPrimitive();
boxStack.push( _box1$1, _box2$1 );
BufferStack.setBuffer( bvh._roots[ root ] );
const result = shapecastTraverse( 0, bvh.geometry, intersectsBounds, intersectsRange, boundsTraverseOrder, nodeOffset );
// cleanup
BufferStack.clearBuffer();
boxPool.releasePrimitive( _box1$1 );
boxPool.releasePrimitive( _box2$1 );
boxStack.pop();
boxStack.pop();
const length = boxStack.length;
if ( length > 0 ) {
_box2$1 = boxStack[ length - 1 ];
_box1$1 = boxStack[ length - 2 ];
}
return result;
}
function shapecastTraverse(
nodeIndex32,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc = null,
nodeIndexOffset = 0, // offset for unique node identifier
depth = 0
) {
const { float32Array, uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
if ( isLeaf ) {
const offset = OFFSET( nodeIndex32, uint32Array );
const count = COUNT( nodeIndex16, uint16Array );
arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, _box1$1 );
return intersectsRangeFunc( offset, count, false, depth, nodeIndexOffset + nodeIndex32 / UINT32_PER_NODE, _box1$1 );
} else {
const left = LEFT_NODE( nodeIndex32 );
const right = RIGHT_NODE( nodeIndex32, uint32Array );
let c1 = left;
let c2 = right;
let score1, score2;
let box1, box2;
if ( nodeScoreFunc ) {
box1 = _box1$1;
box2 = _box2$1;
// bounding data is not offset
arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 );
arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 );
score1 = nodeScoreFunc( box1 );
score2 = nodeScoreFunc( box2 );
if ( score2 < score1 ) {
c1 = right;
c2 = left;
const temp = score1;
score1 = score2;
score2 = temp;
box1 = box2;
// box2 is always set before use below
}
}
// Check box 1 intersection
if ( ! box1 ) {
box1 = _box1$1;
arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 );
}
const isC1Leaf = IS_LEAF( c1 * 2, uint16Array );
const c1Intersection = intersectsBoundsFunc( box1, isC1Leaf, score1, depth + 1, nodeIndexOffset + c1 / UINT32_PER_NODE );
let c1StopTraversal;
if ( c1Intersection === CONTAINED ) {
const offset = getLeftOffset( c1 );
const end = getRightEndOffset( c1 );
const count = end - offset;
c1StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexOffset + c1 / UINT32_PER_NODE, box1 );
} else {
c1StopTraversal =
c1Intersection &&
shapecastTraverse(
c1,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc,
nodeIndexOffset,
depth + 1
);
}
if ( c1StopTraversal ) return true;
// Check box 2 intersection
// cached box2 will have been overwritten by previous traversal
box2 = _box2$1;
arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 );
const isC2Leaf = IS_LEAF( c2 * 2, uint16Array );
const c2Intersection = intersectsBoundsFunc( box2, isC2Leaf, score2, depth + 1, nodeIndexOffset + c2 / UINT32_PER_NODE );
let c2StopTraversal;
if ( c2Intersection === CONTAINED ) {
const offset = getLeftOffset( c2 );
const end = getRightEndOffset( c2 );
const count = end - offset;
c2StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexOffset + c2 / UINT32_PER_NODE, box2 );
} else {
c2StopTraversal =
c2Intersection &&
shapecastTraverse(
c2,
geometry,
intersectsBoundsFunc,
intersectsRangeFunc,
nodeScoreFunc,
nodeIndexOffset,
depth + 1
);
}
if ( c2StopTraversal ) return true;
return false;
// Define these inside the function so it has access to the local variables needed
// when converting to the buffer equivalents
function getLeftOffset( nodeIndex32 ) {
const { uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
// traverse until we find a leaf
while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) {
nodeIndex32 = LEFT_NODE( nodeIndex32 );
nodeIndex16 = nodeIndex32 * 2;
}
return OFFSET( nodeIndex32, uint32Array );
}
function getRightEndOffset( nodeIndex32 ) {
const { uint16Array, uint32Array } = BufferStack;
let nodeIndex16 = nodeIndex32 * 2;
// traverse until we find a leaf
while ( ! IS_LEAF( nodeIndex16, uint16Array ) ) {
// adjust offset to point to the right node
nodeIndex32 = RIGHT_NODE( nodeIndex32, uint32Array );
nodeIndex16 = nodeIndex32 * 2;
}
// return the end offset of the triangle range
return OFFSET( nodeIndex32, uint32Array ) + COUNT( nodeIndex16, uint16Array );
}
}
}
const _bufferStack1 = /* @__PURE__ */ new BufferStack.constructor();
const _bufferStack2 = /* @__PURE__ */ new BufferStack.constructor();
const _boxPool = /* @__PURE__ */ new PrimitivePool( () => new Box3() );
const _leftBox1 = /* @__PURE__ */ new Box3();
const _rightBox1 = /* @__PURE__ */ new Box3();
const _leftBox2 = /* @__PURE__ */ new Box3();
const _rightBox2 = /* @__PURE__ */ new Box3();
let _active = false;
function bvhcast( bvh, otherBvh, matrixToLocal, intersectsRanges ) {
if ( _active ) {
throw new Error( 'MeshBVH: Recursive calls to bvhcast not supported.' );
}
_active = true;
const roots = bvh._roots;
const otherRoots = otherBvh._roots;
let result;
let nodeOffset1 = 0;
let nodeOffset2 = 0;
const invMat = new Matrix4().copy( matrixToLocal ).invert();
// iterate over the first set of roots
for ( let i = 0, il = roots.length; i < il; i ++ ) {
_bufferStack1.setBuffer( roots[ i ] );
nodeOffset2 = 0;
// prep the initial root box
const localBox = _boxPool.getPrimitive();
arrayToBox( BOUNDING_DATA_INDEX( 0 ), _bufferStack1.float32Array, localBox );
localBox.applyMatrix4( invMat );
// iterate over the second set of roots
for ( let j = 0, jl = otherRoots.length; j < jl; j ++ ) {
_bufferStack2.setBuffer( otherRoots[ j ] );
result = _traverse(
0, 0, matrixToLocal, invMat, intersectsRanges,
nodeOffset1, nodeOffset2, 0, 0,
localBox,
);
_bufferStack2.clearBuffer();
nodeOffset2 += otherRoots[ j ].byteLength / BYTES_PER_NODE;
if ( result ) {
break;
}
}
// release stack info
_boxPool.releasePrimitive( localBox );
_bufferStack1.clearBuffer();
nodeOffset1 += roots[ i ].byteLength / BYTES_PER_NODE;
if ( result ) {
break;
}
}
_active = false;
return result;
}
function _traverse(
node1Index32,
node2Index32,
matrix2to1,
matrix1to2,
intersectsRangesFunc,
// offsets for ids
node1IndexOffset = 0,
node2IndexOffset = 0,
// tree depth
depth1 = 0,
depth2 = 0,
currBox = null,
reversed = false,
) {
// get the buffer stacks associated with the current indices
let bufferStack1, bufferStack2;
if ( reversed ) {
bufferStack1 = _bufferStack2;
bufferStack2 = _bufferStack1;
} else {
bufferStack1 = _bufferStack1;
bufferStack2 = _bufferStack2;
}
// get the local instances of the typed buffers
const
float32Array1 = bufferStack1.float32Array,
uint32Array1 = bufferStack1.uint32Array,
uint16Array1 = bufferStack1.uint16Array,
float32Array2 = bufferStack2.float32Array,
uint32Array2 = bufferStack2.uint32Array,
uint16Array2 = bufferStack2.uint16Array;
const node1Index16 = node1Index32 * 2;
const node2Index16 = node2Index32 * 2;
const isLeaf1 = IS_LEAF( node1Index16, uint16Array1 );
const isLeaf2 = IS_LEAF( node2Index16, uint16Array2 );
let result = false;
if ( isLeaf2 && isLeaf1 ) {
// if both bounds are leaf nodes then fire the callback if the boxes intersect
// Note the "nodeIndex" values are just intended to be used as unique identifiers in the tree and
// not used for accessing data
if ( reversed ) {
result = intersectsRangesFunc(
OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ),
OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ),
depth2, node2IndexOffset + node2Index32 / UINT32_PER_NODE,
depth1, node1IndexOffset + node1Index32 / UINT32_PER_NODE,
);
} else {
result = intersectsRangesFunc(
OFFSET( node1Index32, uint32Array1 ), COUNT( node1Index32 * 2, uint16Array1 ),
OFFSET( node2Index32, uint32Array2 ), COUNT( node2Index32 * 2, uint16Array2 ),
depth1, node1IndexOffset + node1Index32 / UINT32_PER_NODE,
depth2, node2IndexOffset + node2Index32 / UINT32_PER_NODE,
);
}
} else if ( isLeaf2 ) {
// SWAP
// If we've traversed to the leaf node on the other bvh then we need to swap over
// to traverse down the first one
// get the new box to use
const newBox = _boxPool.getPrimitive();
arrayToBox( BOUNDING_DATA_INDEX( node2Index32 ), float32Array2, newBox );
newBox.applyMatrix4( matrix2to1 );
// get the child bounds to check before traversal
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
node2Index32, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
node2Index32, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
} else {
// if neither are leaves then we should swap if one of the children does not
// intersect with the current bounds
// get the child bounds to check
const cl2 = LEFT_NODE( node2Index32 );
const cr2 = RIGHT_NODE( node2Index32, uint32Array2 );
arrayToBox( BOUNDING_DATA_INDEX( cl2 ), float32Array2, _leftBox2 );
arrayToBox( BOUNDING_DATA_INDEX( cr2 ), float32Array2, _rightBox2 );
const leftIntersects = currBox.intersectsBox( _leftBox2 );
const rightIntersects = currBox.intersectsBox( _rightBox2 );
if ( leftIntersects && rightIntersects ) {
// continue to traverse both children if they both intersect
result = _traverse(
node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexOffset, node2IndexOffset, depth1, depth2 + 1,
currBox, reversed,
) || _traverse(
node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexOffset, node2IndexOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else if ( leftIntersects ) {
if ( isLeaf1 ) {
// if the current box is a leaf then just continue
result = _traverse(
node1Index32, cl2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexOffset, node2IndexOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else {
// SWAP
// if only one box intersects then we have to swap to the other bvh to continue
const newBox = _boxPool.getPrimitive();
newBox.copy( _leftBox2 ).applyMatrix4( matrix2to1 );
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
cl2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
cl2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
}
} else if ( rightIntersects ) {
if ( isLeaf1 ) {
// if the current box is a leaf then just continue
result = _traverse(
node1Index32, cr2, matrix2to1, matrix1to2, intersectsRangesFunc,
node1IndexOffset, node2IndexOffset, depth1, depth2 + 1,
currBox, reversed,
);
} else {
// SWAP
// if only one box intersects then we have to swap to the other bvh to continue
const newBox = _boxPool.getPrimitive();
newBox.copy( _rightBox2 ).applyMatrix4( matrix2to1 );
const cl1 = LEFT_NODE( node1Index32 );
const cr1 = RIGHT_NODE( node1Index32, uint32Array1 );
arrayToBox( BOUNDING_DATA_INDEX( cl1 ), float32Array1, _leftBox1 );
arrayToBox( BOUNDING_DATA_INDEX( cr1 ), float32Array1, _rightBox1 );
// precompute the intersections otherwise the global boxes will be modified during traversal
const intersectCl1 = newBox.intersectsBox( _leftBox1 );
const intersectCr1 = newBox.intersectsBox( _rightBox1 );
result = (
intersectCl1 && _traverse(
cr2, cl1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
) || (
intersectCr1 && _traverse(
cr2, cr1, matrix1to2, matrix2to1, intersectsRangesFunc,
node2IndexOffset, node1IndexOffset, depth2, depth1 + 1,
newBox, ! reversed,
)
);
_boxPool.releasePrimitive( newBox );
}
}
}
return result;
}
const _tempBox = /* @__PURE__ */ new Box3();
const _tempBuffer = /* @__PURE__ */ new Float32Array( 6 );
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 = shapecast( 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 );
}
}
function isSharedArrayBufferSupported() {
return typeof SharedArrayBuffer !== 'undefined';
}
function convertToBufferType( array, BufferConstructor ) {
if ( array === null ) {
return array;
} else if ( array.buffer ) {
const buffer = array.buffer;
if ( buffer.constructor === BufferConstructor ) {
return array;
}
const ArrayConstructor = array.constructor;
const result = new ArrayConstructor( new BufferConstructor( buffer.byteLength ) );
result.set( array );
return result;
} else {
if ( array.constructor === BufferConstructor ) {
return array;
}
const result = new BufferConstructor( array.byteLength );
new Uint8Array( result ).set( new Uint8Array( array ) );
return result;
}
}
function getVertexCount( geo ) {
return geo.index ? geo.index.count : geo.attributes.position.count;
}
function getTriCount( geo ) {
return getVertexCount( geo ) / 3;
}
function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) {
if ( vertexCount > 65535 ) {
return new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
} else {
return new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
}
}
// ensures that an index is present on the geometry
function ensureIndex( geo, options ) {
if ( ! geo.index ) {
const vertexCount = geo.attributes.position.count;
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
const index = getIndexArray( vertexCount, BufferConstructor );
geo.setIndex( new BufferAttribute( index, 1 ) );
for ( let i = 0; i < vertexCount; i ++ ) {
index[ i ] = i;
}
}
}
// Computes the set of { offset, count } ranges which need independent BVH roots. Each
// region in the geometry index that belongs to a different set of material groups requires
// a separate BVH root, so that triangles indices belonging to one group never get swapped
// with triangle indices belongs to another group. For example, if the groups were like this:
//
// [-------------------------------------------------------------]
// |__________________|
// g0 = [0, 20] |______________________||_____________________|
// g1 = [16, 40] g2 = [41, 60]
//
// we would need four BVH roots: [0, 15], [16, 20], [21, 40], [41, 60].
function getFullPrimitiveRange( geo, range, stride ) {
const primitiveCount = getVertexCount( geo ) / stride;
const drawRange = range ? range : geo.drawRange;
const start = drawRange.start / stride;
const end = ( drawRange.start + drawRange.count ) / stride;
const offset = Math.max( 0, start );
const count = Math.min( primitiveCount, end ) - offset;
return {
offset: Math.floor( offset ),
count: Math.floor( count ),
};
}
function getPrimitiveGroupRanges( geo, stride ) {
return geo.groups.map( group => ( {
offset: group.start / stride,
count: group.count / stride,
} ));
}
// Function that extracts a set of mutually exclusive ranges representing the primitives being
// drawn as determined by the geometry groups, draw range, and user specified range
function getRootPrimitiveRanges( geo, range, stride ) {
const drawRange = getFullPrimitiveRange( geo, range, stride );
const primitiveRanges = getPrimitiveGroupRanges( geo, stride );
if ( ! primitiveRanges.length ) {
return [ drawRange ];
}
const ranges = [];
const drawRangeStart = drawRange.offset;
const drawRangeEnd = drawRange.offset + drawRange.count;
// Create events for group boundaries
const primitiveCount = getVertexCount( geo ) / stride;
const events = [];
for ( const group of primitiveRanges ) {
// Account for cases where group size is set to Infinity
const { offset, count } = group;
const groupStart = offset;
const groupCount = isFinite( count ) ? count : ( primitiveCount - offset );
const groupEnd = ( offset + groupCount );
// Only add events if the group intersects with the draw range
if ( groupStart < drawRangeEnd && groupEnd > drawRangeStart ) {
events.push( { pos: Math.max( drawRangeStart, groupStart ), isStart: true } );
events.push( { pos: Math.min( drawRangeEnd, groupEnd ), isStart: false } );
}
}
// Sort events by position, with 'end' events before 'start' events at the same position
events.sort( ( a, b ) => {
if ( a.pos !== b.pos ) {
return a.pos - b.pos;
} else {
return a.type === 'end' ? - 1 : 1;
}
} );
// sweep through events and create ranges where activeGroups > 0
let activeGroups = 0;
let lastPos = null;
for ( const event of events ) {
const newPos = event.pos;