UNPKG

three-mesh-bvh

Version:

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

522 lines (338 loc) 12.6 kB
import * as THREE from 'three'; import { intersectTris, intersectClosestTri } from './Utils/RayIntersectTriUtlities.js'; import { arrayToBox } from './Utils/ArrayBoxUtilities.js'; import { OrientedBox } from './Utils/OrientedBox.js'; import { SeparatingAxisTriangle } from './Utils/SeparatingAxisTriangle.js'; import { sphereIntersectTriangle } from './Utils/MathUtilities.js'; const boundingBox = new THREE.Box3(); const boxIntersection = new THREE.Vector3(); const xyzFields = [ 'x', 'y', 'z' ]; function setTriangle( tri, i, index, pos ) { const ta = tri.a; const tb = tri.b; const tc = tri.c; let i3 = index.getX( i ); ta.x = pos.getX( i3 ); ta.y = pos.getY( i3 ); ta.z = pos.getZ( i3 ); i3 = index.getX( i + 1 ); tb.x = pos.getX( i3 ); tb.y = pos.getY( i3 ); tb.z = pos.getZ( i3 ); i3 = index.getX( i + 2 ); tc.x = pos.getX( i3 ); tc.y = pos.getY( i3 ); tc.z = pos.getZ( i3 ); } export default class MeshBVHNode { constructor() { // internal nodes have boundingData, left, right, and splitAxis // leaf nodes have offset and count (referring to primitives in the mesh geometry) } intersectRay( ray, target ) { arrayToBox( this.boundingData, boundingBox ); return ray.intersectBox( boundingBox, target ); } raycast( mesh, raycaster, ray, intersects ) { if ( this.count ) intersectTris( mesh, mesh.geometry, raycaster, ray, this.offset, this.count, intersects ); else { if ( this.left.intersectRay( ray, boxIntersection ) ) this.left.raycast( mesh, raycaster, ray, intersects ); if ( this.right.intersectRay( ray, boxIntersection ) ) this.right.raycast( mesh, raycaster, ray, intersects ); } } raycastFirst( mesh, raycaster, ray ) { if ( this.count ) { return intersectClosestTri( mesh, mesh.geometry, raycaster, ray, this.offset, this.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 = this.splitAxis; 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 = this.left; c2 = this.right; } else { c1 = this.right; c2 = this.left; } const c1Intersection = c1.intersectRay( ray, boxIntersection ); const c1Result = c1Intersection ? c1.raycastFirst( mesh, raycaster, 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 only along the split axis const rayOrig = ray.origin[ xyzAxis ]; const toPoint = rayOrig - c1Result.point[ xyzAxis ]; const toChild1 = rayOrig - c2.boundingData[ splitAxis ]; const toChild2 = rayOrig - c2.boundingData[ splitAxis + 3 ]; const toPointSq = toPoint * toPoint; if ( toPointSq <= toChild1 * toChild1 && toPointSq <= toChild2 * toChild2 ) { 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 = c2.intersectRay( ray, boxIntersection ); const c2Result = c2Intersection ? c2.raycastFirst( mesh, raycaster, ray ) : null; if ( c1Result && c2Result ) { return c1Result.distance <= c2Result.distance ? c1Result : c2Result; } else { return c1Result || c2Result || null; } } } } MeshBVHNode.prototype.shapecast = ( function () { const triangle = new SeparatingAxisTriangle(); const cachedBox1 = new THREE.Box3(); const cachedBox2 = new THREE.Box3(); return function shapecast( mesh, intersectsBoundsFunc, intersectsTriangleFunc = null, nodeScoreFunc = null ) { if ( this.count && intersectsTriangleFunc ) { const geometry = mesh.geometry; const index = geometry.index; const pos = geometry.attributes.position; const offset = this.offset; const count = this.count; for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) { setTriangle( triangle, i, index, pos ); triangle.update(); if ( intersectsTriangleFunc( triangle, i, i + 1, i + 2 ) ) { return true; } } return false; } else { const left = this.left; const right = this.right; let c1 = left; let c2 = right; let score1, score2; let box1, box2; if ( nodeScoreFunc ) { box1 = cachedBox1; box2 = cachedBox2; arrayToBox( c1.boundingData, box1 ); arrayToBox( c2.boundingData, box2 ); score1 = nodeScoreFunc( box1 ); score2 = nodeScoreFunc( box2 ); if ( score2 < score1 ) { c1 = right; c2 = left; const temp = score1; score1 = score2; score2 = temp; const tempBox = box1; box1 = box2; box2 = tempBox; } } if ( ! box1 ) { box1 = cachedBox1; arrayToBox( c1.boundingData, box1 ); } const isC1Leaf = ! ! c1.count; const c1Intersection = intersectsBoundsFunc( box1, isC1Leaf, score1, c1 ) && c1.shapecast( mesh, intersectsBoundsFunc, intersectsTriangleFunc, nodeScoreFunc ); if ( c1Intersection ) return true; if ( ! box2 ) { box2 = cachedBox2; arrayToBox( c2.boundingData, box2 ); } const isC2Leaf = ! ! c2.count; const c2Intersection = intersectsBoundsFunc( box2, isC2Leaf, score2, c2 ) && c2.shapecast( mesh, intersectsBoundsFunc, intersectsTriangleFunc, nodeScoreFunc ); if ( c2Intersection ) return true; return false; } }; } )(); MeshBVHNode.prototype.intersectsGeometry = ( function () { const triangle = new SeparatingAxisTriangle(); const triangle2 = new SeparatingAxisTriangle(); const cachedMesh = new THREE.Mesh(); const invertedMat = new THREE.Matrix4(); const obb = new OrientedBox(); const obb2 = new OrientedBox(); return function intersectsGeometry( mesh, geometry, geometryToBvh, cachedObb = null ) { if ( cachedObb === null ) { if ( ! geometry.boundingBox ) { geometry.computeBoundingBox(); } obb.set( geometry.boundingBox.min, geometry.boundingBox.max, geometryToBvh ); obb.update(); cachedObb = obb; } if ( this.count ) { const thisGeometry = mesh.geometry; const thisIndex = thisGeometry.index; const thisPos = thisGeometry.attributes.position; const index = geometry.index; const pos = geometry.attributes.position; const offset = this.offset; const count = this.count; // 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.getInverse( geometryToBvh ); if ( geometry.boundsTree ) { function triangleCallback( tri ) { tri.a.applyMatrix4( geometryToBvh ); tri.b.applyMatrix4( geometryToBvh ); tri.c.applyMatrix4( geometryToBvh ); tri.update(); 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.update(); if ( tri.intersectsTriangle( triangle2 ) ) { return true; } } return false; } arrayToBox( this.boundingData, obb2 ); obb2.matrix.copy( invertedMat ); obb2.update(); cachedMesh.geometry = geometry; const res = geometry.boundsTree.shapecast( cachedMesh, box => obb2.intersectsBox( box ), triangleCallback ); cachedMesh.geometry = null; 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.update(); for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) { setTriangle( triangle2, i2, index, pos ); triangle2.update(); if ( triangle.intersectsTriangle( triangle2 ) ) { return true; } } } } } else { const left = this.left; const right = this.right; arrayToBox( left.boundingData, boundingBox ); const leftIntersection = cachedObb.intersectsBox( boundingBox ) && left.intersectsGeometry( mesh, geometry, geometryToBvh, cachedObb ); if ( leftIntersection ) return true; arrayToBox( right.boundingData, boundingBox ); const rightIntersection = cachedObb.intersectsBox( boundingBox ) && right.intersectsGeometry( mesh, geometry, geometryToBvh, cachedObb ); if ( rightIntersection ) return true; return false; } }; } )(); MeshBVHNode.prototype.intersectsBox = ( function () { const obb = new OrientedBox(); return function intersectsBox( mesh, box, boxToBvh ) { obb.set( box.min, box.max, boxToBvh ); obb.update(); return this.shapecast( mesh, box => obb.intersectsBox( box ), tri => obb.intersectsTriangle( tri ) ); }; } )(); MeshBVHNode.prototype.intersectsSphere = ( function () { return function intersectsSphere( mesh, sphere ) { return this.shapecast( mesh, box => sphere.intersectsBox( box ), tri => sphereIntersectTriangle( sphere, tri ) ); }; } )(); MeshBVHNode.prototype.closestPointToPoint = ( function () { // 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 temp = new THREE.Vector3(); return function closestPointToPoint( mesh, point, target = null, minThreshold = 0, maxThreshold = Infinity ) { let closestDistance = Infinity; this.shapecast( mesh, ( box, isLeaf, score ) => score < closestDistance && score < maxThreshold, tri => { tri.closestPointToPoint( point, temp ); const dist = point.distanceTo( temp ); if ( dist < closestDistance ) { if ( target ) target.copy( temp ); closestDistance = dist; } if ( dist < minThreshold ) return true; return false; }, box => box.distanceToPoint( point ) ); return closestDistance; }; } )(); MeshBVHNode.prototype.closestPointToGeometry = ( function () { // 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 tri2 = new SeparatingAxisTriangle(); const obb = new OrientedBox(); const temp1 = new THREE.Vector3(); const temp2 = new THREE.Vector3(); return function closestPointToGeometry( mesh, geometry, geometryToBvh, target1 = null, target2 = null, minThreshold = 0, maxThreshold = Infinity ) { if ( ! geometry.boundingBox ) geometry.computeBoundingBox(); obb.set( geometry.boundingBox.min, geometry.boundingBox.max, geometryToBvh ); obb.update(); const pos = geometry.attributes.position; const index = geometry.index; let tempTarget1, tempTarget2; if ( target1 ) tempTarget1 = temp1; if ( target2 ) tempTarget2 = temp2; let closestDistance = Infinity; this.shapecast( mesh, ( box, isLeaf, score ) => score < closestDistance && score < maxThreshold, tri => { const sphere1 = tri.sphere; for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) { setTriangle( tri2, i2, index, pos ); tri2.a.applyMatrix4( geometryToBvh ); tri2.b.applyMatrix4( geometryToBvh ); tri2.c.applyMatrix4( geometryToBvh ); tri2.sphere.setFromPoints( tri2.points ); const sphere2 = tri2.sphere; const sphereDist = sphere2.center.distanceTo( sphere1.center ) - sphere2.radius - sphere1.radius; if ( sphereDist > closestDistance ) continue; tri2.update(); const dist = tri.distanceToTriangle( tri2, tempTarget1, tempTarget2 ); if ( dist < closestDistance ) { if ( target1 ) target1.copy( tempTarget1 ); if ( target2 ) target2.copy( tempTarget2 ); closestDistance = dist; } if ( dist < minThreshold ) return true; } return false; }, box => obb.distanceToBox( box, Math.min( closestDistance, maxThreshold ) ) ); return closestDistance; }; } )();