UNPKG

three-bvh-csg

Version:

A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh

592 lines (416 loc) 15.4 kB
import { Matrix4, Matrix3, Triangle, Vector3 } from 'three'; import { getHitSide, collectIntersectingTriangles, getOperationAction, SKIP_TRI, INVERT_TRI, COPLANAR_ALIGNED, COPLANAR_OPPOSITE, } from './operationsUtils.js'; import { getTriCount } from '../utils/geometryUtils.js'; import { HOLLOW_INTERSECTION, HOLLOW_SUBTRACTION } from '../constants.js'; import { isTriDegenerate } from '../utils/triangleUtils.js'; import { Pool } from '../utils/Pool.js'; const _matrix = new Matrix4(); const _inverseMatrix = new Matrix4(); const _builderMatrix = new Matrix4(); const _normalMatrix = new Matrix3(); const _triA = new Triangle(); const _triB = new Triangle(); const _tri = new Triangle(); const _barycoordTri = new Triangle(); const _actions = []; const _builders = []; const _traversed = new Set(); const _midpoint = new Vector3(); const _normal = new Vector3(); const _coplanarTrianglePool = new Pool( () => new Triangle() ); const _coplanarNormal = new Vector3(); const _coplanarTriangles = []; // runs the given operation against a and b using the splitter and appending data to the // geometry builder. export function performOperation( a, b, operations, splitter, builders, options = {}, ) { const { useGroups = true } = options; const { aIntersections, bIntersections } = collectIntersectingTriangles( a, b ); const resultGroups = []; let resultMaterials = null; let groupOffset; groupOffset = useGroups ? 0 : - 1; performWholeTriangleOperations( a, b, aIntersections, operations, false, builders, groupOffset ); performSplitTriangleOperations( a, b, aIntersections, operations, false, splitter, builders, groupOffset ); // find whether the set of operations contains a non-hollow operations. If it does then we need // to perform the second set of triangle additions const nonHollow = operations .findIndex( op => op !== HOLLOW_INTERSECTION && op !== HOLLOW_SUBTRACTION ) !== - 1; if ( nonHollow ) { // clear the index map so for the new geometry being used builders.forEach( builder => builder.clearIndexMap() ); groupOffset = useGroups ? a.geometry.groups.length || 1 : - 1; performWholeTriangleOperations( b, a, bIntersections, operations, true, builders, groupOffset ); performSplitTriangleOperations( b, a, bIntersections, operations, true, splitter, builders, groupOffset ); } // clear the shared info builders.forEach( builder => builder.clearIndexMap() ); _actions.length = 0; return { groups: resultGroups, materials: resultMaterials }; } // perform triangle splitting and CSG operations on the set of split triangles function performSplitTriangleOperations( a, b, intersectionMap, operations, invert, splitter, builders, groupOffset = 0, ) { // transform from a frame -> b frame. When "invert" is true the "b" is the first argument (brush A). _matrix .copy( b.matrixWorld ) .invert() .multiply( a.matrixWorld ); _inverseMatrix .copy( _matrix ) .invert(); // matrix for geometry construction to transform vertices in the brush A's frame if ( invert ) { _builderMatrix.copy( _matrix ); } else { _builderMatrix.identity(); } const invertedGeometry = _builderMatrix.determinant() < 0; _normalMatrix .getNormalMatrix( _builderMatrix ) .multiplyScalar( invertedGeometry ? - 1 : 1 ); const groupIndices = a.geometry.groupIndices; const aIndex = a.geometry.index; const aPosition = a.geometry.attributes.position; const bBVH = b.geometry.boundsTree; const bIndex = b.geometry.index; const bPosition = b.geometry.attributes.position; const splitIds = intersectionMap.ids; // iterate over all split triangle indices for ( let i = 0, l = splitIds.length; i < l; i ++ ) { const ia = splitIds[ i ]; const groupIndex = groupOffset === - 1 ? 0 : groupIndices[ ia ] + groupOffset; // get the triangle in the common frame (brush A's local) const ia3 = 3 * ia; let ia0 = ia3 + 0; let ia1 = ia3 + 1; let ia2 = ia3 + 2; if ( aIndex ) { ia0 = aIndex.getX( ia0 ); ia1 = aIndex.getX( ia1 ); ia2 = aIndex.getX( ia2 ); } _triA.a.fromBufferAttribute( aPosition, ia0 ); _triA.b.fromBufferAttribute( aPosition, ia1 ); _triA.c.fromBufferAttribute( aPosition, ia2 ); if ( invert ) { _triA.a.applyMatrix4( _matrix ); _triA.b.applyMatrix4( _matrix ); _triA.c.applyMatrix4( _matrix ); } // initialize the splitter with the triangle from geometry A splitter.reset(); splitter.initialize( _triA, ia0, ia1, ia2 ); // add coplanar triangles from B to the splitter for later classification _coplanarTriangles.length = 0; _coplanarTrianglePool.clear(); _triA.getNormal( _normal ); const coplanarIndices = intersectionMap.coplanarSet.get( ia ); if ( coplanarIndices ) { for ( const index of coplanarIndices ) { const ib3 = 3 * index; let ib0 = ib3 + 0; let ib1 = ib3 + 1; let ib2 = ib3 + 2; if ( bIndex ) { ib0 = bIndex.getX( ib0 ); ib1 = bIndex.getX( ib1 ); ib2 = bIndex.getX( ib2 ); } const inst = _coplanarTrianglePool.getInstance(); inst.a.fromBufferAttribute( bPosition, ib0 ); inst.b.fromBufferAttribute( bPosition, ib1 ); inst.c.fromBufferAttribute( bPosition, ib2 ); // transform into the common frame when needed if ( ! invert ) { inst.a.applyMatrix4( _inverseMatrix ); inst.b.applyMatrix4( _inverseMatrix ); inst.c.applyMatrix4( _inverseMatrix ); } _coplanarTriangles.push( inst ); } } // split the triangle using cached edges from the bvhcast phase if ( splitter.addConstraintEdge ) { // edges are already in the common frame (brush A's local) — no transform needed const edges = intersectionMap.getIntersectionEdges( ia ); if ( edges ) { for ( const edge of edges ) { splitter.addConstraintEdge( edge ); } } splitter.triangulate(); } else { // split the triangle with the intersecting triangles from B const intersectionSet = intersectionMap.intersectionSet; const intersectingIndices = intersectionSet.get( ia ); for ( let ib = 0, l = intersectingIndices.length; ib < l; ib ++ ) { const index = intersectingIndices[ ib ]; const isCoplanar = coplanarIndices && coplanarIndices.has( index ); const ib3 = 3 * index; let ib0 = ib3 + 0; let ib1 = ib3 + 1; let ib2 = ib3 + 2; if ( bIndex ) { ib0 = bIndex.getX( ib0 ); ib1 = bIndex.getX( ib1 ); ib2 = bIndex.getX( ib2 ); } _triB.a.fromBufferAttribute( bPosition, ib0 ); _triB.b.fromBufferAttribute( bPosition, ib1 ); _triB.c.fromBufferAttribute( bPosition, ib2 ); // transform splitting tris into the common frame when needed if ( ! invert ) { _triB.a.applyMatrix4( _inverseMatrix ); _triB.b.applyMatrix4( _inverseMatrix ); _triB.c.applyMatrix4( _inverseMatrix ); } splitter.splitByTriangle( _triB, isCoplanar ); } } // cache all the attribute data in origA's local frame const { triangles, triangleIndices = [], triangleConnectivity = [] } = splitter; for ( let i = 0, l = builders.length; i < l; i ++ ) { builders[ i ].initInterpolatedAttributeData( a.geometry, _builderMatrix, _normalMatrix, ia0, ia1, ia2 ); } // for all triangles in the split result _traversed.clear(); for ( let ib = 0, l = triangles.length; ib < l; ib ++ ) { // skip the triangle if we've already traversed if ( _traversed.has( ib ) ) { continue; } // try to use the side derived from the clipping but if it turns out to be // uncertain then fall back to the raycasting approach. // If checking the sided ness against brush B's BVH then we need to transform // into the appropriate frame const clippedTri = triangles[ ib ]; const raycastMatrix = invert ? null : _matrix; let hitSide = null; // check against the set of coplanar triangles to see if we can easily determine what to do clippedTri.getMidpoint( _midpoint ); for ( let cp = 0, cpl = _coplanarTriangles.length; cp < cpl; cp ++ ) { const cpt = _coplanarTriangles[ cp ]; if ( cpt.containsPoint( _midpoint ) ) { cpt.getNormal( _coplanarNormal ); hitSide = _normal.dot( _coplanarNormal ) > 0 ? COPLANAR_ALIGNED : COPLANAR_OPPOSITE; break; } } // if the clipped triangle is no coplanar then fall back to raycasting if ( hitSide === null ) { hitSide = getHitSide( clippedTri, bBVH, raycastMatrix ); } _actions.length = 0; _builders.length = 0; // determine action to take for each builder for ( let o = 0, lo = operations.length; o < lo; o ++ ) { const op = getOperationAction( operations[ o ], hitSide, invert ); if ( op !== SKIP_TRI ) { _actions.push( op ); _builders.push( builders[ o ] ); } } if ( _builders.length !== 0 ) { // traverse the connectivity of the triangles to add them to the geometry const stack = [ ib ]; while ( stack.length > 0 ) { const index = stack.pop(); if ( _traversed.has( index ) ) { continue; } // mark this triangle as traversed _traversed.add( index ); // TODO: this is being skipped for now due to the connectivity graph not // including small connections due to floating point error. Adding support // for symmetric vertices across half edges may help this. // push the connected triangle ids onto the stack // const connected = triangleConnectivity[ index ] || []; // for ( let c = 0, l = connected.length; c < l; c ++ ) { // const connectedIndex = connected[ c ]; // if ( triangles[ connectedIndex ] !== null ) { // stack.push( connectedIndex ); // } // } // get the triangle indices const indices = triangleIndices[ index ]; let t0 = null, t1 = null, t2 = null; if ( indices ) { t0 = indices[ 0 ]; t1 = indices[ 1 ]; t2 = indices[ 2 ]; } // get the barycentric coordinates relative to the base triangle const tri = triangles[ index ]; _triA.getBarycoord( tri.a, _barycoordTri.a ); _triA.getBarycoord( tri.b, _barycoordTri.b ); _triA.getBarycoord( tri.c, _barycoordTri.c ); // append the triangle to all builders for ( let k = 0, lk = _builders.length; k < lk; k ++ ) { const builder = _builders[ k ]; const action = _actions[ k ]; const invertTri = action === INVERT_TRI; const invert = invertedGeometry !== invertTri; builder.appendInterpolatedAttributeData( groupIndex, _barycoordTri.a, t0, invert ); if ( invert ) { builder.appendInterpolatedAttributeData( groupIndex, _barycoordTri.c, t2, invert ); builder.appendInterpolatedAttributeData( groupIndex, _barycoordTri.b, t1, invert ); } else { builder.appendInterpolatedAttributeData( groupIndex, _barycoordTri.b, t1, invert ); builder.appendInterpolatedAttributeData( groupIndex, _barycoordTri.c, t2, invert ); } } } } } } return splitIds.length; } // perform CSG operations on the set of whole triangles using a half edge structure // at the moment this isn't always faster due to overhead of building the half edge structure // and degraded connectivity due to split triangles. function performWholeTriangleOperations( a, b, splitTriSet, operations, invert, builders, groupOffset = 0, ) { // _matrix transforms from a's local frame into the common frame (brush A's local) _matrix .copy( b.matrixWorld ) .invert() .multiply( a.matrixWorld ); if ( invert ) { _builderMatrix.copy( _matrix ); } else { _builderMatrix.identity(); } const invertedGeometry = _builderMatrix.determinant() < 0; _normalMatrix .getNormalMatrix( _builderMatrix ) .multiplyScalar( invertedGeometry ? - 1 : 1 ); const bBVH = b.geometry.boundsTree; const groupIndices = a.geometry.groupIndices; const aIndex = a.geometry.index; const aAttributes = a.geometry.attributes; const aPosition = aAttributes.position; const stack = []; const halfEdges = a.geometry.halfEdges; // iterate over every whole triangle, skipping those that are clipped const traversedSet = new Set( splitTriSet.ids ); const triCount = getTriCount( a.geometry ); for ( let id = 0; id < triCount; id ++ ) { // if we've iterated over every triangle then stop if ( traversedSet.size === triCount ) { break; } // skip this triangle if we've already traversed it if ( traversedSet.has( id ) ) { continue; } // track the traversal traversedSet.add( id ); stack.push( id ); // get the vertex indices const i3 = 3 * id; let i0 = i3 + 0; let i1 = i3 + 1; let i2 = i3 + 2; if ( aIndex ) { i0 = aIndex.getX( i0 ); i1 = aIndex.getX( i1 ); i2 = aIndex.getX( i2 ); } // get the vertex position in the common frame (origA's local) for hit testing _tri.a.fromBufferAttribute( aPosition, i0 ); _tri.b.fromBufferAttribute( aPosition, i1 ); _tri.c.fromBufferAttribute( aPosition, i2 ); if ( invert ) { _tri.a.applyMatrix4( _matrix ); _tri.b.applyMatrix4( _matrix ); _tri.c.applyMatrix4( _matrix ); } // get the side and decide if we need to cull the triangle based on the operation. // When !invert, pass _matrix to transform the ray into brush B's BVH frame. const hitSide = getHitSide( _tri, bBVH, invert ? null : _matrix ); // find all attribute sets to append the triangle to _actions.length = 0; _builders.length = 0; for ( let o = 0, lo = operations.length; o < lo; o ++ ) { const op = getOperationAction( operations[ o ], hitSide, invert ); if ( op !== SKIP_TRI ) { _actions.push( op ); _builders.push( builders[ o ] ); } } // continue to iterate on the stack until every triangle has been handled while ( stack.length > 0 ) { const currId = stack.pop(); for ( let i = 0; i < 3; i ++ ) { const sid = halfEdges.getSiblingTriangleIndex( currId, i ); if ( sid !== - 1 && ! traversedSet.has( sid ) ) { stack.push( sid ); traversedSet.add( sid ); } } if ( _builders.length !== 0 ) { const i3 = 3 * currId; let i0 = i3 + 0; let i1 = i3 + 1; let i2 = i3 + 2; if ( aIndex ) { i0 = aIndex.getX( i0 ); i1 = aIndex.getX( i1 ); i2 = aIndex.getX( i2 ); } const groupIndex = groupOffset === - 1 ? 0 : groupIndices[ currId ] + groupOffset; _tri.a.fromBufferAttribute( aPosition, i0 ); _tri.b.fromBufferAttribute( aPosition, i1 ); _tri.c.fromBufferAttribute( aPosition, i2 ); if ( ! isTriDegenerate( _tri ) ) { for ( let k = 0, lk = _builders.length; k < lk; k ++ ) { const builder = _builders[ k ]; const action = _actions[ k ]; const invertTri = action === INVERT_TRI; const invert = invertTri !== invertedGeometry; builder.appendIndexFromGeometry( a.geometry, _builderMatrix, _normalMatrix, groupIndex, i0, invert ); if ( invert ) { builder.appendIndexFromGeometry( a.geometry, _builderMatrix, _normalMatrix, groupIndex, i2, invert ); builder.appendIndexFromGeometry( a.geometry, _builderMatrix, _normalMatrix, groupIndex, i1, invert ); } else { builder.appendIndexFromGeometry( a.geometry, _builderMatrix, _normalMatrix, groupIndex, i1, invert ); builder.appendIndexFromGeometry( a.geometry, _builderMatrix, _normalMatrix, groupIndex, i2, invert ); } } } } } } }