UNPKG

three-mesh-bvh

Version:

A BVH implementation to speed up raycasting against three.js meshes.

2,017 lines (1,352 loc) 245 kB
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;