UNPKG

three-mesh-bvh

Version:

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

506 lines (331 loc) 13.5 kB
import { Box3, Vector3, Matrix4 } from 'three'; import { CONTAINED } from './Constants.js'; import { OrientedBox } from '../math/OrientedBox.js'; import { ExtendedTriangle } from '../math/ExtendedTriangle.js'; import { intersectTris, intersectClosestTri } from '../utils/GeometryRayIntersectUtilities.js'; import { setTriangle } from '../utils/TriangleUtilities.js'; import { arrayToBox } from '../utils/ArrayBoxUtilities.js'; import { PrimitivePool } from '../utils/PrimitivePool.js'; import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF, BOUNDING_DATA_INDEX, SPLIT_AXIS } from './nodeBufferFunctions.js'; const boundingBox = new Box3(); const boxIntersection = new Vector3(); const xyzFields = [ 'x', 'y', 'z' ]; export function raycast( nodeIndex32, geometry, side, ray, intersects ) { let nodeIndex16 = nodeIndex32 * 2, float32Array = _float32Array, uint16Array = _uint16Array, uint32Array = _uint32Array; const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); if ( isLeaf ) { const offset = OFFSET( nodeIndex32, uint32Array ); const count = COUNT( nodeIndex16, uint16Array ); intersectTris( geometry, side, ray, offset, count, intersects ); } else { const leftIndex = LEFT_NODE( nodeIndex32 ); if ( intersectRay( leftIndex, float32Array, ray, boxIntersection ) ) { raycast( leftIndex, geometry, side, ray, intersects ); } const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array ); if ( intersectRay( rightIndex, float32Array, ray, boxIntersection ) ) { raycast( rightIndex, geometry, side, ray, intersects ); } } } export function raycastFirst( nodeIndex32, geometry, side, ray ) { let nodeIndex16 = nodeIndex32 * 2, float32Array = _float32Array, uint16Array = _uint16Array, uint32Array = _uint32Array; const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); if ( isLeaf ) { const offset = OFFSET( nodeIndex32, uint32Array ); const count = COUNT( nodeIndex16, uint16Array ); return intersectClosestTri( geometry, side, ray, offset, count ); } else { // consider the position of the split plane with respect to the oncoming ray; whichever direction // the ray is coming from, look for an intersection among that side of the tree first const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array ); const xyzAxis = xyzFields[ splitAxis ]; const rayDir = ray.direction[ xyzAxis ]; const leftToRight = rayDir >= 0; // c1 is the child to check first let c1, c2; if ( leftToRight ) { c1 = LEFT_NODE( nodeIndex32 ); c2 = RIGHT_NODE( nodeIndex32, uint32Array ); } else { c1 = RIGHT_NODE( nodeIndex32, uint32Array ); c2 = LEFT_NODE( nodeIndex32 ); } const c1Intersection = intersectRay( c1, float32Array, ray, boxIntersection ); const c1Result = c1Intersection ? raycastFirst( c1, geometry, side, ray ) : null; // if we got an intersection in the first node and it's closer than the second node's bounding // box, we don't need to consider the second node because it couldn't possibly be a better result if ( c1Result ) { // check if the point is within the second bounds // "point" is in the local frame of the bvh const point = c1Result.point[ xyzAxis ]; const isOutside = leftToRight ? point <= float32Array[ c2 + splitAxis ] : // min bounding data point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data if ( isOutside ) { return c1Result; } } // either there was no intersection in the first node, or there could still be a closer // intersection in the second, so check the second node and then take the better of the two const c2Intersection = intersectRay( c2, float32Array, ray, boxIntersection ); const c2Result = c2Intersection ? raycastFirst( c2, geometry, side, ray ) : null; if ( c1Result && c2Result ) { return c1Result.distance <= c2Result.distance ? c1Result : c2Result; } else { return c1Result || c2Result || null; } } } export const shapecast = ( function () { let _box1, _box2; const boxStack = []; const boxPool = new PrimitivePool( () => new Box3() ); return function shapecast( ...args ) { _box1 = boxPool.getPrimitive(); _box2 = boxPool.getPrimitive(); boxStack.push( _box1, _box2 ); const result = shapecastTraverse( ...args ); boxPool.releasePrimitive( _box1 ); boxPool.releasePrimitive( _box2 ); boxStack.pop(); boxStack.pop(); const length = boxStack.length; if ( length > 0 ) { _box2 = boxStack[ length - 1 ]; _box1 = boxStack[ length - 2 ]; } return result; }; function shapecastTraverse( nodeIndex32, geometry, intersectsBoundsFunc, intersectsRangeFunc, nodeScoreFunc = null, nodeIndexByteOffset = 0, // offset for unique node identifier depth = 0 ) { // Define these inside the function so it has access to the local variables needed // when converting to the buffer equivalents function getLeftOffset( nodeIndex32 ) { let nodeIndex16 = nodeIndex32 * 2, uint16Array = _uint16Array, uint32Array = _uint32Array; // 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 ) { let nodeIndex16 = nodeIndex32 * 2, uint16Array = _uint16Array, uint32Array = _uint32Array; // 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 ); } let nodeIndex16 = nodeIndex32 * 2, float32Array = _float32Array, uint16Array = _uint16Array, uint32Array = _uint32Array; 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 ); return intersectsRangeFunc( offset, count, false, depth, nodeIndexByteOffset + nodeIndex32, _box1 ); } 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; box2 = _box2; // 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; arrayToBox( BOUNDING_DATA_INDEX( c1 ), float32Array, box1 ); } const isC1Leaf = IS_LEAF( c1 * 2, uint16Array ); const c1Intersection = intersectsBoundsFunc( box1, isC1Leaf, score1, depth + 1, nodeIndexByteOffset + c1 ); let c1StopTraversal; if ( c1Intersection === CONTAINED ) { const offset = getLeftOffset( c1 ); const end = getRightEndOffset( c1 ); const count = end - offset; c1StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c1, box1 ); } else { c1StopTraversal = c1Intersection && shapecastTraverse( c1, geometry, intersectsBoundsFunc, intersectsRangeFunc, nodeScoreFunc, nodeIndexByteOffset, depth + 1 ); } if ( c1StopTraversal ) return true; // Check box 2 intersection // cached box2 will have been overwritten by previous traversal box2 = _box2; arrayToBox( BOUNDING_DATA_INDEX( c2 ), float32Array, box2 ); const isC2Leaf = IS_LEAF( c2 * 2, uint16Array ); const c2Intersection = intersectsBoundsFunc( box2, isC2Leaf, score2, depth + 1, nodeIndexByteOffset + c2 ); let c2StopTraversal; if ( c2Intersection === CONTAINED ) { const offset = getLeftOffset( c2 ); const end = getRightEndOffset( c2 ); const count = end - offset; c2StopTraversal = intersectsRangeFunc( offset, count, true, depth + 1, nodeIndexByteOffset + c2, box2 ); } else { c2StopTraversal = c2Intersection && shapecastTraverse( c2, geometry, intersectsBoundsFunc, intersectsRangeFunc, nodeScoreFunc, nodeIndexByteOffset, depth + 1 ); } if ( c2StopTraversal ) return true; return false; } } } )(); export const intersectsGeometry = ( function () { const triangle = new ExtendedTriangle(); const triangle2 = new ExtendedTriangle(); const invertedMat = new Matrix4(); const obb = new OrientedBox(); const obb2 = new OrientedBox(); return function intersectsGeometry( nodeIndex32, geometry, otherGeometry, geometryToBvh, cachedObb = null ) { let nodeIndex16 = nodeIndex32 * 2, float32Array = _float32Array, uint16Array = _uint16Array, uint32Array = _uint32Array; if ( cachedObb === null ) { if ( ! otherGeometry.boundingBox ) { otherGeometry.computeBoundingBox(); } obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); cachedObb = obb; } const isLeaf = IS_LEAF( nodeIndex16, uint16Array ); if ( isLeaf ) { const thisGeometry = geometry; const thisIndex = thisGeometry.index; const thisPos = thisGeometry.attributes.position; const index = otherGeometry.index; const pos = otherGeometry.attributes.position; const offset = OFFSET( nodeIndex32, uint32Array ); const count = COUNT( nodeIndex16, uint16Array ); // get the inverse of the geometry matrix so we can transform our triangles into the // geometry space we're trying to test. We assume there are fewer triangles being checked // here. invertedMat.copy( geometryToBvh ).invert(); if ( otherGeometry.boundsTree ) { arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2 ); obb2.matrix.copy( invertedMat ); obb2.needsUpdate = true; const res = otherGeometry.boundsTree.shapecast( { intersectsBounds: box => obb2.intersectsBox( box ), intersectsTriangle: tri => { tri.a.applyMatrix4( geometryToBvh ); tri.b.applyMatrix4( geometryToBvh ); tri.c.applyMatrix4( geometryToBvh ); tri.needsUpdate = true; for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) { // this triangle needs to be transformed into the current BVH coordinate frame setTriangle( triangle2, i, thisIndex, thisPos ); triangle2.needsUpdate = true; if ( tri.intersectsTriangle( triangle2 ) ) { return true; } } return false; } } ); return res; } else { for ( let i = offset * 3, l = ( count + offset * 3 ); i < l; i += 3 ) { // this triangle needs to be transformed into the current BVH coordinate frame setTriangle( triangle, i, thisIndex, thisPos ); triangle.a.applyMatrix4( invertedMat ); triangle.b.applyMatrix4( invertedMat ); triangle.c.applyMatrix4( invertedMat ); triangle.needsUpdate = true; for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) { setTriangle( triangle2, i2, index, pos ); triangle2.needsUpdate = true; if ( triangle.intersectsTriangle( triangle2 ) ) { return true; } } } } } else { const left = nodeIndex32 + 8; const right = uint32Array[ nodeIndex32 + 6 ]; arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox ); const leftIntersection = cachedObb.intersectsBox( boundingBox ) && intersectsGeometry( left, geometry, otherGeometry, geometryToBvh, cachedObb ); if ( leftIntersection ) return true; arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox ); const rightIntersection = cachedObb.intersectsBox( boundingBox ) && intersectsGeometry( right, geometry, otherGeometry, geometryToBvh, cachedObb ); if ( rightIntersection ) return true; return false; } }; } )(); function intersectRay( nodeIndex32, array, ray, target ) { arrayToBox( nodeIndex32, array, boundingBox ); return ray.intersectBox( boundingBox, target ); } const bufferStack = []; let _prevBuffer; let _float32Array; let _uint16Array; let _uint32Array; export function setBuffer( buffer ) { if ( _prevBuffer ) { bufferStack.push( _prevBuffer ); } _prevBuffer = buffer; _float32Array = new Float32Array( buffer ); _uint16Array = new Uint16Array( buffer ); _uint32Array = new Uint32Array( buffer ); } export function clearBuffer() { _prevBuffer = null; _float32Array = null; _uint16Array = null; _uint32Array = null; if ( bufferStack.length ) { setBuffer( bufferStack.pop() ); } }