UNPKG

three-mesh-bvh

Version:

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

1,190 lines (754 loc) 29.3 kB
import { Vector3, BufferAttribute, Box3, FrontSide, Matrix4 } from 'three'; import { CENTER, BYTES_PER_NODE, IS_LEAFNODE_FLAG } from './Constants.js'; import { buildPackedTree } from './buildFunctions.js'; import { raycast, raycastFirst, shapecast, intersectsGeometry, setBuffer, clearBuffer, } from './castFunctions.js'; import { OrientedBox } from '../math/OrientedBox.js'; import { ExtendedTriangle } from '../math/ExtendedTriangle.js'; import { PrimitivePool } from '../utils/PrimitivePool.js'; import { arrayToBox } from '../utils/ArrayBoxUtilities.js'; import { iterateOverTriangles, setTriangle } from '../utils/TriangleUtilities.js'; import { convertRaycastIntersect } from '../utils/GeometryRayIntersectUtilities.js'; const SKIP_GENERATION = Symbol( 'skip tree generation' ); const aabb = /* @__PURE__ */ new Box3(); const aabb2 = /* @__PURE__ */ new Box3(); const tempMatrix = /* @__PURE__ */ new Matrix4(); const obb = /* @__PURE__ */ new OrientedBox(); const obb2 = /* @__PURE__ */ new OrientedBox(); const temp = /* @__PURE__ */ new Vector3(); const temp1 = /* @__PURE__ */ new Vector3(); const temp2 = /* @__PURE__ */ new Vector3(); const temp3 = /* @__PURE__ */ new Vector3(); const temp4 = /* @__PURE__ */ new Vector3(); const tempBox = /* @__PURE__ */ new Box3(); const trianglePool = /* @__PURE__ */ new PrimitivePool( () => new ExtendedTriangle() ); export class MeshBVH { static serialize( bvh, options = {} ) { if ( options.isBufferGeometry ) { console.warn( 'MeshBVH.serialize: The arguments for the function have changed. See documentation for new signature.' ); return MeshBVH.serialize( arguments[ 0 ], { cloneBuffers: arguments[ 2 ] === undefined ? true : arguments[ 2 ], } ); } options = { cloneBuffers: true, ...options, }; const geometry = bvh.geometry; const rootData = bvh._roots; const indexAttribute = geometry.getIndex(); let result; if ( options.cloneBuffers ) { result = { roots: rootData.map( root => root.slice() ), index: indexAttribute.array.slice(), }; } else { result = { roots: rootData, index: indexAttribute.array, }; } return result; } static deserialize( data, geometry, options = {} ) { if ( typeof options === 'boolean' ) { console.warn( 'MeshBVH.deserialize: The arguments for the function have changed. See documentation for new signature.' ); return MeshBVH.deserialize( arguments[ 0 ], arguments[ 1 ], { setIndex: arguments[ 2 ] === undefined ? true : arguments[ 2 ], } ); } options = { setIndex: true, ...options, }; const { index, roots } = data; const bvh = new MeshBVH( geometry, { ...options, [ SKIP_GENERATION ]: true } ); bvh._roots = roots; if ( options.setIndex ) { const indexAttribute = geometry.getIndex(); if ( indexAttribute === null ) { const newIndex = new BufferAttribute( data.index, 1, false ); geometry.setIndex( newIndex ); } else if ( indexAttribute.array !== index ) { indexAttribute.array.set( index ); indexAttribute.needsUpdate = true; } } return bvh; } constructor( geometry, options = {} ) { if ( ! geometry.isBufferGeometry ) { throw new Error( 'MeshBVH: Only BufferGeometries are supported.' ); } else if ( geometry.index && geometry.index.isInterleavedBufferAttribute ) { throw new Error( 'MeshBVH: InterleavedBufferAttribute is not supported for the index attribute.' ); } // default options options = Object.assign( { strategy: CENTER, maxDepth: 40, maxLeafTris: 10, verbose: true, useSharedArrayBuffer: false, setBoundingBox: true, onProgress: null, // undocumented options // Whether to skip generating the tree. Used for deserialization. [ SKIP_GENERATION ]: false, }, options ); if ( options.useSharedArrayBuffer && typeof SharedArrayBuffer === 'undefined' ) { throw new Error( 'MeshBVH: SharedArrayBuffer is not available.' ); } this._roots = null; if ( ! options[ SKIP_GENERATION ] ) { this._roots = buildPackedTree( geometry, options ); if ( ! geometry.boundingBox && options.setBoundingBox ) { geometry.boundingBox = this.getBoundingBox( new Box3() ); } } // retain references to the geometry so we can use them it without having to // take a geometry reference in every function. this.geometry = geometry; } refit( nodeIndices = null ) { if ( nodeIndices && Array.isArray( nodeIndices ) ) { nodeIndices = new Set( nodeIndices ); } const geometry = this.geometry; const indexArr = geometry.index.array; const posAttr = geometry.attributes.position; const posArr = posAttr.array; // support for an interleaved position buffer const bufferOffset = posAttr.offset || 0; let stride = 3; if ( posAttr.isInterleavedBufferAttribute ) { stride = posAttr.data.stride; } let buffer, uint32Array, uint16Array, float32Array; let byteOffset = 0; const roots = this._roots; for ( let i = 0, l = roots.length; i < l; i ++ ) { buffer = roots[ i ]; uint32Array = new Uint32Array( buffer ); uint16Array = new Uint16Array( buffer ); float32Array = new Float32Array( buffer ); _traverse( 0, byteOffset ); byteOffset += buffer.byteLength; } function _traverse( node32Index, byteOffset, force = false ) { const node16Index = node32Index * 2; const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG; if ( isLeaf ) { const offset = uint32Array[ node32Index + 6 ]; const count = uint16Array[ node16Index + 14 ]; let minx = Infinity; let miny = Infinity; let minz = Infinity; let maxx = - Infinity; let maxy = - Infinity; let maxz = - Infinity; for ( let i = 3 * offset, l = 3 * ( offset + count ); i < l; i ++ ) { const index = indexArr[ i ] * stride + bufferOffset; const x = posArr[ index + 0 ]; const y = posArr[ index + 1 ]; const z = posArr[ index + 2 ]; if ( x < minx ) minx = x; if ( x > maxx ) maxx = x; if ( y < miny ) miny = y; if ( y > maxy ) maxy = y; if ( z < minz ) minz = z; if ( z > maxz ) maxz = z; } if ( float32Array[ node32Index + 0 ] !== minx || float32Array[ node32Index + 1 ] !== miny || float32Array[ node32Index + 2 ] !== minz || float32Array[ node32Index + 3 ] !== maxx || float32Array[ node32Index + 4 ] !== maxy || float32Array[ node32Index + 5 ] !== maxz ) { float32Array[ node32Index + 0 ] = minx; float32Array[ node32Index + 1 ] = miny; float32Array[ node32Index + 2 ] = minz; float32Array[ node32Index + 3 ] = maxx; float32Array[ node32Index + 4 ] = maxy; float32Array[ node32Index + 5 ] = maxz; return true; } else { return false; } } else { const left = node32Index + 8; const right = uint32Array[ node32Index + 6 ]; // the identifying node indices provided by the shapecast function include offsets of all // root buffers to guarantee they're unique between roots so offset left and right indices here. const offsetLeft = left + byteOffset; const offsetRight = right + byteOffset; let forceChildren = force; let includesLeft = false; let includesRight = false; if ( nodeIndices ) { // if we see that neither the left or right child are included in the set that need to be updated // then we assume that all children need to be updated. if ( ! forceChildren ) { includesLeft = nodeIndices.has( offsetLeft ); includesRight = nodeIndices.has( offsetRight ); forceChildren = ! includesLeft && ! includesRight; } } else { includesLeft = true; includesRight = true; } const traverseLeft = forceChildren || includesLeft; const traverseRight = forceChildren || includesRight; let leftChange = false; if ( traverseLeft ) { leftChange = _traverse( left, byteOffset, forceChildren ); } let rightChange = false; if ( traverseRight ) { rightChange = _traverse( right, byteOffset, forceChildren ); } const didChange = leftChange || rightChange; if ( didChange ) { for ( let i = 0; i < 3; i ++ ) { const lefti = left + i; const righti = right + i; const minLeftValue = float32Array[ lefti ]; const maxLeftValue = float32Array[ lefti + 3 ]; const minRightValue = float32Array[ righti ]; const maxRightValue = float32Array[ righti + 3 ]; float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue; float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue; } } return didChange; } } } 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 = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG; if ( isLeaf ) { const offset = uint32Array[ node32Index + 6 ]; const count = uint16Array[ node16Index + 14 ]; callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), offset, count ); } else { // TODO: use node functions here const left = node32Index + BYTES_PER_NODE / 4; const right = uint32Array[ node32Index + 6 ]; const splitAxis = uint32Array[ node32Index + 7 ]; const stopTraversal = callback( depth, isLeaf, new Float32Array( buffer, node32Index * 4, 6 ), splitAxis ); if ( ! stopTraversal ) { _traverse( left, depth + 1 ); _traverse( right, depth + 1 ); } } } } /* Core Cast Functions */ raycast( ray, materialOrSide = FrontSide ) { const roots = this._roots; const geometry = this.geometry; const intersects = []; const isMaterial = materialOrSide.isMaterial; const isArrayMaterial = Array.isArray( materialOrSide ); const groups = geometry.groups; const side = isMaterial ? materialOrSide.side : materialOrSide; for ( let i = 0, l = roots.length; i < l; i ++ ) { const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side; const startCount = intersects.length; setBuffer( roots[ i ] ); raycast( 0, geometry, materialSide, ray, intersects ); clearBuffer(); if ( isArrayMaterial ) { const materialIndex = groups[ i ].materialIndex; for ( let j = startCount, jl = intersects.length; j < jl; j ++ ) { intersects[ j ].face.materialIndex = materialIndex; } } } return intersects; } raycastFirst( ray, materialOrSide = FrontSide ) { const roots = this._roots; const geometry = this.geometry; const isMaterial = materialOrSide.isMaterial; const isArrayMaterial = Array.isArray( materialOrSide ); let closestResult = null; const groups = geometry.groups; const side = isMaterial ? materialOrSide.side : materialOrSide; for ( let i = 0, l = roots.length; i < l; i ++ ) { const materialSide = isArrayMaterial ? materialOrSide[ groups[ i ].materialIndex ].side : side; setBuffer( roots[ i ] ); const result = raycastFirst( 0, geometry, materialSide, ray ); clearBuffer(); if ( result != null && ( closestResult == null || result.distance < closestResult.distance ) ) { closestResult = result; if ( isArrayMaterial ) { result.face.materialIndex = groups[ i ].materialIndex; } } } return closestResult; } intersectsGeometry( otherGeometry, geomToMesh ) { const geometry = this.geometry; let result = false; for ( const root of this._roots ) { setBuffer( root ); result = intersectsGeometry( 0, geometry, otherGeometry, geomToMesh ); clearBuffer(); if ( result ) { break; } } return result; } shapecast( callbacks, _intersectsTriangleFunc, _orderNodesFunc ) { const geometry = this.geometry; if ( callbacks instanceof Function ) { if ( _intersectsTriangleFunc ) { // Support the previous function signature that provided three sequential index buffer // indices here. const originalTriangleFunc = _intersectsTriangleFunc; _intersectsTriangleFunc = ( tri, index, contained, depth ) => { const i3 = index * 3; return originalTriangleFunc( tri, i3, i3 + 1, i3 + 2, contained, depth ); }; } callbacks = { boundsTraverseOrder: _orderNodesFunc, intersectsBounds: callbacks, intersectsTriangle: _intersectsTriangleFunc, intersectsRange: null, }; console.warn( 'MeshBVH: Shapecast function signature has changed and now takes an object of callbacks as a second argument. See docs for new signature.' ); } const triangle = trianglePool.getPrimitive(); let { boundsTraverseOrder, intersectsBounds, intersectsRange, intersectsTriangle, } = callbacks; if ( intersectsRange && intersectsTriangle ) { const originalIntersectsRange = intersectsRange; intersectsRange = ( offset, count, contained, depth, nodeIndex ) => { if ( ! originalIntersectsRange( offset, count, contained, depth, nodeIndex ) ) { return iterateOverTriangles( offset, count, geometry, intersectsTriangle, contained, depth, triangle ); } return true; }; } else if ( ! intersectsRange ) { if ( intersectsTriangle ) { intersectsRange = ( offset, count, contained, depth ) => { return iterateOverTriangles( offset, count, geometry, intersectsTriangle, contained, depth, triangle ); }; } else { intersectsRange = ( offset, count, contained ) => { return contained; }; } } let result = false; let byteOffset = 0; for ( const root of this._roots ) { setBuffer( root ); result = shapecast( 0, geometry, intersectsBounds, intersectsRange, boundsTraverseOrder, byteOffset ); clearBuffer(); if ( result ) { break; } byteOffset += root.byteLength; } trianglePool.releasePrimitive( triangle ); return result; } bvhcast( otherBvh, matrixToLocal, callbacks ) { // BVHCast function for intersecting two BVHs against each other. Ultimately just uses two recursive shapecast calls rather // than an approach that walks down the tree (see bvhcast.js file for more info). let { intersectsRanges, intersectsTriangles, } = callbacks; const indexAttr = this.geometry.index; const positionAttr = this.geometry.attributes.position; const otherIndexAttr = otherBvh.geometry.index; const otherPositionAttr = otherBvh.geometry.attributes.position; tempMatrix.copy( matrixToLocal ).invert(); const triangle = trianglePool.getPrimitive(); const triangle2 = trianglePool.getPrimitive(); if ( intersectsTriangles ) { function iterateOverDoubleTriangles( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) { for ( let i2 = offset2, l2 = offset2 + count2; i2 < l2; i2 ++ ) { setTriangle( triangle2, i2 * 3, otherIndexAttr, otherPositionAttr ); triangle2.a.applyMatrix4( matrixToLocal ); triangle2.b.applyMatrix4( matrixToLocal ); triangle2.c.applyMatrix4( matrixToLocal ); triangle2.needsUpdate = true; for ( let i1 = offset1, l1 = offset1 + count1; i1 < l1; i1 ++ ) { setTriangle( triangle, i1 * 3, indexAttr, positionAttr ); triangle.needsUpdate = true; if ( intersectsTriangles( triangle, triangle2, i1, i2, depth1, index1, depth2, index2 ) ) { return true; } } } return false; } if ( intersectsRanges ) { const originalIntersectsRanges = intersectsRanges; intersectsRanges = function ( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) { if ( ! originalIntersectsRanges( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ) ) { return iterateOverDoubleTriangles( offset1, count1, offset2, count2, depth1, index1, depth2, index2 ); } return true; }; } else { intersectsRanges = iterateOverDoubleTriangles; } } otherBvh.getBoundingBox( aabb2 ); aabb2.applyMatrix4( matrixToLocal ); const result = this.shapecast( { intersectsBounds: box => aabb2.intersectsBox( box ), intersectsRange: ( offset1, count1, contained, depth1, nodeIndex1, box ) => { aabb.copy( box ); aabb.applyMatrix4( tempMatrix ); return otherBvh.shapecast( { intersectsBounds: box => aabb.intersectsBox( box ), intersectsRange: ( offset2, count2, contained, depth2, nodeIndex2 ) => { return intersectsRanges( offset1, count1, offset2, count2, depth1, nodeIndex1, depth2, nodeIndex2 ); }, } ); } } ); trianglePool.releasePrimitive( triangle ); trianglePool.releasePrimitive( triangle2 ); return result; } /* Derived Cast Functions */ intersectsBox( box, boxToMesh ) { obb.set( box.min, box.max, boxToMesh ); obb.needsUpdate = true; return this.shapecast( { intersectsBounds: box => obb.intersectsBox( box ), intersectsTriangle: tri => obb.intersectsTriangle( tri ) } ); } intersectsSphere( sphere ) { return this.shapecast( { intersectsBounds: box => sphere.intersectsBox( box ), intersectsTriangle: tri => tri.intersectsSphere( sphere ) } ); } closestPointToGeometry( otherGeometry, geometryToBvh, target1 = { }, target2 = { }, minThreshold = 0, maxThreshold = Infinity ) { if ( ! otherGeometry.boundingBox ) { otherGeometry.computeBoundingBox(); } obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh ); obb.needsUpdate = true; const geometry = this.geometry; const pos = geometry.attributes.position; const index = geometry.index; const otherPos = otherGeometry.attributes.position; const otherIndex = otherGeometry.index; const triangle = trianglePool.getPrimitive(); const triangle2 = trianglePool.getPrimitive(); let tempTarget1 = temp1; let tempTargetDest1 = temp2; let tempTarget2 = null; let tempTargetDest2 = null; if ( target2 ) { tempTarget2 = temp3; tempTargetDest2 = temp4; } let closestDistance = Infinity; let closestDistanceTriIndex = null; let closestDistanceOtherTriIndex = null; tempMatrix.copy( geometryToBvh ).invert(); obb2.matrix.copy( tempMatrix ); this.shapecast( { boundsTraverseOrder: box => { return obb.distanceToBox( box ); }, intersectsBounds: ( box, isLeaf, score ) => { if ( score < closestDistance && score < maxThreshold ) { // if we know the triangles of this bounds will be intersected next then // save the bounds to use during triangle checks. if ( isLeaf ) { obb2.min.copy( box.min ); obb2.max.copy( box.max ); obb2.needsUpdate = true; } return true; } return false; }, intersectsRange: ( offset, count ) => { if ( otherGeometry.boundsTree ) { // if the other geometry has a bvh then use the accelerated path where we use shapecast to find // the closest bounds in the other geometry to check. return otherGeometry.boundsTree.shapecast( { boundsTraverseOrder: box => { return obb2.distanceToBox( box ); }, intersectsBounds: ( box, isLeaf, score ) => { return score < closestDistance && score < maxThreshold; }, intersectsRange: ( otherOffset, otherCount ) => { for ( let i2 = otherOffset * 3, l2 = ( otherOffset + otherCount ) * 3; i2 < l2; i2 += 3 ) { setTriangle( triangle2, i2, otherIndex, otherPos ); triangle2.a.applyMatrix4( geometryToBvh ); triangle2.b.applyMatrix4( geometryToBvh ); triangle2.c.applyMatrix4( geometryToBvh ); triangle2.needsUpdate = true; for ( let i = offset * 3, l = ( offset + count ) * 3; i < l; i += 3 ) { setTriangle( triangle, i, index, pos ); triangle.needsUpdate = true; const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); if ( dist < closestDistance ) { tempTargetDest1.copy( tempTarget1 ); if ( tempTargetDest2 ) { tempTargetDest2.copy( tempTarget2 ); } closestDistance = dist; closestDistanceTriIndex = i / 3; closestDistanceOtherTriIndex = i2 / 3; } // stop traversal if we find a point that's under the given threshold if ( dist < minThreshold ) { return true; } } } }, } ); } else { // If no bounds tree then we'll just check every triangle. const triCount = otherIndex ? otherIndex.count : otherPos.count; for ( let i2 = 0, l2 = triCount; i2 < l2; i2 += 3 ) { setTriangle( triangle2, i2, otherIndex, otherPos ); triangle2.a.applyMatrix4( geometryToBvh ); triangle2.b.applyMatrix4( geometryToBvh ); triangle2.c.applyMatrix4( geometryToBvh ); triangle2.needsUpdate = true; for ( let i = offset * 3, l = ( offset + count ) * 3; i < l; i += 3 ) { setTriangle( triangle, i, index, pos ); triangle.needsUpdate = true; const dist = triangle.distanceToTriangle( triangle2, tempTarget1, tempTarget2 ); if ( dist < closestDistance ) { tempTargetDest1.copy( tempTarget1 ); if ( tempTargetDest2 ) { tempTargetDest2.copy( tempTarget2 ); } closestDistance = dist; closestDistanceTriIndex = i / 3; closestDistanceOtherTriIndex = i2 / 3; } // stop traversal if we find a point that's under the given threshold if ( dist < minThreshold ) { return true; } } } } }, } ); trianglePool.releasePrimitive( triangle ); trianglePool.releasePrimitive( triangle2 ); if ( closestDistance === Infinity ) return null; if ( ! target1.point ) target1.point = tempTargetDest1.clone(); else target1.point.copy( tempTargetDest1 ); target1.distance = closestDistance, target1.faceIndex = closestDistanceTriIndex; if ( target2 ) { if ( ! target2.point ) target2.point = tempTargetDest2.clone(); else target2.point.copy( tempTargetDest2 ); target2.point.applyMatrix4( tempMatrix ); tempTargetDest1.applyMatrix4( tempMatrix ); target2.distance = tempTargetDest1.sub( target2.point ).length(); target2.faceIndex = closestDistanceOtherTriIndex; } return target1; } closestPointToPoint( point, target = { }, minThreshold = 0, maxThreshold = Infinity ) { // early out if under minThreshold // skip checking if over maxThreshold // set minThreshold = maxThreshold to quickly check if a point is within a threshold // returns Infinity if no value found const minThresholdSq = minThreshold * minThreshold; const maxThresholdSq = maxThreshold * maxThreshold; let closestDistanceSq = Infinity; let closestDistanceTriIndex = null; this.shapecast( { boundsTraverseOrder: box => { temp.copy( point ).clamp( box.min, box.max ); return temp.distanceToSquared( point ); }, intersectsBounds: ( box, isLeaf, score ) => { return score < closestDistanceSq && score < maxThresholdSq; }, intersectsTriangle: ( tri, triIndex ) => { tri.closestPointToPoint( point, temp ); const distSq = point.distanceToSquared( temp ); if ( distSq < closestDistanceSq ) { temp1.copy( temp ); closestDistanceSq = distSq; closestDistanceTriIndex = triIndex; } if ( distSq < minThresholdSq ) { return true; } else { return false; } }, } ); if ( closestDistanceSq === Infinity ) return null; const closestDistance = Math.sqrt( closestDistanceSq ); if ( ! target.point ) target.point = temp1.clone(); else target.point.copy( temp1 ); target.distance = closestDistance, target.faceIndex = closestDistanceTriIndex; return target; } getBoundingBox( target ) { target.makeEmpty(); const roots = this._roots; roots.forEach( buffer => { arrayToBox( 0, new Float32Array( buffer ), tempBox ); target.union( tempBox ); } ); return target; } } // Deprecation const originalRaycast = MeshBVH.prototype.raycast; MeshBVH.prototype.raycast = function ( ...args ) { if ( args[ 0 ].isMesh ) { console.warn( 'MeshBVH: The function signature and results frame for "raycast" has changed. See docs for new signature.' ); const [ mesh, raycaster, ray, intersects, ] = args; const results = originalRaycast.call( this, ray, mesh.material ); results.forEach( hit => { hit = convertRaycastIntersect( hit, mesh, raycaster ); if ( hit ) { intersects.push( hit ); } } ); return intersects; } else { return originalRaycast.apply( this, args ); } }; const originalRaycastFirst = MeshBVH.prototype.raycastFirst; MeshBVH.prototype.raycastFirst = function ( ...args ) { if ( args[ 0 ].isMesh ) { console.warn( 'MeshBVH: The function signature and results frame for "raycastFirst" has changed. See docs for new signature.' ); const [ mesh, raycaster, ray, ] = args; return convertRaycastIntersect( originalRaycastFirst.call( this, ray, mesh.material ), mesh, raycaster ); } else { return originalRaycastFirst.apply( this, args ); } }; const originalClosestPointToPoint = MeshBVH.prototype.closestPointToPoint; MeshBVH.prototype.closestPointToPoint = function ( ...args ) { if ( args[ 0 ].isMesh ) { console.warn( 'MeshBVH: The function signature and results frame for "closestPointToPoint" has changed. See docs for new signature.' ); args.unshift(); const target = args[ 1 ]; const result = {}; args[ 1 ] = result; originalClosestPointToPoint.apply( this, args ); if ( target ) { target.copy( result.point ); } return result.distance; } else { return originalClosestPointToPoint.apply( this, args ); } }; const originalClosestPointToGeometry = MeshBVH.prototype.closestPointToGeometry; MeshBVH.prototype.closestPointToGeometry = function ( ...args ) { const target1 = args[ 2 ]; const target2 = args[ 3 ]; if ( target1 && target1.isVector3 || target2 && target2.isVector3 ) { console.warn( 'MeshBVH: The function signature and results frame for "closestPointToGeometry" has changed. See docs for new signature.' ); const result1 = {}; const result2 = {}; const geometryToBvh = args[ 1 ]; args[ 2 ] = result1; args[ 3 ] = result2; originalClosestPointToGeometry.apply( this, args ); if ( target1 ) { target1.copy( result1.point ); } if ( target2 ) { target2.copy( result2.point ).applyMatrix4( geometryToBvh ); } return result1.distance; } else { return originalClosestPointToGeometry.apply( this, args ); } }; const originalRefit = MeshBVH.prototype.refit; MeshBVH.prototype.refit = function ( ...args ) { const nodeIndices = args[ 0 ]; const terminationIndices = args[ 1 ]; if ( terminationIndices && ( terminationIndices instanceof Set || Array.isArray( terminationIndices ) ) ) { console.warn( 'MeshBVH: The function signature for "refit" has changed. See docs for new signature.' ); const newNodeIndices = new Set(); terminationIndices.forEach( v => newNodeIndices.add( v ) ); if ( nodeIndices ) { nodeIndices.forEach( v => newNodeIndices.add( v ) ); } originalRefit.call( this, newNodeIndices ); } else { originalRefit.apply( this, args ); } }; [ 'intersectsGeometry', 'shapecast', 'intersectsBox', 'intersectsSphere', ].forEach( name => { const originalFunc = MeshBVH.prototype[ name ]; MeshBVH.prototype[ name ] = function ( ...args ) { if ( args[ 0 ] === null || args[ 0 ].isMesh ) { args.shift(); console.warn( `MeshBVH: The function signature for "${ name }" has changed and no longer takes Mesh. See docs for new signature.` ); } return originalFunc.apply( this, args ); }; } );