three-mesh-bvh
Version:
A BVH implementation to speed up raycasting against three.js meshes.
814 lines (503 loc) • 18.6 kB
JavaScript
import { Triangle, Vector3, Vector2, Line3, Plane } from 'three';
import { SeparatingAxisBounds } from './SeparatingAxisBounds.js';
import { closestPointsSegmentToSegment, sphereIntersectTriangle } from './MathUtilities.js';
const componentKeys = [ 'x', 'y', 'z' ];
const ZERO_EPSILON = 1e-15;
const ZERO_EPSILON_SQR = ZERO_EPSILON * ZERO_EPSILON;
function isNearZero( value ) {
return Math.abs( value ) < ZERO_EPSILON;
}
export class ExtendedTriangle extends Triangle {
constructor( ...args ) {
super( ...args );
this.isExtendedTriangle = true;
this.satAxes = new Array( 4 ).fill().map( () => new Vector3() );
this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() );
this.points = [ this.a, this.b, this.c ];
this.plane = new Plane();
this.isDegenerateIntoSegment = false;
this.isDegenerateIntoPoint = false;
this.degenerateSegment = new Line3();
this.needsUpdate = true;
}
intersectsSphere( sphere ) {
return sphereIntersectTriangle( sphere, this );
}
update() {
const a = this.a;
const b = this.b;
const c = this.c;
const points = this.points;
const satAxes = this.satAxes;
const satBounds = this.satBounds;
const axis0 = satAxes[ 0 ];
const sab0 = satBounds[ 0 ];
this.getNormal( axis0 );
sab0.setFromPoints( axis0, points );
const axis1 = satAxes[ 1 ];
const sab1 = satBounds[ 1 ];
axis1.subVectors( a, b );
sab1.setFromPoints( axis1, points );
const axis2 = satAxes[ 2 ];
const sab2 = satBounds[ 2 ];
axis2.subVectors( b, c );
sab2.setFromPoints( axis2, points );
const axis3 = satAxes[ 3 ];
const sab3 = satBounds[ 3 ];
axis3.subVectors( c, a );
sab3.setFromPoints( axis3, points );
const lengthAB = axis1.length();
const lengthBC = axis2.length();
const lengthCA = axis3.length();
this.isDegenerateIntoPoint = false;
this.isDegenerateIntoSegment = false;
if ( lengthAB < ZERO_EPSILON ) {
if ( lengthBC < ZERO_EPSILON || lengthCA < ZERO_EPSILON ) {
this.isDegenerateIntoPoint = true;
} else {
this.isDegenerateIntoSegment = true;
this.degenerateSegment.start.copy( a );
this.degenerateSegment.end.copy( c );
}
} else if ( lengthBC < ZERO_EPSILON ) {
if ( lengthCA < ZERO_EPSILON ) {
this.isDegenerateIntoPoint = true;
} else {
this.isDegenerateIntoSegment = true;
this.degenerateSegment.start.copy( b );
this.degenerateSegment.end.copy( a );
}
} else if ( lengthCA < ZERO_EPSILON ) {
this.isDegenerateIntoSegment = true;
this.degenerateSegment.start.copy( c );
this.degenerateSegment.end.copy( b );
}
this.plane.setFromNormalAndCoplanarPoint( axis0, a );
this.needsUpdate = false;
}
}
ExtendedTriangle.prototype.closestPointToSegment = ( function () {
const point1 = /* @__PURE__ */ new Vector3();
const point2 = /* @__PURE__ */ new Vector3();
const edge = /* @__PURE__ */ new Line3();
return function distanceToSegment( segment, target1 = null, target2 = null ) {
const { start, end } = segment;
const points = this.points;
let distSq;
let closestDistanceSq = Infinity;
// check the triangle edges
for ( let i = 0; i < 3; i ++ ) {
const nexti = ( i + 1 ) % 3;
edge.start.copy( points[ i ] );
edge.end.copy( points[ nexti ] );
closestPointsSegmentToSegment( edge, segment, point1, point2 );
distSq = point1.distanceToSquared( point2 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( point2 );
}
}
// check end points
this.closestPointToPoint( start, point1 );
distSq = start.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( start );
}
this.closestPointToPoint( end, point1 );
distSq = end.distanceToSquared( point1 );
if ( distSq < closestDistanceSq ) {
closestDistanceSq = distSq;
if ( target1 ) target1.copy( point1 );
if ( target2 ) target2.copy( end );
}
return Math.sqrt( closestDistanceSq );
};
} )();
ExtendedTriangle.prototype.intersectsTriangle = ( function () {
const saTri2 = /* @__PURE__ */ new ExtendedTriangle();
const cachedSatBounds = /* @__PURE__ */ new SeparatingAxisBounds();
const cachedSatBounds2 = /* @__PURE__ */ new SeparatingAxisBounds();
const tmpVec = /* @__PURE__ */ new Vector3();
const dir1 = /* @__PURE__ */ new Vector3();
const dir2 = /* @__PURE__ */ new Vector3();
const tempDir = /* @__PURE__ */ new Vector3();
const edge1 = /* @__PURE__ */ new Line3();
const edge2 = /* @__PURE__ */ new Line3();
const tempPoint = /* @__PURE__ */ new Vector3();
const bounds1 = /* @__PURE__ */ new Vector2();
const bounds2 = /* @__PURE__ */ new Vector2();
function coplanarIntersectsTriangle( self, other, target, suppressLog ) {
// Perform separating axis intersection test only for coplanar triangles
// There should be at least one non-degenerate triangle when calling this
// Otherwise we won't know the plane normal
const planeNormal = tmpVec;
if ( ! self.isDegenerateIntoPoint && ! self.isDegenerateIntoSegment ) {
planeNormal.copy( self.plane.normal );
} else {
planeNormal.copy( other.plane.normal );
}
const satBounds1 = self.satBounds;
const satAxes1 = self.satAxes;
for ( let i = 1; i < 4; i ++ ) {
const sb = satBounds1[ i ];
const sa = satAxes1[ i ];
cachedSatBounds.setFromPoints( sa, other.points );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
tempDir.copy( planeNormal ).cross( sa );
cachedSatBounds.setFromPoints( tempDir, self.points );
cachedSatBounds2.setFromPoints( tempDir, other.points );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
const satBounds2 = other.satBounds;
const satAxes2 = other.satAxes;
for ( let i = 1; i < 4; i ++ ) {
const sb = satBounds2[ i ];
const sa = satAxes2[ i ];
cachedSatBounds.setFromPoints( sa, self.points );
if ( sb.isSeparated( cachedSatBounds ) ) return false;
tempDir.crossVectors( planeNormal, sa );
cachedSatBounds.setFromPoints( tempDir, self.points );
cachedSatBounds2.setFromPoints( tempDir, other.points );
if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;
}
if ( target ) {
// TODO find two points that intersect on the edges and make that the result
if ( ! suppressLog ) {
console.warn( 'ExtendedTriangle.intersectsTriangle: Triangles are coplanar which does not support an output edge. Setting edge to 0, 0, 0.' );
}
target.start.set( 0, 0, 0 );
target.end.set( 0, 0, 0 );
}
return true;
}
function findSingleBounds( a, b, c, aProj, bProj, cProj, aDist, bDist, cDist, bounds, edge ) {
let t = aDist / ( aDist - bDist );
bounds.x = aProj + ( bProj - aProj ) * t;
edge.start.subVectors( b, a ).multiplyScalar( t ).add( a );
t = aDist / ( aDist - cDist );
bounds.y = aProj + ( cProj - aProj ) * t;
edge.end.subVectors( c, a ).multiplyScalar( t ).add( a );
}
/**
* Calculates intersection segment of a triangle with intersection line.
* Intersection line is snapped to its biggest component.
* And triangle points are passed as a projection on that component.
* @returns whether this is a coplanar case or not
*/
function findIntersectionLineBounds( self, aProj, bProj, cProj, abDist, acDist, aDist, bDist, cDist, bounds, edge ) {
if ( abDist > 0 ) {
// then bcDist < 0
findSingleBounds( self.c, self.a, self.b, cProj, aProj, bProj, cDist, aDist, bDist, bounds, edge );
} else if ( acDist > 0 ) {
findSingleBounds( self.b, self.a, self.c, bProj, aProj, cProj, bDist, aDist, cDist, bounds, edge );
} else if ( bDist * cDist > 0 || aDist != 0 ) {
findSingleBounds( self.a, self.b, self.c, aProj, bProj, cProj, aDist, bDist, cDist, bounds, edge );
} else if ( bDist != 0 ) {
findSingleBounds( self.b, self.a, self.c, bProj, aProj, cProj, bDist, aDist, cDist, bounds, edge );
} else if ( cDist != 0 ) {
findSingleBounds( self.c, self.a, self.b, cProj, aProj, bProj, cDist, aDist, bDist, bounds, edge );
} else {
return true;
}
return false;
}
function intersectTriangleSegment( triangle, degenerateTriangle, target, suppressLog ) {
const segment = degenerateTriangle.degenerateSegment;
const startDist = triangle.plane.distanceToPoint( segment.start );
const endDist = triangle.plane.distanceToPoint( segment.end );
if ( isNearZero( startDist ) ) {
if ( isNearZero( endDist ) ) {
return coplanarIntersectsTriangle( triangle, degenerateTriangle, target, suppressLog );
} else {
// Is this fine to modify target even if there might be no intersection?
if ( target ) {
target.start.copy( segment.start );
target.end.copy( segment.start );
}
return triangle.containsPoint( segment.start );
}
} else if ( isNearZero( endDist ) ) {
if ( target ) {
target.start.copy( segment.end );
target.end.copy( segment.end );
}
return triangle.containsPoint( segment.end );
} else {
if ( triangle.plane.intersectLine( segment, tmpVec ) != null ) {
if ( target ) {
target.start.copy( tmpVec );
target.end.copy( tmpVec );
}
return triangle.containsPoint( tmpVec );
} else {
return false;
}
}
}
function intersectTrianglePoint( triangle, degenerateTriangle, target ) {
const point = degenerateTriangle.a;
if ( isNearZero( triangle.plane.distanceToPoint( point ) ) && triangle.containsPoint( point ) ) {
if ( target ) {
target.start.copy( point );
target.end.copy( point );
}
return true;
} else {
return false;
}
}
function intersectSegmentPoint( segmentTri, pointTri, target ) {
const segment = segmentTri.degenerateSegment;
const point = pointTri.a;
segment.closestPointToPoint( point, true, tmpVec );
if ( point.distanceToSquared( tmpVec ) < ZERO_EPSILON_SQR ) {
if ( target ) {
target.start.copy( point );
target.end.copy( point );
}
return true;
} else {
return false;
}
}
function handleDegenerateCases( self, other, target, suppressLog ) {
if ( self.isDegenerateIntoSegment ) {
if ( other.isDegenerateIntoSegment ) {
// TODO: replace with Line.distanceSqToLine3 after r179
const segment1 = self.degenerateSegment;
const segment2 = other.degenerateSegment;
const delta1 = dir1;
const delta2 = dir2;
segment1.delta( delta1 );
segment2.delta( delta2 );
const startDelta = tmpVec.subVectors( segment2.start, segment1.start );
const denom = delta1.x * delta2.y - delta1.y * delta2.x;
if ( isNearZero( denom ) ) {
return false;
}
const t = ( startDelta.x * delta2.y - startDelta.y * delta2.x ) / denom;
const u = - ( delta1.x * startDelta.y - delta1.y * startDelta.x ) / denom;
if ( t < 0 || t > 1 || u < 0 || u > 1 ) {
return false;
}
const z1 = segment1.start.z + delta1.z * t;
const z2 = segment2.start.z + delta2.z * u;
if ( isNearZero( z1 - z2 ) ) {
if ( target ) {
target.start.copy( segment1.start ).addScaledVector( delta1, t );
target.end.copy( segment1.start ).addScaledVector( delta1, t );
}
return true;
} else {
return false;
}
} else if ( other.isDegenerateIntoPoint ) {
return intersectSegmentPoint( self, other, target );
} else {
return intersectTriangleSegment( other, self, target, suppressLog );
}
} else if ( self.isDegenerateIntoPoint ) {
if ( other.isDegenerateIntoPoint ) {
if ( other.a.distanceToSquared( self.a ) < ZERO_EPSILON_SQR ) {
if ( target ) {
target.start.copy( self.a );
target.end.copy( self.a );
}
return true;
} else {
return false;
}
} else if ( other.isDegenerateIntoSegment ) {
return intersectSegmentPoint( other, self, target );
} else {
return intersectTrianglePoint( other, self, target );
}
} else {
if ( other.isDegenerateIntoPoint ) {
return intersectTrianglePoint( self, other, target );
} else if ( other.isDegenerateIntoSegment ) {
return intersectTriangleSegment( self, other, target, suppressLog );
} /* else this is a general triangle-traingle case, so return undefined */
}
}
/* TODO: If the triangles are coplanar and intersecting the target is nonsensical. It should at least
* be a line contained by both triangles if not a different special case somehow represented in the return result.
*
* General triangle intersection code is based on Moller's algorithm from here: https://web.stanford.edu/class/cs277/resources/papers/Moller1997b.pdf
* Reference implementation from here: https://github.com/erich666/jgt-code/blob/master/Volume_08/Number_1/Shen2003/tri_tri_test/include/Moller97.c#L570
* All degeneracies are handled before the general algorithm.
* Coplanar check is different from Moller's and based on SAT tests.
*/
return function intersectsTriangle( other, target = null, suppressLog = false ) {
if ( this.needsUpdate ) {
this.update();
}
if ( ! other.isExtendedTriangle ) {
saTri2.copy( other );
saTri2.update();
other = saTri2;
} else if ( other.needsUpdate ) {
other.update();
}
const res = handleDegenerateCases( this, other, target, suppressLog );
if ( res !== undefined ) {
return res;
}
const plane1 = this.plane;
const plane2 = other.plane;
let a1Dist = plane2.distanceToPoint( this.a );
let b1Dist = plane2.distanceToPoint( this.b );
let c1Dist = plane2.distanceToPoint( this.c );
if ( isNearZero( a1Dist ) )
a1Dist = 0;
if ( isNearZero( b1Dist ) )
b1Dist = 0;
if ( isNearZero( c1Dist ) )
c1Dist = 0;
const a1b1Dist = a1Dist * b1Dist;
const a1c1Dist = a1Dist * c1Dist;
if ( a1b1Dist > 0 && a1c1Dist > 0 ) {
return false;
}
let a2Dist = plane1.distanceToPoint( other.a );
let b2Dist = plane1.distanceToPoint( other.b );
let c2Dist = plane1.distanceToPoint( other.c );
if ( isNearZero( a2Dist ) )
a2Dist = 0;
if ( isNearZero( b2Dist ) )
b2Dist = 0;
if ( isNearZero( c2Dist ) )
c2Dist = 0;
const a2b2Dist = a2Dist * b2Dist;
const a2c2Dist = a2Dist * c2Dist;
if ( a2b2Dist > 0 && a2c2Dist > 0 ) {
return false;
}
dir1.copy( plane1.normal );
dir2.copy( plane2.normal );
const intersectionLine = dir1.cross( dir2 );
let componentIndex = 0;
let maxComponent = Math.abs( intersectionLine.x );
const comp1 = Math.abs( intersectionLine.y );
if ( comp1 > maxComponent ) {
maxComponent = comp1;
componentIndex = 1;
}
const comp2 = Math.abs( intersectionLine.z );
if ( comp2 > maxComponent ) {
componentIndex = 2;
}
const key = componentKeys[ componentIndex ];
const a1Proj = this.a[ key ];
const b1Proj = this.b[ key ];
const c1Proj = this.c[ key ];
const a2Proj = other.a[ key ];
const b2Proj = other.b[ key ];
const c2Proj = other.c[ key ];
if ( findIntersectionLineBounds( this, a1Proj, b1Proj, c1Proj, a1b1Dist, a1c1Dist, a1Dist, b1Dist, c1Dist, bounds1, edge1 ) ) {
return coplanarIntersectsTriangle( this, other, target, suppressLog );
}
if ( findIntersectionLineBounds( other, a2Proj, b2Proj, c2Proj, a2b2Dist, a2c2Dist, a2Dist, b2Dist, c2Dist, bounds2, edge2 ) ) {
return coplanarIntersectsTriangle( this, other, target, suppressLog );
}
if ( bounds1.y < bounds1.x ) {
const tmp = bounds1.y;
bounds1.y = bounds1.x;
bounds1.x = tmp;
tempPoint.copy( edge1.start );
edge1.start.copy( edge1.end );
edge1.end.copy( tempPoint );
}
if ( bounds2.y < bounds2.x ) {
const tmp = bounds2.y;
bounds2.y = bounds2.x;
bounds2.x = tmp;
tempPoint.copy( edge2.start );
edge2.start.copy( edge2.end );
edge2.end.copy( tempPoint );
}
if ( bounds1.y < bounds2.x || bounds2.y < bounds1.x ) {
return false;
}
if ( target ) {
if ( bounds2.x > bounds1.x ) {
target.start.copy( edge2.start );
} else {
target.start.copy( edge1.start );
}
if ( bounds2.y < bounds1.y ) {
target.end.copy( edge2.end );
} else {
target.end.copy( edge1.end );
}
}
return true;
};
} )();
ExtendedTriangle.prototype.distanceToPoint = ( function () {
const target = /* @__PURE__ */ new Vector3();
return function distanceToPoint( point ) {
this.closestPointToPoint( point, target );
return point.distanceTo( target );
};
} )();
ExtendedTriangle.prototype.distanceToTriangle = ( function () {
const point = /* @__PURE__ */ new Vector3();
const point2 = /* @__PURE__ */ new Vector3();
const cornerFields = [ 'a', 'b', 'c' ];
const line1 = /* @__PURE__ */ new Line3();
const line2 = /* @__PURE__ */ new Line3();
return function distanceToTriangle( other, target1 = null, target2 = null ) {
const lineTarget = target1 || target2 ? line1 : null;
if ( this.intersectsTriangle( other, lineTarget ) ) {
if ( target1 || target2 ) {
if ( target1 ) lineTarget.getCenter( target1 );
if ( target2 ) lineTarget.getCenter( target2 );
}
return 0;
}
let closestDistanceSq = Infinity;
// check all point distances
for ( let i = 0; i < 3; i ++ ) {
let dist;
const field = cornerFields[ i ];
const otherVec = other[ field ];
this.closestPointToPoint( otherVec, point );
dist = otherVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( otherVec );
}
const thisVec = this[ field ];
other.closestPointToPoint( thisVec, point );
dist = thisVec.distanceToSquared( point );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( thisVec );
if ( target2 ) target2.copy( point );
}
}
for ( let i = 0; i < 3; i ++ ) {
const f11 = cornerFields[ i ];
const f12 = cornerFields[ ( i + 1 ) % 3 ];
line1.set( this[ f11 ], this[ f12 ] );
for ( let i2 = 0; i2 < 3; i2 ++ ) {
const f21 = cornerFields[ i2 ];
const f22 = cornerFields[ ( i2 + 1 ) % 3 ];
line2.set( other[ f21 ], other[ f22 ] );
closestPointsSegmentToSegment( line1, line2, point, point2 );
const dist = point.distanceToSquared( point2 );
if ( dist < closestDistanceSq ) {
closestDistanceSq = dist;
if ( target1 ) target1.copy( point );
if ( target2 ) target2.copy( point2 );
}
}
}
return Math.sqrt( closestDistanceSq );
};
} )();