UNPKG

three-mesh-bvh

Version:

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

615 lines (387 loc) 13.5 kB
import { Box3, BufferGeometry, Matrix4, Mesh, Vector3, Ray, Sphere } from 'three'; import { BVH, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh'; const _geometry = /* @__PURE__ */ new BufferGeometry(); const _matrix = /* @__PURE__ */ new Matrix4(); const _inverseMatrix = /* @__PURE__ */ new Matrix4(); const _box = /* @__PURE__ */ new Box3(); const _sphere = /* @__PURE__ */ new Sphere(); const _vec = /* @__PURE__ */ new Vector3(); const _ray = /* @__PURE__ */ new Ray(); const _mesh = /* @__PURE__ */ new Mesh(); const _geometryRange = {}; export class ObjectBVH extends BVH { constructor( root, options = {} ) { options = { precise: false, includeInstances: true, matrixWorld: Array.isArray( root ) ? new Matrix4() : root.matrixWorld, maxLeafSize: 1, ...options, }; super(); // collect all the leaf node objects in the geometries const objectSet = new Set(); collectObjects( root, objectSet ); // calculate the number of bits required for the primary id, leaving the remainder // for the instanceId count const objects = Array.from( objectSet ); const idBits = Math.ceil( Math.log2( objects.length ) ); const idMask = constructIdMask( idBits ); this.objects = objects; this.idBits = idBits; this.idMask = idMask; this.primitiveBuffer = null; this.primitiveBufferStride = 1; // settings this.precise = options.precise; this.includeInstances = options.includeInstances; this.matrixWorld = options.matrixWorld; this.init( options ); } getObjectFromId( compositeId ) { const { idMask, objects } = this; const id = getObjectId( compositeId, idMask ); return objects[ id ]; } getInstanceFromId( compositeId ) { const { idMask, idBits } = this; return getInstanceId( compositeId, idBits, idMask ); } init( options ) { const { objects, idBits } = this; this.primitiveBuffer = new Uint32Array( this._countPrimitives( objects ) ); this._fillPrimitiveBuffer( objects, idBits, this.primitiveBuffer ); super.init( options ); } writePrimitiveBounds( i, targetBuffer, writeOffset ) { // TODO: it would be best to cache this matrix inversion const { primitiveBuffer } = this; _inverseMatrix.copy( this.matrixWorld ).invert(); this._getPrimitiveBoundingBox( primitiveBuffer[ i ], _inverseMatrix, _box ); const { min, max } = _box; targetBuffer[ writeOffset + 0 ] = min.x; targetBuffer[ writeOffset + 1 ] = min.y; targetBuffer[ writeOffset + 2 ] = min.z; targetBuffer[ writeOffset + 3 ] = max.x; targetBuffer[ writeOffset + 4 ] = max.y; targetBuffer[ writeOffset + 5 ] = max.z; } getRootRanges() { return [ { offset: 0, count: this.primitiveBuffer.length } ]; } shapecast( callbacks ) { return super.shapecast( { ...callbacks, intersectsPrimitive: callbacks.intersectsObject, scratchPrimitive: null, iterate: iterateOverObjects, } ); } // TODO: this is out of sync with the MeshBVH raycast signature. // Change this to "raycastObject3D"? Or add an equivalent? raycast( raycaster, intersects = [] ) { const { matrixWorld, includeInstances } = this; const { firstHitOnly } = raycaster; const localIntersects = []; // transform the ray into the local bvh frame _inverseMatrix.copy( matrixWorld ).invert(); _ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix ); let closestDistance = Infinity; let closestHit = null; this.shapecast( { boundsTraverseOrder: box => { return box.distanceToPoint( _ray.origin ); }, intersectsBounds: box => { if ( firstHitOnly ) { if ( ! _ray.intersectBox( box, _vec ) ) { return NOT_INTERSECTED; } // early out if the box is further than the closest raycast _vec.applyMatrix4( matrixWorld ); return raycaster.ray.origin.distanceTo( _vec ) < closestDistance ? INTERSECTED : NOT_INTERSECTED; } else { return _ray.intersectsBox( box ) ? INTERSECTED : NOT_INTERSECTED; } }, intersectsObject( object, instanceId ) { // skip non visible objects if ( ! object.visible ) { return; } localIntersects.length = 0; if ( object.isInstancedMesh && includeInstances ) { // raycast the instance _mesh.geometry = object.geometry; _mesh.material = object.material; object.getMatrixAt( instanceId, _mesh.matrixWorld ); _mesh.matrixWorld.premultiply( object.matrixWorld ); _mesh.raycast( raycaster, localIntersects ); localIntersects.forEach( hit => { hit.object = object; hit.instanceId = instanceId; } ); _mesh.material = null; } else if ( object.isBatchedMesh && includeInstances ) { if ( ! object.getVisibleAt( instanceId ) ) { return; } // extract the geometry & material const geometryId = object.getGeometryIdAt( instanceId ); const geometryRange = object.getGeometryRangeAt( geometryId, _geometryRange ); _geometry.index = object.geometry.index; _geometry.attributes = object.geometry.attributes; _geometry.setDrawRange( geometryRange.start, geometryRange.count ); _mesh.geometry = _geometry; _mesh.material = object.material; // perform a raycast against the proxy mesh object.getMatrixAt( instanceId, _mesh.matrixWorld ); _mesh.matrixWorld.premultiply( object.matrixWorld ); _mesh.raycast( raycaster, localIntersects ); // fix up the fields localIntersects.forEach( hit => { hit.object = object; hit.batchId = instanceId; } ); _mesh.material = null; _geometry.index = null; _geometry.attributes = null; _geometry.setDrawRange( 0, Infinity ); } else { object.raycast( raycaster, localIntersects ); } // find the closest hit to track if ( firstHitOnly ) { localIntersects.forEach( hit => { if ( hit.distance < closestDistance ) { closestDistance = hit.distance; closestHit = hit; } } ); } else { intersects.push( ...localIntersects ); } }, } ); // save the closest hit only if firstHitOnly = true if ( firstHitOnly && closestHit ) { intersects.push( closestHit ); } return intersects; } // get the bounding box of a primitive node accounting for the bvh options _getPrimitiveBoundingBox( compositeId, inverseMatrixWorld, target ) { const { objects, idMask, idBits, precise, includeInstances } = this; const id = getObjectId( compositeId, idMask ); const instanceId = getInstanceId( compositeId, idBits, idMask ); const object = objects[ id ]; if ( ! includeInstances && ( object.isInstancedMesh || object.isBatchedMesh ) ) { // if we're not using instances then just account for the overall bounds of the BatchedMesh and InstancedMesh if ( ! object.boundingBox ) { object.computeBoundingBox(); } if ( ! object.boundingSphere ) { object.computeBoundingSphere(); } _matrix .copy( object.matrixWorld ) .premultiply( inverseMatrixWorld ); _sphere .copy( object.boundingSphere ) .applyMatrix4( _matrix ); target .copy( object.boundingBox ) .applyMatrix4( _matrix ); shrinkToSphere( target, _sphere ); } else if ( precise ) { // calculate precise bounds if necessary by calculating the bounds of all vertices // in the bvh frame if ( object.isInstancedMesh ) { object .getMatrixAt( instanceId, _matrix ); _matrix .premultiply( object.matrixWorld ) .premultiply( inverseMatrixWorld ); getPreciseBounds( object.geometry, _matrix, target ); } else if ( object.isBatchedMesh ) { const geometryId = object.getGeometryIdAt( instanceId ); const geometryRange = object.getGeometryRangeAt( geometryId, _geometryRange ); _geometry.index = object.geometry.index; _geometry.attributes = object.geometry.attributes; _geometry.setDrawRange( geometryRange.start, geometryRange.count ); object .getMatrixAt( instanceId, _matrix ); _matrix .premultiply( object.matrixWorld ) .premultiply( inverseMatrixWorld ); getPreciseBounds( _geometry, _matrix, target ); _geometry.attributes = null; } else { _matrix .copy( object.matrixWorld ) .premultiply( inverseMatrixWorld ); target.setFromObject( object, true ).applyMatrix4( inverseMatrixWorld ); } } else { // otherwise use the fast path of extracting the cached, AABB bounds and transforming them // into the local BVH frame if ( object.isInstancedMesh ) { if ( ! object.geometry.boundingBox ) { object.geometry.computeBoundingBox(); } if ( ! object.geometry.boundingSphere ) { object.geometry.computeBoundingSphere(); } object .getMatrixAt( instanceId, _matrix ); _matrix .premultiply( object.matrixWorld ) .premultiply( inverseMatrixWorld ); _sphere .copy( object.geometry.boundingSphere ) .applyMatrix4( _matrix ); target .copy( object.geometry.boundingBox ) .applyMatrix4( _matrix ); shrinkToSphere( target, _sphere ); } else if ( object.isBatchedMesh ) { const geometryId = object.getGeometryIdAt( instanceId ); object .getMatrixAt( instanceId, _matrix ); _matrix .premultiply( object.matrixWorld ) .premultiply( inverseMatrixWorld ); object .getBoundingSphereAt( geometryId, _sphere ) .applyMatrix4( _matrix ); object .getBoundingBoxAt( geometryId, target ) .applyMatrix4( _matrix ); shrinkToSphere( target, _sphere ); } else { target .setFromObject( object, false ) .applyMatrix4( inverseMatrixWorld ); } } } // counts the total number of primitives required by the objects in given array of objects _countPrimitives( objects ) { const { includeInstances } = this; let total = 0; objects.forEach( object => { if ( object.isInstancedMesh && includeInstances ) { total += object.count; } else if ( object.isBatchedMesh && includeInstances ) { if ( ! ( 'instanceCount' in object ) ) { throw new Error( 'ObjectBVH: Three.js revision >= r169 is required to use BatchedMesh.' ); } total += object.instanceCount; } else { total ++; } } ); return total; } _fillPrimitiveBuffer( objects, idBits, target ) { const { includeInstances } = this; let index = 0; objects.forEach( ( object, i ) => { if ( object.isInstancedMesh && includeInstances ) { const count = object.count; for ( let c = 0; c < count; c ++ ) { target[ index ] = ( c << idBits ) | i; index ++; } } else if ( object.isBatchedMesh && includeInstances ) { const { instanceCount, maxInstanceCount } = object; let foundInstances = 0; let iter = 0; while ( foundInstances < instanceCount && iter < maxInstanceCount ) { // TODO: it would be better to have a consistent way of querying whether an // instance were active try { object.getVisibleAt( iter ); target[ index ] = ( iter << idBits ) | i; foundInstances ++; index ++; } catch { // } iter ++; } } else { target[ index ] = i; index ++; } } ); } } // id functions // construct a mask with the given number of bits set to 1 function constructIdMask( idBits ) { let mask = 0; for ( let i = 0; i < idBits; i ++ ) { mask = mask << 1 | 1; } return mask; } // extract the primary object id given the provided mask function getObjectId( id, idMask ) { return id & idMask; } // extract the instance id given the mask and number of bits to shift function getInstanceId( id, idBits, idMask ) { return ( id & ( ~ idMask ) ) >> idBits; } // traverse the full scene and collect all leaves function collectObjects( root, objectSet = new Set() ) { if ( Array.isArray( root ) ) { root.forEach( object => collectObjects( object, objectSet ) ); } else { root.traverse( child => { if ( child.isMesh || child.isLine || child.isPoints ) { objectSet.add( child ); } } ); } } // calculate precise box bounds of the given geometry in the given frame function getPreciseBounds( geometry, matrix, target ) { target.makeEmpty(); const drawRange = geometry.drawRange; const indexAttr = geometry.index; const posAttr = geometry.attributes.position; const start = drawRange.start; const vertCount = indexAttr ? indexAttr.count : posAttr.count; const count = Math.min( vertCount - start, drawRange.count ); for ( let i = start, l = start + count; i < l; i ++ ) { let vi = i; if ( indexAttr ) { vi = indexAttr.getX( vi ); } _vec.fromBufferAttribute( posAttr, vi ).applyMatrix4( matrix ); target.expandByPoint( _vec ); } return target; } // iterator helper for raycasting function iterateOverObjects( offset, count, bvh, callback, contained, depth, /* scratch */ ) { const { primitiveBuffer, objects, idMask, idBits } = bvh; for ( let i = offset, l = count + offset; i < l; i ++ ) { const compositeId = primitiveBuffer[ i ]; const id = getObjectId( compositeId, idMask ); const instanceId = getInstanceId( compositeId, idBits, idMask ); const object = objects[ id ]; if ( callback( object, instanceId, contained, depth ) ) { return true; } } return false; } function shrinkToSphere( box, sphere ) { _vec.copy( sphere.center ).addScalar( - sphere.radius ); box.min.max( _vec ); _vec.copy( sphere.center ).addScalar( sphere.radius ); box.max.min( _vec ); }