UNPKG

three-bvh-csg

Version:

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

236 lines (169 loc) 7.34 kB
import { Matrix4, Matrix3, Triangle } from 'three'; import { getHitSide, collectIntersectingTriangles, appendAttributeFromTriangle, appendAttributesFromIndices, getOperationAction, SKIP_TRI, INVERT_TRI, } from './operationsUtils.js'; const _matrix = new Matrix4(); const _normalMatrix = new Matrix3(); const _triA = new Triangle(); const _triB = new Triangle(); const _tri = new Triangle(); const _barycoordTri = new Triangle(); function getFirstIdFromSet( set ) { for ( const id of set ) return id; } // runs the given operation against a and b using the splitter and appending data to the // typedAttributeData object. export function performOperation( a, b, operation, splitter, typedAttributeData, 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, operation, false, typedAttributeData, groupOffset ); performSplitTriangleOperations( a, b, aIntersections, operation, false, splitter, typedAttributeData, groupOffset ); groupOffset = useGroups ? a.geometry.groups.length || 1 : - 1; performWholeTriangleOperations( b, a, bIntersections, operation, true, typedAttributeData, groupOffset ); performSplitTriangleOperations( b, a, bIntersections, operation, true, splitter, typedAttributeData, groupOffset ); return { groups: resultGroups, materials: resultMaterials }; } // perform triangle splitting and CSG operations on the set of split triangles function performSplitTriangleOperations( a, b, intersectionMap, operation, invert, splitter, attributeInfo, groupOffset = 0 ) { const invertedGeometry = a.matrixWorld.determinant() < 0; // transforms into the local frame of matrix b _matrix .copy( b.matrixWorld ) .invert() .multiply( a.matrixWorld ); _normalMatrix .getNormalMatrix( a.matrixWorld ) .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; const intersectionSet = intersectionMap.intersectionSet; // 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; const attrSet = attributeInfo.getGroupSet( groupIndex ); // get the triangle in the geometry B local frame const ia3 = 3 * ia; const ia0 = aIndex.getX( ia3 + 0 ); const ia1 = aIndex.getX( ia3 + 1 ); const ia2 = aIndex.getX( ia3 + 2 ); _triA.a.fromBufferAttribute( aPosition, ia0 ).applyMatrix4( _matrix ); _triA.b.fromBufferAttribute( aPosition, ia1 ).applyMatrix4( _matrix ); _triA.c.fromBufferAttribute( aPosition, ia2 ).applyMatrix4( _matrix ); // initialize the splitter with the triangle from geometry A splitter.initialize( _triA ); // split the triangle with the intersecting triangles from B const intersectingIndices = intersectionSet[ ia ]; for ( let ib = 0, l = intersectingIndices.length; ib < l; ib ++ ) { const ib3 = 3 * intersectingIndices[ ib ]; const ib0 = bIndex.getX( ib3 + 0 ); const ib1 = bIndex.getX( ib3 + 1 ); const ib2 = bIndex.getX( ib3 + 2 ); _triB.a.fromBufferAttribute( bPosition, ib0 ); _triB.b.fromBufferAttribute( bPosition, ib1 ); _triB.c.fromBufferAttribute( bPosition, ib2 ); splitter.splitByTriangle( _triB ); } // for all triangles in the split result const triangles = splitter.triangles; for ( let ib = 0, l = triangles.length; ib < l; ib ++ ) { // get the barycentric coordinates of the clipped triangle to add const clippedTri = triangles[ ib ]; // try to use the side derived from the clipping but if it turns out to be // uncertain then fall back to the raycasting approach const hitSide = getHitSide( clippedTri, bBVH ); const action = getOperationAction( operation, hitSide, invert ); if ( action !== SKIP_TRI ) { _triA.getBarycoord( clippedTri.a, _barycoordTri.a ); _triA.getBarycoord( clippedTri.b, _barycoordTri.b ); _triA.getBarycoord( clippedTri.c, _barycoordTri.c ); const invertTri = action === INVERT_TRI; appendAttributeFromTriangle( ia, _barycoordTri, a.geometry, a.matrixWorld, _normalMatrix, attrSet, invertedGeometry !== invertTri ); } } } 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, operation, invert, attributeInfo, groupOffset = 0 ) { const invertedGeometry = a.matrixWorld.determinant() < 0; // matrix for transforming into the local frame of geometry b _matrix .copy( b.matrixWorld ) .invert() .multiply( a.matrixWorld ); _normalMatrix .getNormalMatrix( a.matrixWorld ) .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; const traverseSet = new Set(); for ( let i = 0, l = aIndex.count / 3; i < l; i ++ ) { if ( ! ( i in splitTriSet.intersectionSet ) ) { traverseSet.add( i ); } } while ( traverseSet.size > 0 ) { const id = getFirstIdFromSet( traverseSet ); traverseSet.delete( id ); stack.push( id ); // get the vertex indices const i3 = 3 * id; const i0 = aIndex.getX( i3 + 0 ); const i1 = aIndex.getX( i3 + 1 ); const i2 = aIndex.getX( i3 + 2 ); // get the vertex position in the frame of geometry b so we can // perform hit testing _tri.a.fromBufferAttribute( aPosition, i0 ).applyMatrix4( _matrix ); _tri.b.fromBufferAttribute( aPosition, i1 ).applyMatrix4( _matrix ); _tri.c.fromBufferAttribute( aPosition, i2 ).applyMatrix4( _matrix ); // get the side and decide if we need to cull the triangle based on the operation const hitSide = getHitSide( _tri, bBVH ); const action = getOperationAction( operation, hitSide, invert ); while ( stack.length > 0 ) { const currId = stack.pop(); const groupIndex = groupOffset === - 1 ? 0 : groupIndices[ currId ] + groupOffset; const attrSet = attributeInfo.getGroupSet( groupIndex ); for ( let i = 0; i < 3; i ++ ) { const sid = halfEdges.getSiblingTriangleIndex( currId, i ); if ( sid !== - 1 && traverseSet.has( sid ) ) { stack.push( sid ); traverseSet.delete( sid ); } } if ( action === SKIP_TRI ) { continue; } const i3 = 3 * currId; const i0 = aIndex.getX( i3 + 0 ); const i1 = aIndex.getX( i3 + 1 ); const i2 = aIndex.getX( i3 + 2 ); const invertTri = action === INVERT_TRI; appendAttributesFromIndices( i0, i1, i2, aAttributes, a.matrixWorld, _normalMatrix, attrSet, invertTri !== invertedGeometry ); } } }