three-bvh-csg
Version:
A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh
407 lines (271 loc) • 9.67 kB
JavaScript
import { Ray, Matrix4, DoubleSide, Vector3, Vector4, Triangle, Line3 } from 'three';
import { IntersectionMap } from './IntersectionMap.js';
import { ADDITION, SUBTRACTION, INTERSECTION, DIFFERENCE } from './constants.js';
const _ray = new Ray();
const _matrix = new Matrix4();
const _tri = new Triangle();
const _vec3 = new Vector3();
const _vec4a = new Vector4();
const _vec4b = new Vector4();
const _vec4c = new Vector4();
const _vec4_0 = new Vector4();
const _vec4_1 = new Vector4();
const _vec4_2 = new Vector4();
const _edge = new Line3();
const _normal = new Vector3();
const JITTER_EPSILON = 1e-8;
const OFFSET_EPSILON = 1e-15;
export const BACK_SIDE = - 1;
export const FRONT_SIDE = 1;
export const COPLANAR_OPPOSITE = - 2;
export const COPLANAR_ALIGNED = 2;
export const INVERT_TRI = 0;
export const ADD_TRI = 1;
export const SKIP_TRI = 2;
let _debugContext = null;
export function setDebugContext( debugData ) {
_debugContext = debugData;
}
export function getHitSide( tri, bvh ) {
// random function that returns [ - 0.5, 0.5 ];
function rand() {
return Math.random() - 0.5;
}
// get the ray the check the triangle for
tri.getNormal( _normal );
_ray.direction.copy( _normal );
tri.getMidpoint( _ray.origin );
const total = 3;
let count = 0;
let minDistance = Infinity;
for ( let i = 0; i < total; i ++ ) {
// jitter the ray slightly
_ray.direction.x += rand() * JITTER_EPSILON;
_ray.direction.y += rand() * JITTER_EPSILON;
_ray.direction.z += rand() * JITTER_EPSILON;
// and invert it so we can account for floating point error by checking both directions
// to catch coplanar distances
_ray.direction.multiplyScalar( - 1 );
// check if the ray hit the backside
const hit = bvh.raycastFirst( _ray, DoubleSide );
let hitBackSide = Boolean( hit && _ray.direction.dot( hit.face.normal ) > 0 );
if ( hitBackSide ) {
count ++;
}
if ( hit !== null ) {
minDistance = Math.min( minDistance, hit.distance );
}
// if we're right up against another face then we're coplanar
if ( minDistance <= OFFSET_EPSILON ) {
return hit.face.normal.dot( _normal ) > 0 ? COPLANAR_ALIGNED : COPLANAR_OPPOSITE;
}
// if our current casts meet our requirements then early out
if ( count / total > 0.5 || ( i - count + 1 ) / total > 0.5 ) {
break;
}
}
return count / total > 0.5 ? BACK_SIDE : FRONT_SIDE;
}
// returns the intersected triangles and returns objects mapping triangle indices to
// the other triangles intersected
export function collectIntersectingTriangles( a, b ) {
const aIntersections = new IntersectionMap();
const bIntersections = new IntersectionMap();
_matrix
.copy( a.matrixWorld )
.invert()
.multiply( b.matrixWorld );
a.geometry.boundsTree.bvhcast( b.geometry.boundsTree, _matrix, {
intersectsTriangles( triangleA, triangleB, ia, ib ) {
if ( triangleA.intersectsTriangle( triangleB, _edge, true ) ) {
// if the edge distance is zero (and not from being coplanar) then exit early and don't include the
// triangle in the set of intersecting triangles
if ( _edge.distanceSq() === 0 && triangleA.plane.normal.dot( triangleB.plane.normal ) < 1.0 - 1e-10 ) {
return false;
}
aIntersections.add( ia, ib );
bIntersections.add( ib, ia );
if ( _debugContext ) {
_debugContext.addEdge( _edge );
_debugContext.addIntersectingTriangles( ia, triangleA, ib, triangleB );
}
}
return false;
}
} );
return { aIntersections, bIntersections };
}
// Add the barycentric interpolated values fro the triangle into the new attribute data
export function appendAttributeFromTriangle(
triIndex,
baryCoordTri,
geometry,
matrixWorld,
normalMatrix,
attributeInfo,
invert = false,
) {
const attributes = geometry.attributes;
const indexAttr = geometry.index;
const i3 = triIndex * 3;
const i0 = indexAttr.getX( i3 + 0 );
const i1 = indexAttr.getX( i3 + 1 );
const i2 = indexAttr.getX( i3 + 2 );
for ( const key in attributeInfo ) {
// check if the key we're asking for is in the geometry at all
const attr = attributes[ key ];
const arr = attributeInfo[ key ];
if ( ! ( key in attributes ) ) {
throw new Error( `CSG Operations: Attribute ${ key } not available on geometry.` );
}
// handle normals and positions specially because they require transforming
// TODO: handle tangents
const itemSize = attr.itemSize;
if ( key === 'position' ) {
_tri.a.fromBufferAttribute( attr, i0 ).applyMatrix4( matrixWorld );
_tri.b.fromBufferAttribute( attr, i1 ).applyMatrix4( matrixWorld );
_tri.c.fromBufferAttribute( attr, i2 ).applyMatrix4( matrixWorld );
pushBarycoordInterpolatedValues( _tri.a, _tri.b, _tri.c, baryCoordTri, 3, arr, invert );
} else if ( key === 'normal' ) {
_tri.a.fromBufferAttribute( attr, i0 ).applyNormalMatrix( normalMatrix );
_tri.b.fromBufferAttribute( attr, i1 ).applyNormalMatrix( normalMatrix );
_tri.c.fromBufferAttribute( attr, i2 ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_tri.a.multiplyScalar( - 1 );
_tri.b.multiplyScalar( - 1 );
_tri.c.multiplyScalar( - 1 );
}
pushBarycoordInterpolatedValues( _tri.a, _tri.b, _tri.c, baryCoordTri, 3, arr, invert, true );
} else {
_vec4a.fromBufferAttribute( attr, i0 );
_vec4b.fromBufferAttribute( attr, i1 );
_vec4c.fromBufferAttribute( attr, i2 );
pushBarycoordInterpolatedValues( _vec4a, _vec4b, _vec4c, baryCoordTri, itemSize, arr, invert );
}
}
}
// Append all the values of the attributes for the triangle onto the new attribute arrays
export function appendAttributesFromIndices(
i0,
i1,
i2,
attributes,
matrixWorld,
normalMatrix,
attributeInfo,
invert = false,
) {
appendAttributeFromIndex( i0, attributes, matrixWorld, normalMatrix, attributeInfo, invert );
appendAttributeFromIndex( invert ? i2 : i1, attributes, matrixWorld, normalMatrix, attributeInfo, invert );
appendAttributeFromIndex( invert ? i1 : i2, attributes, matrixWorld, normalMatrix, attributeInfo, invert );
}
// Returns the triangle to add when performing an operation
export function getOperationAction( operation, hitSide, invert = false ) {
switch ( operation ) {
case ADDITION:
if ( hitSide === FRONT_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {
return ADD_TRI;
}
break;
case SUBTRACTION:
if ( invert ) {
if ( hitSide === BACK_SIDE ) {
return INVERT_TRI;
}
} else {
if ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) {
return ADD_TRI;
}
}
break;
case DIFFERENCE:
if ( hitSide === BACK_SIDE ) {
return INVERT_TRI;
} else if ( hitSide === FRONT_SIDE ) {
return ADD_TRI;
}
break;
case INTERSECTION:
if ( hitSide === BACK_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {
return ADD_TRI;
}
break;
default:
throw new Error( `Unrecognized CSG operation enum "${ operation }".` );
}
return SKIP_TRI;
}
// takes a set of barycentric values in the form of a triangle, a set of vectors, number of components,
// and whether to invert the result and pushes the new values onto the provided attribute array
function pushBarycoordInterpolatedValues( v0, v1, v2, baryCoordTri, itemSize, attrArr, invert = false, normalize = false ) {
// adds the appropriate number of values for the vector onto the array
const addValues = v => {
attrArr.push( v.x );
if ( itemSize > 1 ) attrArr.push( v.y );
if ( itemSize > 2 ) attrArr.push( v.z );
if ( itemSize > 3 ) attrArr.push( v.w );
};
// barycentric interpolate the first component
_vec4_0.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.a.x )
.addScaledVector( v1, baryCoordTri.a.y )
.addScaledVector( v2, baryCoordTri.a.z );
_vec4_1.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.b.x )
.addScaledVector( v1, baryCoordTri.b.y )
.addScaledVector( v2, baryCoordTri.b.z );
_vec4_2.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.c.x )
.addScaledVector( v1, baryCoordTri.c.y )
.addScaledVector( v2, baryCoordTri.c.z );
if ( normalize ) {
_vec4_0.normalize();
_vec4_1.normalize();
_vec4_2.normalize();
}
// if the face is inverted then add the values in an inverted order
addValues( _vec4_0 );
if ( invert ) {
addValues( _vec4_2 );
addValues( _vec4_1 );
} else {
addValues( _vec4_1 );
addValues( _vec4_2 );
}
}
// Adds the values for the given vertex index onto the new attribute arrays
function appendAttributeFromIndex(
index,
attributes,
matrixWorld,
normalMatrix,
attributeInfo,
invert = false,
) {
for ( const key in attributeInfo ) {
// check if the key we're asking for is in the geometry at all
const attr = attributes[ key ];
const arr = attributeInfo[ key ];
if ( ! ( key in attributes ) ) {
throw new Error( `CSG Operations: Attribute ${ key } no available on geometry.` );
}
// specially handle the position and normal attributes because they require transforms
// TODO: handle tangents
const itemSize = attr.itemSize;
if ( key === 'position' ) {
_vec3.fromBufferAttribute( attr, index ).applyMatrix4( matrixWorld );
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else if ( key === 'normal' ) {
_vec3.fromBufferAttribute( attr, index ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_vec3.multiplyScalar( - 1 );
}
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else {
arr.push( attr.getX( index ) );
if ( itemSize > 1 ) arr.push( attr.getY( index ) );
if ( itemSize > 2 ) arr.push( attr.getZ( index ) );
if ( itemSize > 3 ) arr.push( attr.getW( index ) );
}
}
}