three-bvh-csg
Version:
A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh
2,356 lines (1,474 loc) • 60.4 kB
JavaScript
import { Vector3, Mesh, Matrix4, Ray, Triangle, Vector4, Line3, DoubleSide, Plane, Matrix3, BufferAttribute, BufferGeometry, Group, Color, MeshPhongMaterial, MathUtils, LineSegments, LineBasicMaterial, InstancedMesh, SphereBufferGeometry, MeshBasicMaterial } from 'three';
import { MeshBVH, ExtendedTriangle } from 'three-mesh-bvh';
const HASH_MULTIPLIER = ( 1 + 1e-7 ) * 1e6;
function hashNumber( v ) {
return ~ ~ ( v * HASH_MULTIPLIER );
}
function hashVertex( v ) {
return `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) }`;
}
const _vertices = [ new Vector3(), new Vector3(), new Vector3() ];
class HalfEdgeMap {
constructor( geometry = null ) {
this.data = null;
this.unmatchedEdges = null;
this.matchedEdges = null;
this.useDrawRange = true;
if ( geometry ) {
this.updateFrom( geometry );
}
}
getSiblingTriangleIndex( triIndex, edgeIndex ) {
const otherIndex = this.data[ triIndex * 3 + edgeIndex ];
return otherIndex === - 1 ? - 1 : ~ ~ ( otherIndex / 3 );
}
getSiblingEdgeIndex( triIndex, edgeIndex ) {
const otherIndex = this.data[ triIndex * 3 + edgeIndex ];
return otherIndex === - 1 ? - 1 : ( otherIndex % 3 );
}
updateFrom( geometry ) {
// runs on the assumption that there is a 1 : 1 match of edges
const map = new Map();
// attributes
const { attributes } = geometry;
const indexAttr = geometry.index;
const posAttr = attributes.position;
// get the potential number of triangles
let triCount = indexAttr ? indexAttr.count / 3 : posAttr.count / 3;
const maxTriCount = triCount;
// get the real number of triangles from the based on the draw range
let offset = 0;
if ( this.useDrawRange ) {
offset = geometry.drawRange.start;
if ( geometry.drawRange.count !== Infinity ) {
triCount = ~ ~ ( geometry.drawRange.count / 3 );
}
}
// initialize the connectivity buffer - 1 means no connectivity
let data = this.data;
if ( ! data || data.length < 3 * maxTriCount ) {
data = new Int32Array( 3 * maxTriCount );
}
data.fill( - 1 );
// iterate over all triangles
let unmatchedEdges = 0;
let matchedEdges = 0;
for ( let i = 0; i < triCount; i ++ ) {
const i3 = 3 * i + offset;
for ( let e = 0; e < 3; e ++ ) {
let i0 = i3 + e;
if ( indexAttr ) {
i0 = indexAttr.getX( i0 );
}
_vertices[ e ].fromBufferAttribute( posAttr, i0 );
}
for ( let e = 0; e < 3; e ++ ) {
const nextE = ( e + 1 ) % 3;
const _vec0 = _vertices[ e ];
const _vec1 = _vertices[ nextE ];
const vh0 = hashVertex( _vec0 );
const vh1 = hashVertex( _vec1 );
const reverseHash = `${ vh1 }_${ vh0 }`;
if ( map.has( reverseHash ) ) {
// create a reference between the two triangles and clear the hash
const otherIndex = map.get( reverseHash );
data[ i3 + e ] = otherIndex;
data[ otherIndex ] = i3 + e;
map.delete( reverseHash );
unmatchedEdges --;
matchedEdges ++;
} else {
// save the triangle and triangle edge index captured in one value
// triIndex = ~ ~ ( i0 / 3 );
// edgeIndex = i0 % 3;
const hash = `${ vh0 }_${ vh1 }`;
map.set( hash, i3 + e );
unmatchedEdges ++;
}
}
}
this.matchedEdges = matchedEdges;
this.unmatchedEdges = unmatchedEdges;
this.data = data;
}
}
function areSharedArrayBuffersSupported() {
return typeof SharedArrayBuffer !== 'undefined';
}
function convertToSharedArrayBuffer( array ) {
if ( array.buffer instanceof SharedArrayBuffer ) {
return array;
}
const cons = array.constructor;
const buffer = array.buffer;
const sharedBuffer = new SharedArrayBuffer( buffer.byteLength );
const uintArray = new Uint8Array( buffer );
const sharedUintArray = new Uint8Array( sharedBuffer );
sharedUintArray.set( uintArray, 0 );
return new cons( sharedBuffer );
}
class Brush extends Mesh {
constructor( ...args ) {
super( ...args );
this.isBrush = true;
this._previousMatrix = new Matrix4();
this._previousMatrix.elements.fill( 0 );
}
markUpdated() {
this._previousMatrix.copy( this.matrix );
}
isDirty() {
const { matrix, _previousMatrix } = this;
const el1 = matrix.elements;
const el2 = _previousMatrix.elements;
for ( let i = 0; i < 16; i ++ ) {
if ( el1[ i ] !== el2[ i ] ) {
return true;
}
}
return false;
}
prepareGeometry() {
// generate shared array buffers
const geometry = this.geometry;
const attributes = geometry.attributes;
if ( areSharedArrayBuffersSupported() ) {
for ( const key in attributes ) {
const attribute = attributes[ key ];
if ( attribute.isInterleavedBufferAttribute ) {
throw new Error( 'Brush: InterleavedBufferAttributes are not supported.' );
}
attribute.array = convertToSharedArrayBuffer( attribute.array );
}
}
// generate bounds tree
if ( ! geometry.boundsTree ) {
geometry.boundsTree = new MeshBVH( geometry, { maxLeafTris: 3 } );
if ( geometry.halfEdges ) {
geometry.halfEdges.updateFrom( geometry );
}
}
// generate half edges
if ( ! geometry.halfEdges ) {
geometry.halfEdges = new HalfEdgeMap( geometry );
}
// save group indices for materials
if ( ! geometry.groupIndices ) {
const triCount = geometry.index.count / 3;
const array = new Uint16Array( triCount );
const groups = geometry.groups;
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const { start, count } = groups[ i ];
for ( let g = start / 3, lg = ( start + count ) / 3; g < lg; g ++ ) {
array[ g ] = i;
}
}
geometry.groupIndices = array;
}
}
disposeCacheData() {
const { geometry } = this;
geometry.halfEdges = null;
geometry.boundsTree = null;
geometry.groupIndices = null;
}
}
class IntersectionMap {
constructor() {
this.intersectionSet = {};
this.ids = [];
}
add( id, intersectionId ) {
const { intersectionSet, ids } = this;
if ( ! intersectionSet[ id ] ) {
intersectionSet[ id ] = [];
ids.push( id );
}
intersectionSet[ id ].push( intersectionId );
}
}
const ADDITION = 0;
const SUBTRACTION = 1;
const DIFFERENCE = 3;
const INTERSECTION = 4;
const _ray = new Ray();
const _matrix$2 = new Matrix4();
const _tri$2 = 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$1 = new Line3();
const _normal$1 = new Vector3();
const JITTER_EPSILON = 1e-8;
const OFFSET_EPSILON = 1e-15;
const BACK_SIDE = - 1;
const FRONT_SIDE = 1;
const COPLANAR_OPPOSITE = - 2;
const COPLANAR_ALIGNED = 2;
const INVERT_TRI = 0;
const ADD_TRI = 1;
const SKIP_TRI = 2;
let _debugContext = null;
function setDebugContext( debugData ) {
_debugContext = debugData;
}
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$1 );
_ray.direction.copy( _normal$1 );
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$1 ) > 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
function collectIntersectingTriangles( a, b ) {
const aIntersections = new IntersectionMap();
const bIntersections = new IntersectionMap();
_matrix$2
.copy( a.matrixWorld )
.invert()
.multiply( b.matrixWorld );
a.geometry.boundsTree.bvhcast( b.geometry.boundsTree, _matrix$2, {
intersectsTriangles( triangleA, triangleB, ia, ib ) {
if ( triangleA.intersectsTriangle( triangleB, _edge$1, 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$1.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$1 );
_debugContext.addIntersectingTriangles( ia, triangleA, ib, triangleB );
}
}
return false;
}
} );
return { aIntersections, bIntersections };
}
// Add the barycentric interpolated values fro the triangle into the new attribute data
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$2.a.fromBufferAttribute( attr, i0 ).applyMatrix4( matrixWorld );
_tri$2.b.fromBufferAttribute( attr, i1 ).applyMatrix4( matrixWorld );
_tri$2.c.fromBufferAttribute( attr, i2 ).applyMatrix4( matrixWorld );
pushBarycoordInterpolatedValues( _tri$2.a, _tri$2.b, _tri$2.c, baryCoordTri, 3, arr, invert );
} else if ( key === 'normal' ) {
_tri$2.a.fromBufferAttribute( attr, i0 ).applyNormalMatrix( normalMatrix );
_tri$2.b.fromBufferAttribute( attr, i1 ).applyNormalMatrix( normalMatrix );
_tri$2.c.fromBufferAttribute( attr, i2 ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_tri$2.a.multiplyScalar( - 1 );
_tri$2.b.multiplyScalar( - 1 );
_tri$2.c.multiplyScalar( - 1 );
}
pushBarycoordInterpolatedValues( _tri$2.a, _tri$2.b, _tri$2.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
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
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 ) );
}
}
}
const EPSILON = 1e-14;
const COPLANAR_EPSILON = 1e-10;
const _edge = new Line3();
const _foundEdge = new Line3();
const _vec$1 = new Vector3();
const _planeNormal = new Vector3();
const _plane$1 = new Plane();
const _exTriangle = new ExtendedTriangle();
function isTriDegenerate( tri ) {
return tri.a.distanceToSquared( tri.b ) < EPSILON ||
tri.a.distanceToSquared( tri.c ) < EPSILON ||
tri.b.distanceToSquared( tri.c ) < EPSILON;
}
// A pool of triangles to avoid unnecessary triangle creation
class TrianglePool {
constructor() {
this._pool = [];
this._index = 0;
}
getTriangle() {
if ( this._index >= this._pool.length ) {
this._pool.push( new Triangle() );
}
return this._pool[ this._index ++ ];
}
clear() {
this._index = 0;
}
reset() {
this._pool.length = 0;
this._index = 0;
}
}
// Utility class for splitting triangles
class TriangleSplitter {
constructor() {
this.trianglePool = new TrianglePool();
this.triangles = [];
this.normal = new Vector3();
}
// initialize the class with a triangle
initialize( tri ) {
const { triangles, trianglePool, normal } = this;
triangles.length = 0;
trianglePool.clear();
if ( Array.isArray( tri ) ) {
for ( let i = 0, l = tri.length; i < l; i ++ ) {
const t = tri[ i ];
if ( i === 0 ) {
t.getNormal( normal );
} else if ( Math.abs( 1.0 - t.getNormal( _vec$1 ).dot( normal ) ) > EPSILON ) {
throw new Error( 'Triangle Splitter: Cannot initialize with triangles that have different normals.' );
}
const poolTri = trianglePool.getTriangle();
poolTri.copy( t );
triangles.push( poolTri );
}
} else {
tri.getNormal( normal );
const poolTri = trianglePool.getTriangle();
poolTri.copy( tri );
triangles.push( poolTri );
}
}
// Split the current set of triangles by passing a single triangle in. If the triangle is
// coplanar it will attempt to split by the triangle edge planes
splitByTriangle( triangle ) {
const { normal, triangles } = this;
triangle.getPlane( _plane$1 );
if ( Math.abs( 1.0 - Math.abs( _plane$1.normal.dot( normal ) ) ) < COPLANAR_EPSILON ) {
// if the triangle is coplanar then split by the edge planes
const arr = [ triangle.a, triangle.b, triangle.c ];
for ( let i = 0; i < 3; i ++ ) {
const nexti = ( i + 1 ) % 3;
const v0 = arr[ i ];
const v1 = arr[ nexti ];
_vec$1.subVectors( v1, v0 ).normalize();
_planeNormal.crossVectors( normal, _vec$1 );
_plane$1.setFromNormalAndCoplanarPoint( _planeNormal, v0 );
this.splitByPlane( _plane$1, triangle, i );
}
for ( let i = 0, l = triangles.length; i < l; i ++ ) {
const t = triangles[ i ];
t.coplanarCount = 0;
}
} else {
// otherwise split by the triangle plane
this.splitByPlane( _plane$1, triangle );
}
}
// Split the triangles by the given plan. If a triangle is provided then we ensure we
// intersect the triangle before splitting the plane
splitByPlane( plane, triangle = null, coplanarIndex = - 1 ) {
const { triangles, trianglePool } = this;
// init our triangle to check for intersection
let splittingTriangle = null;
if ( triangle !== null ) {
splittingTriangle = _exTriangle;
splittingTriangle.copy( triangle );
splittingTriangle.needsUpdate = true;
}
// try to split every triangle in the class
for ( let i = 0, l = triangles.length; i < l; i ++ ) {
const tri = triangles[ i ];
const { a, b, c } = tri;
// skip the triangle if we don't intersect with it
if ( splittingTriangle ) {
if ( ! splittingTriangle.intersectsTriangle( tri, _edge, true ) ) {
continue;
}
}
let intersects = 0;
let vertexSplitEnd = - 1;
let positiveSide = 0;
let coplanarEdge = false;
const arr = [ a, b, c ];
for ( let t = 0; t < 3; t ++ ) {
// get the triangle edge
const tNext = ( t + 1 ) % 3;
_edge.start.copy( arr[ t ] );
_edge.end.copy( arr[ tNext ] );
// track if the start point sits on the plane or if it's on the positive side of it
// so we can use that information to determine whether to split later.
const startDist = plane.distanceToPoint( _edge.start );
const endDist = plane.distanceToPoint( _edge.end );
if ( Math.abs( startDist ) < COPLANAR_EPSILON && Math.abs( endDist ) < COPLANAR_EPSILON ) {
coplanarEdge = true;
break;
}
// we only don't consider this an intersection if the start points hits the plane
if ( Math.abs( startDist ) < COPLANAR_EPSILON ) {
continue;
}
if ( startDist > 0 ) {
positiveSide ++;
}
// double check the end point since the "intersectLine" function sometimes does not
// return it as an intersection (see issue #28)
// Because we ignore the start point intersection above we have to make sure we check the end
// point intersection here.
let didIntersect = ! ! plane.intersectLine( _edge, _vec$1 );
if ( ! didIntersect && Math.abs( endDist ) < COPLANAR_EPSILON ) {
_vec$1.copy( _edge.end );
didIntersect = true;
}
// check if we intersect the plane (ignoring the start point so we don't double count)
if ( didIntersect && ! ( _vec$1.distanceTo( _edge.start ) < EPSILON ) ) {
// if we intersect at the end point then we track that point as one that we
// have to split down the middle
if ( _vec$1.distanceTo( _edge.end ) < EPSILON ) {
vertexSplitEnd = t;
}
// track the split edge
if ( intersects === 0 ) {
_foundEdge.start.copy( _vec$1 );
} else {
_foundEdge.end.copy( _vec$1 );
}
intersects ++;
}
}
if ( coplanarEdge ) {
continue;
}
// skip splitting if:
// - we have two points on the plane then the plane intersects the triangle exactly on an edge
// - the plane does not intersect on 2 points
// - the intersection edge is too small
if ( intersects === 2 && _foundEdge.distance() > COPLANAR_EPSILON ) {
if ( vertexSplitEnd !== - 1 ) {
vertexSplitEnd = ( vertexSplitEnd + 1 ) % 3;
// we're splitting along a vertex
let otherVert1 = 0;
if ( otherVert1 === vertexSplitEnd ) otherVert1 = ( otherVert1 + 1 ) % 3;
let otherVert2 = otherVert1 + 1;
if ( otherVert2 === vertexSplitEnd ) otherVert2 = ( otherVert2 + 1 ) % 3;
const nextTri = trianglePool.getTriangle();
nextTri.a.copy( arr[ otherVert2 ] );
nextTri.b.copy( _foundEdge.end );
nextTri.c.copy( _foundEdge.start );
if ( ! isTriDegenerate( nextTri ) ) {
triangles.push( nextTri );
}
tri.a.copy( arr[ otherVert1 ] );
tri.b.copy( _foundEdge.start );
tri.c.copy( _foundEdge.end );
if ( isTriDegenerate( tri ) ) {
triangles.splice( i, 1 );
i --;
l --;
}
} else {
// we're splitting with a quad and a triangle
const singleVert = arr.findIndex( v => {
if ( positiveSide >= 2 ) {
return plane.distanceToPoint( v ) < 0;
} else {
return plane.distanceToPoint( v ) > 0;
}
} );
if ( singleVert === 0 ) {
let tmp = _foundEdge.start;
_foundEdge.start = _foundEdge.end;
_foundEdge.end = tmp;
} else if ( singleVert === - 1 ) {
continue;
}
const nextVert1 = ( singleVert + 1 ) % 3;
const nextVert2 = ( singleVert + 2 ) % 3;
const nextTri1 = trianglePool.getTriangle();
const nextTri2 = trianglePool.getTriangle();
// choose the triangle that has the larger areas (shortest split distance)
if ( arr[ nextVert1 ].distanceToSquared( _foundEdge.start ) < arr[ nextVert2 ].distanceToSquared( _foundEdge.end ) ) {
nextTri1.a.copy( arr[ nextVert1 ] );
nextTri1.b.copy( _foundEdge.start );
nextTri1.c.copy( _foundEdge.end );
nextTri2.a.copy( arr[ nextVert1 ] );
nextTri2.b.copy( arr[ nextVert2 ] );
nextTri2.c.copy( _foundEdge.start );
} else {
nextTri1.a.copy( arr[ nextVert2 ] );
nextTri1.b.copy( _foundEdge.start );
nextTri1.c.copy( _foundEdge.end );
nextTri2.a.copy( arr[ nextVert1 ] );
nextTri2.b.copy( arr[ nextVert2 ] );
nextTri2.c.copy( _foundEdge.end );
}
tri.a.copy( arr[ singleVert ] );
tri.b.copy( _foundEdge.end );
tri.c.copy( _foundEdge.start );
// don't add degenerate triangles to the list
if ( ! isTriDegenerate( nextTri1 ) ) {
triangles.push( nextTri1 );
}
if ( ! isTriDegenerate( nextTri2 ) ) {
triangles.push( nextTri2 );
}
if ( isTriDegenerate( tri ) ) {
triangles.splice( i, 1 );
i --;
l --;
}
}
} else if ( intersects === 3 ) {
console.warn( 'TriangleClipper: Coplanar clip not handled' );
}
}
}
reset() {
this.triangles.length = 0;
}
}
// Make a new array wrapper class that more easily affords expansion when reaching it's max capacity
class TypeBackedArray {
constructor( type, initialSize = 500 ) {
const bufferType = areSharedArrayBuffersSupported() ? SharedArrayBuffer : ArrayBuffer;
this.expansionFactor = 1.5;
this.type = type;
this.array = new type( new bufferType( initialSize * type.BYTES_PER_ELEMENT ) );
this.length = 0;
}
expand( size = null ) {
const { type, array, expansionFactor } = this;
if ( size === null ) {
size = ~ ~ ( array.length * expansionFactor );
}
const newArray = new type( size );
newArray.set( array, 0 );
this.array = newArray;
}
push( ...args ) {
let { array, length } = this;
if ( length + args.length > array.length ) {
this.expand();
array = this.array;
}
for ( let i = 0, l = args.length; i < l; i ++ ) {
array[ length + i ] = args[ i ];
}
this.length += args.length;
}
clear() {
this.length = 0;
}
}
// utility class for for tracking attribute data in type-backed arrays
class TypedAttributeData {
constructor() {
this.groupAttributes = [ {} ];
this.groupCount = 0;
}
getType( name ) {
return this.groupAttributes[ 0 ][ name ].type;
}
getTotalLength( name ) {
const { groupCount, groupAttributes } = this;
let length = 0;
for ( let i = 0; i < groupCount; i ++ ) {
const attrSet = groupAttributes[ i ];
length += attrSet[ name ].length;
}
return length;
}
getGroupSet( index = 0 ) {
// throw an error if we've never
const { groupAttributes } = this;
if ( groupAttributes[ index ] ) {
this.groupCount = Math.max( this.groupCount, index + 1 );
return groupAttributes[ index ];
}
// add any new group sets required
const rootAttrSet = groupAttributes[ 0 ];
this.groupCount = Math.max( this.groupCount, index + 1 );
while ( index >= groupAttributes.length ) {
const newAttrSet = {};
groupAttributes.push( newAttrSet );
for ( const key in rootAttrSet ) {
newAttrSet[ key ] = new TypeBackedArray( rootAttrSet[ key ].type );
}
}
return groupAttributes[ index ];
}
getGroupArray( name, index = 0 ) {
// throw an error if we've never
const { groupAttributes } = this;
const rootAttrSet = groupAttributes[ 0 ];
const referenceAttr = rootAttrSet[ name ];
if ( ! referenceAttr ) {
throw new Error( `TypedAttributeData: Attribute with "${ name }" has not been initialized` );
}
return this.getGroupSet( index )[ name ];
}
// initializes an attribute array with the given name, type, and size
initializeArray( name, type ) {
const { groupAttributes } = this;
const rootSet = groupAttributes[ 0 ];
const referenceAttr = rootSet[ name ];
if ( referenceAttr ) {
if ( referenceAttr.type !== type ) {
throw new Error( `TypedAttributeData: Array ${ name } already initialized with a different type.` );
}
} else {
for ( let i = 0, l = groupAttributes.length; i < l; i ++ ) {
groupAttributes[ i ][ name ] = new TypeBackedArray( type );
}
}
}
clear() {
this.groupCount = 0;
const { groupAttributes } = this;
groupAttributes.forEach( attrSet => {
for ( const key in attrSet ) {
attrSet[ key ].clear();
}
} );
}
delete( key ) {
this.groupAttributes.forEach( attrSet => {
delete attrSet[ key ];
} );
}
reset() {
this.groupAttributes = [];
}
}
class TriangleIntersectData {
constructor( tri ) {
this.triangle = new Triangle().copy( tri );
this.intersects = {};
}
addTriangle( index, tri ) {
this.intersects[ index ] = new Triangle().copy( tri );
}
getIntersectArray() {
const array = [];
const { intersects } = this;
for ( const key in intersects ) {
array.push( intersects[ key ] );
}
return array;
}
}
class TriangleIntersectionSets {
constructor() {
this.data = {};
}
addTriangleIntersection( ia, triA, ib, triB ) {
const { data } = this;
if ( ! data[ ia ] ) {
data[ ia ] = new TriangleIntersectData( triA );
}
data[ ia ].addTriangle( ib, triB );
}
getTrianglesAsArray( id = null ) {
const { data } = this;
const arr = [];
if ( id !== null ) {
if ( id in data ) {
arr.push( data[ id ].triangle );
}
} else {
for ( const key in data ) {
arr.push( data[ key ].triangle );
}
}
return arr;
}
getTriangleIndices() {
return Object.keys( this.data ).map( i => parseInt( i ) );
}
getIntersectionIndices( id ) {
const { data } = this;
if ( ! data[ id ] ) {
return [];
} else {
return Object.keys( data[ id ].intersects ).map( i => parseInt( i ) );
}
}
getIntersectionsAsArray( id = null, id2 = null ) {
const { data } = this;
const triSet = new Set();
const arr = [];
const addTriangles = key => {
if ( ! data[ key ] ) return;
if ( id2 !== null ) {
if ( data[ key ].intersects[ id2 ] ) {
arr.push( data[ key ].intersects[ id2 ] );
}
} else {
const intersects = data[ key ].intersects;
for ( const key2 in intersects ) {
if ( ! triSet.has( key2 ) ) {
triSet.add( key2 );
arr.push( intersects[ key2 ] );
}
}
}
};
if ( id !== null ) {
addTriangles( id );
} else {
for ( const key in data ) {
addTriangles( key );
}
}
return arr;
}
reset() {
this.data = {};
}
}
class OperationDebugData {
constructor() {
this.enabled = false;
this.triangleIntersectsA = new TriangleIntersectionSets();
this.triangleIntersectsB = new TriangleIntersectionSets();
this.intersectionEdges = [];
}
addIntersectingTriangles( ia, triA, ib, triB ) {
const { triangleIntersectsA, triangleIntersectsB } = this;
triangleIntersectsA.addTriangleIntersection( ia, triA, ib, triB );
triangleIntersectsB.addTriangleIntersection( ib, triB, ia, triA );
}
addEdge( edge ) {
this.intersectionEdges.push( edge.clone() );
}
reset() {
this.triangleIntersectsA.reset();
this.triangleIntersectsB.reset();
this.intersectionEdges = [];
}
}
const _matrix$1 = new Matrix4();
const _normalMatrix = new Matrix3();
const _triA = new Triangle();
const _triB = new Triangle();
const _tri$1 = 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.
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$1
.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$1 );
_triA.b.fromBufferAttribute( aPosition, ia1 ).applyMatrix4( _matrix$1 );
_triA.c.fromBufferAttribute( aPosition, ia2 ).applyMatrix4( _matrix$1 );
// 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$1
.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$1.a.fromBufferAttribute( aPosition, i0 ).applyMatrix4( _matrix$1 );
_tri$1.b.fromBufferAttribute( aPosition, i1 ).applyMatrix4( _matrix$1 );
_tri$1.c.fromBufferAttribute( aPosition, i2 ).applyMatrix4( _matrix$1 );
// get the side and decide if we need to cull the triangle based on the operation
const hitSide = getHitSide( _tri$1, 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 );
}
}
}
// applies the given set of attribute data to the provided geometry. If the attributes are
// not large enough to hold the new set of data then new attributes will be created. Otherwise
// the existing attributes will be used and draw range updated to accommodate the new size.
function applyToGeometry( geometry, referenceGeometry, groups, attributeInfo ) {
let needsDisposal = false;
let drawRange = - 1;
const groupCount = attributeInfo.groupCount;
// set the data
const attributes = geometry.attributes;
const rootAttrSet = attributeInfo.groupAttributes[ 0 ];
for ( const key in rootAttrSet ) {
const requiredLength = attributeInfo.getTotalLength( key, groupCount );
const type = rootAttrSet[ key ].type;
let attr = attributes[ key ];
if ( ! attr || attr.array.length < requiredLength ) {
// create the attribute if it doesn't exist yet
const refAttr = referenceGeometry.attributes[ key ];
attr = new BufferAttribute( new type( requiredLength ), refAttr.itemSize, refAttr.normalized );
geometry.setAttribute( key, attr );
needsDisposal = true;
}
let offset = 0;
for ( let i = 0; i < groupCount; i ++ ) {
const { array, type, length } = attributeInfo.groupAttributes[ i ][ key ];
const trimmedArray = new type( array.buffer, 0, length );
attr.array.set( trimmedArray, offset );
offset += trimmedArray.length;
}
attr.needsUpdate = true;
drawRange = requiredLength / attr.itemSize;
}
// update the draw range
geometry.setDrawRange( 0, drawRange );
geometry.clearGroups();
let groupOffset = 0;
for ( let i = 0; i < groupCount; i ++ ) {
const posCount = attributeInfo.getGroupArray( 'position', i ).length / 3;
if ( posCount !== 0 ) {
const group = groups[ i ];
geometry.addGroup( groupOffset, posCount, group.materialIndex );
groupOffset += posCount;
}
}
// remove or update the index appropriately
if ( geometry.index ) {
const indexArray = geometry.index.array;
if ( indexArray.length < drawRange ) {
geometry.index = null;
needsDisposal = true;
} else {
for ( let i = 0, l = indexArray.length; i < l; i ++ ) {
indexArray[ i ] = i;
}
}
}
// remove the bounds tree if it exists because its now out of date
// TODO: can we have this dispose in the same way that a brush does?
geometry.boundsTree = null;
if ( needsDisposal ) {
geometry.dispose();
}
return geometry;
}
function getMaterialList( groups, materials ) {
let result = materials;
if ( ! Array.isArray( materials ) ) {
result = [];
groups.forEach( g => {
result[ g.materialIndex ] = materials;
} );
}
return result;
}
// Utility class for performing CSG operations
class Evaluator {
constructor() {
this.triangleSplitter = new TriangleSplitter();
this.attributeData = new TypedAttributeData();
this.attributes = [ 'position', 'uv', 'normal' ];
this.useGroups = true;
this.debug = new OperationDebugData();
}
evaluate( a, b, operation, targetBrush = new Brush() ) {
a.prepareGeometry();
b.prepareGeometry();
const { triangleSplitter, attributeData, attributes, useGroups, debug } = this;
const targetGeometry = targetBrush.geometry;
const aAttributes = a.geometry.attributes;
for ( let i = 0, l = attributes.length; i < l; i ++ ) {
const key = attributes[ i ];
const attr = aAttributes[ key ];
attributeData.initializeArray( key, attr.array.constructor );
}
for ( const key in attributeData.attributes ) {
if ( ! attributes.includes( key ) ) {
attributeData.delete( key );
}
}
for ( const key in targetGeometry.attributes ) {
if ( ! attributes.includes( key ) ) {
targetGeometry.deleteAttribute( key );
targetGeometry.dispose();
}
}
attributeData.clear();
if ( debug.enabled ) {
debug.reset();
setDebugContext( debug );
}
performOperation( a, b, operation, triangleSplitter, attributeData, { useGroups } );
if ( debug.enabled ) {
setDebugContext( null );
}
// structure the groups appropriately
const aGroups = ! useGroups || a.geometry.groups.length === 0 ?
[ { start: 0, count: Infinity, materialIndex: 0 } ] :
a.geometry.groups.map( group => ( { ...group } ) );
const bGroups = ! useGroups || b.geometry.groups.length === 0 ?
[ { start: 0, count: Infinity, materialIndex: 0 } ] :
b.geometry.groups.map( group => ( { ...group } ) );
// get the materials
const aMaterials = getMaterialList( aGroups, a.material );
const bMaterials = getMaterialList( bGroups, b.material );
// adjust the material index
bGroups.forEach( g => {
g.materialIndex += aMaterials.length;
} );
// apply groups and attribute data to the geometry
applyToGeometry( targetGeometry, a.geometry, [ ...aGroups, ...bGroups ], attributeData );
// generate the minimum set of materials needed for the list of groups and adjust the groups
// if they're needed
const groups = targetGeometry.groups;
if ( useGroups ) {
const materialMap = new Map();
const allMaterials = [ ...aMaterials, ...bMaterials ];
// create a map from old to new index and remove materials that aren't used
let newIndex = 0;
for ( let i = 0, l = allMaterials.length; i < l; i ++ ) {
const foundGroup = Boolean( groups.find( group => group.materialIndex === i ) );
if ( ! foundGroup ) {
allMaterials[ i ] = null;
} else {
materialMap.set( i, newIndex );
newIndex ++;
}
}
// adjust the groups indices
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const group = groups[ i ];
group.materialIndex = materialMap.get( group.materialIndex );
}
targetBrush.material = allMaterials.filter( material => material );
}
return targetBrush;
}
evaluateHierarchy( root, target = new Brush() ) {
root.updateMatrixWorld( true );
const flatTraverse = ( obj, cb ) => {
const children = obj.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
const child = children[ i ];
if ( child.isOperationGroup ) {
flatTraverse( child, cb );
} else {
cb( child );
}
}
};
const traverse = brush => {
const children = brush.children;
let didChange = false;
for ( let i = 0, l = children.length; i < l; i ++ ) {
const child = children[ i ];
didChange = traverse( child ) || didChange;
}
const isDirty = brush.isDirty();
if ( isDirty ) {
brush.markUpdated();
}
if ( didChange && ! brush.isOperationGroup ) {
let result;
flatTraverse( brush, child => {
if ( ! result ) {
result = this.evaluate( brush, child, child.operation );
} else {
result = this.evaluate( result, child, child.operation );
}
} );
brush._cachedGeometry = result.geometry;
brush._cachedMaterials = result.material;
return true;
} else {
return didChange || isDirty;
}
};
traverse( root );
target.geometry = root._cachedGeometry;
target.material = root._cachedMaterials;
return target;
}
reset() {
this.triangleSplitter.reset();
}
}
class Operation extends Brush {
constructor( ...args ) {
super( ...args );
this.isOperation = true;
this.operation = ADDITION;
this._cachedGeometry = new BufferGeometry();
this._cachedMaterials = null;
this._previousOperation = null;
}
markUpdated() {
super.markUpdated();
this._previousOperation = this.operation;
}
isDirty() {
return this.operation !== this._previousOperation || super.isDirty();
}
insertBefore( brush ) {
const parent = this.parent;
const index = parent.children.indexOf( this );
parent.children.splice( index, 0, brush );
}
insertAfter( brush ) {
const parent = this.parent;
const index = parent.children.indexOf( this );
parent.children.splice( index + 1, 0, brush );
}
}
class OperationGroup extends Group {
constructor() {
super();
this.isOperationGroup = true;
this._previousMatrix = new Matrix4();
}
markUpdated() {
this._previousMatrix.copy( this.matrix );
}
isDirty() {
const { matrix, _previousMatrix } = this;
const el1 = matrix.elements;
const el2 = _previousMatrix.elements;
for ( let i = 0; i < 16; i ++ ) {
if ( el1[ i ] !== el2[ i ] ) {
return true;
}
}
return false;
}
}
function addWorldPosition( shader ) {
if ( /varying\s+vec3\s+wPosition/.test( shader.vertexShader ) ) return;
shader.vertexShader = `
varying vec3 wPosition;
${shader.vertexShader}
`.replace(
/#include <displacementmap_vertex>/,
v =>
`${v}
wPosition = (modelMatrix * vec4( transformed, 1.0 )).xyz;
`,
);
shader.fragmentShader = `
varying vec3 wPosition;
${shader.fragmentShader}
`;
return shader;
}
function csgGridShaderMixin( shader ) {
shader.uniforms = {
...shader.uniforms,
checkerboardColor: { value: new Color( 0x111111 ) }
};
addWorldPosition( shader );
shader.defines = { CSG_GRID: 1 };
shader.fragmentShader = shader.fragmentShader.replace(
/#include <common>/,
v =>
/* glsl */`
${v}
uniform vec3 checkerboardColor;
float getCheckerboard( vec2 p, float scale ) {
p /= scale;
p += vec2( 0.5 );
vec2 line = mod( p, 2.0 ) - vec2( 1.0 );
line = abs( line );
vec2 pWidth = fwidth( line );
vec2 value = smoothstep( 0.5 - pWidth / 2.0, 0.5 + pWidth / 2.0, line );
float result = value.x * value.y + ( 1.0 - value.x ) * ( 1.0 - value.y );
return result;
}
float getGrid( vec2 p, float scale, float thickness ) {
p /= 0.5 * scale;
vec2 stride = mod( p, 2.0 ) - vec2( 1.0 );
stride = abs( stride );
vec2 pWidth = fwidth( p );
vec2 line = smoothstep( 1.0 - pWidth / 2.0, 1.0 + pWidth / 2.0, stride + thickness * pWidth );
return max( line.x, line.y );
}
vec3 getFaceColor( vec2 p, vec3 color ) {
float checkLarge = getCheckerboard( p, 1.0 );
float checkSmall = abs( getCheckerboard( p, 0.1 ) );
float lines = getGrid( p, 10.0, 1.0 );
vec3 checkColor = mix(
vec3( 0.7 ) * color,
vec3( 1.0 ) * color,
checkSmall * 0.4 + checkLarge * 0.6
);
vec3 gridColor = vec3( 1.0 );
return mix( checkColor, gridColor, lines );
}
float angleBetween( vec3 a, vec3 b ) {
return acos( abs( dot( a, b ) ) );
}
vec3 planeProject( vec3 norm, vec3 other ) {
float d = dot( norm, other );
return normalize( other - norm * d );
}
vec3 getBlendFactors( vec3 norm ) {
vec3 xVec = vec3( 1.0, 0.0, 0.0 );
vec3 yVec = vec3( 0.0, 1.0, 0.0 );
vec3 zVec = vec3( 0.0, 0.0, 1.0 );
vec3 projX = planeProject( xVec, norm );
vec3 projY = planeProject( yVec, norm );
vec3 projZ = planeProject( zVec, norm );
float xAngle = max(
angleBetween( xVec, projY ),
angleBetween( xVec, projZ )
);
float yAngle = max(
angleBetween( yVec, projX ),
angleBetween( yVec, projZ )
);
float zAngle = max(
angleBetween( zVec, projX ),
angleBetween( zVec, projY )
);
return vec3( xAngle, yAngle, zAngle ) / ( 0.5 * PI );
}
` ).replace(
/#include <normal_fragment_maps>/,
v =>
/* glsl */`${v}
#if CSG_GRID
{
vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
float yCont = abs( dot( vec3( 0.0, 1.0, 0.0 ), worldNormal ) );
float zCont = abs( dot( vec3( 0.0, 0.0, 1.0 ), worldNormal ) );
float xCont = abs( dot( vec3( 1.0, 0.0, 0.0 ), worldNormal ) );
vec3 factors = getBlendFactors( worldNormal );
factors = smoothstep( vec3( 0.475 ), vec3( 0.525 ), vec3( 1.0 ) - factors );
float weight = factors.x + factors.y + factors.z;
factors /= weight;
vec3 color =
getFaceColor( wPosition.yz, diffuseColor.rgb ) * factors.x +
getFaceColor( wPosition.xz, diffuseColor.rgb ) * factors.y +
getFaceColor( wPosition.xy, diffuseColor.rgb ) * factors.z;
diffuseColor.rgb = color;
}
#endif
`,
);
return shader;
}
class GridMaterial extends MeshPhongMaterial {
get enableGrid() {
return Boolean( this._enableGrid );
}
set enableGrid( v ) {
if ( this._enableGrid !== v ) {
this._enableGrid = v;
this.needsUpdate = true;
}
}
constructor( ...args ) {
super( ...args );
this.enableGrid = true;
}
o