diff --git a/examples/files.json b/examples/files.json index 6294a84efbf8fc..f084c03eec21e8 100644 --- a/examples/files.json +++ b/examples/files.json @@ -197,6 +197,7 @@ "webgl_renderer_pathtracer", "webgl_refraction", "webgl_rtt", + "webgl_sculpt", "webgl_shader", "webgl_shader_lava", "webgl_shaders_ocean", @@ -527,7 +528,8 @@ "webxr_xr_dragging_custom_depth", "webxr_xr_haptics", "webxr_xr_marchingcubes", - "webxr_xr_paint" + "webxr_xr_paint", + "webxr_xr_sculpt" ], "games": [ "games_fps" diff --git a/examples/jsm/sculpt/Sculpt.js b/examples/jsm/sculpt/Sculpt.js new file mode 100644 index 00000000000000..cd3cd4e9e57faf --- /dev/null +++ b/examples/jsm/sculpt/Sculpt.js @@ -0,0 +1,680 @@ +// Ported from SculptGL by Stéphane Ginier +// https://github.com/stephomi/sculptgl + +import { + BufferAttribute, + Matrix4, + Vector3 +} from 'three'; + +import { + Flags, + getMemory, + sub, + sqrLen, + sqrDist, + intersectionRayTriangle, + vertexOnLine +} from './SculptUtils.js'; + +import { SculptMesh } from './SculptMesh.js'; + +import { + subdivisionPass, + decimationPass, + getFrontVertices, + areaNormal, + areaCenter, + toolFlatten, + toolInflate, + toolSmooth, + toolPinch, + toolCrease, + toolDrag, + toolScale +} from './SculptTools.js'; + +// ---- Main Sculpt Class ---- + +const _v3NearLocal = new Vector3(); +const _v3FarLocal = new Vector3(); +const _matInverse = new Matrix4(); +const _tmpInter = [ 0, 0, 0 ]; +const _tmpV1 = [ 0, 0, 0 ]; +const _tmpV2 = [ 0, 0, 0 ]; +const _tmpV3 = [ 0, 0, 0 ]; +const _v3Temp = new Vector3(); + +class Sculpt { + + constructor( mesh, camera, domElement ) { + + this.tool = 'brush'; + this.size = 50; + this.strength = 0.5; + this.negative = false; + this.subdivision = 0.75; + this.decimation = 0.75; + + this._mesh = mesh; + this._camera = camera; + this._domElement = domElement; + + this._sculptMesh = new SculptMesh(); + this._sculptMesh.initFromGeometry( mesh.geometry ); + + // Sync geometry once at init + this._syncGeometry(); + + this._sculpting = false; + this._lastMouseX = 0; + this._lastMouseY = 0; + this._pickedFace = - 1; + this._interPoint = [ 0, 0, 0 ]; + this._eyeDir = [ 0, 0, 0 ]; + this._rLocal2 = 0; + this._pickedNormal = [ 0, 0, 0 ]; + this._dragDir = [ 0, 0, 0 ]; + this._scalePrevX = 0; + this._scaleDelta = 0; + this._cachedRect = null; + + // Bind event handlers + this._onPointerDown = this._onPointerDown.bind( this ); + this._onPointerMove = this._onPointerMove.bind( this ); + this._onPointerUp = this._onPointerUp.bind( this ); + + domElement.addEventListener( 'pointerdown', this._onPointerDown ); + domElement.addEventListener( 'pointermove', this._onPointerMove ); + domElement.addEventListener( 'pointerup', this._onPointerUp ); + + } + + dispose() { + + this._domElement.removeEventListener( 'pointerdown', this._onPointerDown ); + this._domElement.removeEventListener( 'pointermove', this._onPointerMove ); + this._domElement.removeEventListener( 'pointerup', this._onPointerUp ); + + } + + // ---- Picking ---- + + _getRect() { + + if ( this._cachedRect === null ) { + + this._cachedRect = this._domElement.getBoundingClientRect(); + + } + + return this._cachedRect; + + } + + _unproject( mouseX, mouseY, z ) { + + const rect = this._getRect(); + const x = ( ( mouseX - rect.left ) / rect.width ) * 2 - 1; + const y = - ( ( mouseY - rect.top ) / rect.height ) * 2 + 1; + _v3Temp.set( x, y, z ).unproject( this._camera ); + return [ _v3Temp.x, _v3Temp.y, _v3Temp.z ]; + + } + + _project( point ) { + + _v3Temp.set( point[ 0 ], point[ 1 ], point[ 2 ] ).project( this._camera ); + const rect = this._getRect(); + return [ + ( _v3Temp.x * 0.5 + 0.5 ) * rect.width + rect.left, + ( - _v3Temp.y * 0.5 + 0.5 ) * rect.height + rect.top, + _v3Temp.z + ]; + + } + + _pickClosestFace( near, eyeDir ) { + + const sm = this._sculptMesh; + const iFacesCandidates = sm.intersectRay( near, eyeDir ); + const vAr = sm.getVertices(); + const fAr = sm.getFaces(); + let distance = Infinity; + this._pickedFace = - 1; + + for ( let i = 0, l = iFacesCandidates.length; i < l; ++ i ) { + + const indFace = iFacesCandidates[ i ] * 4; + const ind1 = fAr[ indFace ] * 3, ind2 = fAr[ indFace + 1 ] * 3, ind3 = fAr[ indFace + 2 ] * 3; + _tmpV1[ 0 ] = vAr[ ind1 ]; _tmpV1[ 1 ] = vAr[ ind1 + 1 ]; _tmpV1[ 2 ] = vAr[ ind1 + 2 ]; + _tmpV2[ 0 ] = vAr[ ind2 ]; _tmpV2[ 1 ] = vAr[ ind2 + 1 ]; _tmpV2[ 2 ] = vAr[ ind2 + 2 ]; + _tmpV3[ 0 ] = vAr[ ind3 ]; _tmpV3[ 1 ] = vAr[ ind3 + 1 ]; _tmpV3[ 2 ] = vAr[ ind3 + 2 ]; + const hitDist = intersectionRayTriangle( near, eyeDir, _tmpV1, _tmpV2, _tmpV3, _tmpInter ); + if ( hitDist >= 0 && hitDist < distance ) { + + distance = hitDist; + this._interPoint[ 0 ] = _tmpInter[ 0 ]; + this._interPoint[ 1 ] = _tmpInter[ 1 ]; + this._interPoint[ 2 ] = _tmpInter[ 2 ]; + this._pickedFace = iFacesCandidates[ i ]; + + } + + } + + return this._pickedFace !== - 1; + + } + + _intersectionRayMesh( mouseX, mouseY ) { + + const vNear = this._unproject( mouseX, mouseY, 0 ); + const vFar = this._unproject( mouseX, mouseY, 0.1 ); + + // Transform to local space + _matInverse.copy( this._mesh.matrixWorld ).invert(); + _v3NearLocal.set( vNear[ 0 ], vNear[ 1 ], vNear[ 2 ] ).applyMatrix4( _matInverse ); + _v3FarLocal.set( vFar[ 0 ], vFar[ 1 ], vFar[ 2 ] ).applyMatrix4( _matInverse ); + + const near = [ _v3NearLocal.x, _v3NearLocal.y, _v3NearLocal.z ]; + const far = [ _v3FarLocal.x, _v3FarLocal.y, _v3FarLocal.z ]; + const eyeDir = this._eyeDir; + sub( eyeDir, far, near ); + const len = Math.sqrt( sqrLen( eyeDir ) ); + eyeDir[ 0 ] /= len; eyeDir[ 1 ] /= len; eyeDir[ 2 ] /= len; + + if ( ! this._pickClosestFace( near, eyeDir ) ) return false; + + this._updateLocalAndWorldRadius2(); + return true; + + } + + _updateLocalAndWorldRadius2() { + + // Transform intersection to world space + const ip = this._interPoint; + _v3Temp.set( ip[ 0 ], ip[ 1 ], ip[ 2 ] ).applyMatrix4( this._mesh.matrixWorld ); + const wx = _v3Temp.x, wy = _v3Temp.y, wz = _v3Temp.z; + + const screenInter = this._project( [ wx, wy, wz ] ); + const offsetX = this.size; + const worldPoint = this._unproject( screenInter[ 0 ] + offsetX, screenInter[ 1 ], screenInter[ 2 ] ); + const rWorld2 = sqrDist( [ wx, wy, wz ], worldPoint ); + + // Convert to local space + const m = this._mesh.matrixWorld.elements; + const scale2 = m[ 0 ] * m[ 0 ] + m[ 4 ] * m[ 4 ] + m[ 8 ] * m[ 8 ]; + this._rLocal2 = rWorld2 / scale2; + + } + + _pickVerticesInSphere( rLocal2 ) { + + const sm = this._sculptMesh; + const vAr = sm.getVertices(); + const vertSculptFlags = sm.getVerticesSculptFlags(); + const inter = this._interPoint; + const iFacesInCells = sm.intersectSphere( inter, rLocal2 ); + const iVerts = sm.getVerticesFromFaces( iFacesInCells ); + const nbVerts = iVerts.length; + const sculptFlag = ++ Flags.SCULPT; + const pickedVertices = new Uint32Array( getMemory( 4 * nbVerts ), 0, nbVerts ); + let acc = 0; + const itx = inter[ 0 ], ity = inter[ 1 ], itz = inter[ 2 ]; + for ( let i = 0; i < nbVerts; ++ i ) { + + const ind = iVerts[ i ]; + const j = ind * 3; + const dx = itx - vAr[ j ], dy = ity - vAr[ j + 1 ], dz = itz - vAr[ j + 2 ]; + if ( dx * dx + dy * dy + dz * dz < rLocal2 ) { + + vertSculptFlags[ ind ] = sculptFlag; + pickedVertices[ acc ++ ] = ind; + + } + + } + + return new Uint32Array( pickedVertices.subarray( 0, acc ) ); + + } + + _computePickedNormal() { + + const sm = this._sculptMesh; + const fAr = sm.getFaces(); + const vAr = sm.getVertices(); + const nAr = sm.getNormals(); + const id = this._pickedFace * 4; + const iv1 = fAr[ id ] * 3, iv2 = fAr[ id + 1 ] * 3, iv3 = fAr[ id + 2 ] * 3; + + const d1 = 1.0 / Math.max( 1e-10, Math.sqrt( sqrDist( this._interPoint, [ vAr[ iv1 ], vAr[ iv1 + 1 ], vAr[ iv1 + 2 ] ] ) ) ); + const d2 = 1.0 / Math.max( 1e-10, Math.sqrt( sqrDist( this._interPoint, [ vAr[ iv2 ], vAr[ iv2 + 1 ], vAr[ iv2 + 2 ] ] ) ) ); + const d3 = 1.0 / Math.max( 1e-10, Math.sqrt( sqrDist( this._interPoint, [ vAr[ iv3 ], vAr[ iv3 + 1 ], vAr[ iv3 + 2 ] ] ) ) ); + const invSum = 1.0 / ( d1 + d2 + d3 ); + + this._pickedNormal[ 0 ] = ( nAr[ iv1 ] * d1 + nAr[ iv2 ] * d2 + nAr[ iv3 ] * d3 ) * invSum; + this._pickedNormal[ 1 ] = ( nAr[ iv1 + 1 ] * d1 + nAr[ iv2 + 1 ] * d2 + nAr[ iv3 + 1 ] * d3 ) * invSum; + this._pickedNormal[ 2 ] = ( nAr[ iv1 + 2 ] * d1 + nAr[ iv2 + 2 ] * d2 + nAr[ iv3 + 2 ] * d3 ) * invSum; + const len = Math.sqrt( sqrLen( this._pickedNormal ) ); + if ( len > 0 ) { this._pickedNormal[ 0 ] /= len; this._pickedNormal[ 1 ] /= len; this._pickedNormal[ 2 ] /= len; } + + } + + // ---- Dynamic topology ---- + + _dynamicTopology( iVerts ) { + + const sm = this._sculptMesh; + const subFactor = this.subdivision; + const decFactor = this.decimation; + if ( subFactor === 0 && decFactor === 0 ) return iVerts; + if ( iVerts.length === 0 ) { + + iVerts = sm.getVerticesFromFaces( [ this._pickedFace ] ); + + } + + let iFaces = sm.getFacesFromVertices( iVerts ); + const radius2 = this._rLocal2; + const center = this._interPoint; + const d2Max = radius2 * ( 1.1 - subFactor ) * 0.2; + const d2Min = ( d2Max / 4.2025 ) * decFactor; + + if ( subFactor ) iFaces = subdivisionPass( sm, iFaces, center, radius2, d2Max ); + if ( decFactor ) iFaces = decimationPass( sm, iFaces, center, radius2, d2Min ); + + iVerts = sm.getVerticesFromFaces( iFaces ); + const nbVerts = iVerts.length; + const sculptFlag = Flags.SCULPT; + const vscf = sm.getVerticesSculptFlags(); + const iVertsInRadius = new Uint32Array( getMemory( nbVerts * 4 ), 0, nbVerts ); + let acc = 0; + for ( let i = 0; i < nbVerts; ++ i ) { + + const iVert = iVerts[ i ]; + if ( vscf[ iVert ] === sculptFlag ) iVertsInRadius[ acc ++ ] = iVert; + + } + + const result = new Uint32Array( iVertsInRadius.subarray( 0, acc ) ); + sm.updateTopology( iFaces, iVerts ); + sm._updateGeometry( iFaces, iVerts ); + return result; + + } + + // ---- Ray-based API (for XR / programmatic use) ---- + + _intersectionFromRay( worldOrigin, worldDirection, worldRadius ) { + + // Transform ray to local space + _matInverse.copy( this._mesh.matrixWorld ).invert(); + _v3NearLocal.copy( worldOrigin ).applyMatrix4( _matInverse ); + _v3FarLocal.copy( worldDirection ).transformDirection( _matInverse ).normalize(); + + const near = [ _v3NearLocal.x, _v3NearLocal.y, _v3NearLocal.z ]; + const eyeDir = this._eyeDir; + eyeDir[ 0 ] = _v3FarLocal.x; eyeDir[ 1 ] = _v3FarLocal.y; eyeDir[ 2 ] = _v3FarLocal.z; + + if ( ! this._pickClosestFace( near, eyeDir ) ) return false; + + // Set radius in local space from world radius + const m = this._mesh.matrixWorld.elements; + const scale2 = m[ 0 ] * m[ 0 ] + m[ 4 ] * m[ 4 ] + m[ 8 ] * m[ 8 ]; + this._rLocal2 = ( worldRadius * worldRadius ) / scale2; + + return true; + + } + + strokeFromRay( origin, direction, worldRadius ) { + + if ( ! this._intersectionFromRay( origin, direction, worldRadius ) ) return false; + this._applyStroke(); + this._syncGeometry(); + return true; + + } + + endStroke() { + + this._sculptMesh.balanceOctree(); + + } + + // ---- Stroke pipeline ---- + + _applyStroke() { + + const rLocal2 = this._rLocal2; + let iVerts = this._pickVerticesInSphere( rLocal2 ); + + const sm = this._sculptMesh; + const tool = this.tool; + + // Compute before dynamic topology changes the picked face + if ( tool === 'crease' ) this._computePickedNormal(); + + // Dynamic topology for all tools except scale + if ( tool !== 'scale' ) { + + iVerts = this._dynamicTopology( iVerts ); + + } + + const iVertsFront = getFrontVertices( sm, iVerts, this._eyeDir ); + const center = this._interPoint; + const intensity = this.strength; + const negative = this.negative; + + if ( tool === 'brush' ) { + + const aN = areaNormal( sm, iVertsFront ); + if ( ! aN ) return; + const aC = areaCenter( sm, iVertsFront ); + const off = Math.sqrt( rLocal2 ) * 0.1; + aC[ 0 ] += aN[ 0 ] * ( negative ? - off : off ); + aC[ 1 ] += aN[ 1 ] * ( negative ? - off : off ); + aC[ 2 ] += aN[ 2 ] * ( negative ? - off : off ); + toolFlatten( sm, iVerts, aN, aC, center, rLocal2, intensity, negative ); + + } else if ( tool === 'inflate' ) { + + toolInflate( sm, iVerts, center, rLocal2, intensity, negative ); + + } else if ( tool === 'smooth' ) { + + toolSmooth( sm, iVerts, intensity ); + + } else if ( tool === 'flatten' ) { + + const aN = areaNormal( sm, iVertsFront ); + if ( ! aN ) return; + const aC = areaCenter( sm, iVertsFront ); + toolFlatten( sm, iVerts, aN, aC, center, rLocal2, intensity, negative ); + + } else if ( tool === 'pinch' ) { + + toolPinch( sm, iVerts, center, rLocal2, intensity, negative ); + + } else if ( tool === 'crease' ) { + + const pN = this._pickedNormal; + toolCrease( sm, iVerts, pN, center, rLocal2, intensity, negative ); + + } else if ( tool === 'drag' ) { + + toolDrag( sm, iVerts, center, rLocal2, this._dragDir ); + + } else if ( tool === 'scale' ) { + + toolScale( sm, iVerts, center, rLocal2, this._scaleDelta ); + + } + + // Update geometry + const iFaces = sm.getFacesFromVertices( iVerts ); + sm._updateGeometry( iFaces, iVerts ); + + } + + _makeStroke( mouseX, mouseY ) { + + if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return false; + this._applyStroke(); + return true; + + } + + _sculptStroke( mouseX, mouseY ) { + + const dx = mouseX - this._lastMouseX; + const dy = mouseY - this._lastMouseY; + const dist = Math.sqrt( dx * dx + dy * dy ); + const minSpacing = 0.15 * this.size; + + if ( dist <= minSpacing ) return; + + const step = 1.0 / Math.floor( dist / minSpacing ); + const stepX = dx * step; + const stepY = dy * step; + let mx = this._lastMouseX + stepX; + let my = this._lastMouseY + stepY; + + for ( let i = step; i <= 1.0; i += step ) { + + if ( ! this._makeStroke( mx, my ) ) break; + mx += stepX; + my += stepY; + + } + + this._lastMouseX = mouseX; + this._lastMouseY = mouseY; + + this._syncGeometry(); + + } + + _updateDragDir( mouseX, mouseY ) { + + const vNear = this._unproject( mouseX, mouseY, 0 ); + const vFar = this._unproject( mouseX, mouseY, 0.1 ); + + _matInverse.copy( this._mesh.matrixWorld ).invert(); + _v3NearLocal.set( vNear[ 0 ], vNear[ 1 ], vNear[ 2 ] ).applyMatrix4( _matInverse ); + _v3FarLocal.set( vFar[ 0 ], vFar[ 1 ], vFar[ 2 ] ).applyMatrix4( _matInverse ); + + const near = [ _v3NearLocal.x, _v3NearLocal.y, _v3NearLocal.z ]; + const far = [ _v3FarLocal.x, _v3FarLocal.y, _v3FarLocal.z ]; + + const center = this._interPoint; + const newCenter = vertexOnLine( center, near, far ); + this._dragDir[ 0 ] = newCenter[ 0 ] - center[ 0 ]; + this._dragDir[ 1 ] = newCenter[ 1 ] - center[ 1 ]; + this._dragDir[ 2 ] = newCenter[ 2 ] - center[ 2 ]; + this._interPoint[ 0 ] = newCenter[ 0 ]; + this._interPoint[ 1 ] = newCenter[ 1 ]; + this._interPoint[ 2 ] = newCenter[ 2 ]; + + // Update eye dir + const eyeDir = this._eyeDir; + sub( eyeDir, far, near ); + const len = Math.sqrt( sqrLen( eyeDir ) ); + eyeDir[ 0 ] /= len; eyeDir[ 1 ] /= len; eyeDir[ 2 ] /= len; + + this._updateLocalAndWorldRadius2(); + + } + + _sculptStrokeDrag( mouseX, mouseY ) { + + const sm = this._sculptMesh; + const dx = mouseX - this._lastMouseX; + const dy = mouseY - this._lastMouseY; + const dist = Math.sqrt( dx * dx + dy * dy ); + const minSpacing = 0.15 * this.size; + const step = 1.0 / Math.max( 1, Math.floor( dist / minSpacing ) ); + const stepX = dx * step; + const stepY = dy * step; + let mx = this._lastMouseX; + let my = this._lastMouseY; + + for ( let i = 0.0; i < 1.0; i += step ) { + + mx += stepX; + my += stepY; + this._updateDragDir( mx, my ); + const iVerts = this._pickVerticesInSphere( this._rLocal2 ); + const iVertsDyn = this._dynamicTopology( iVerts ); + toolDrag( sm, iVertsDyn, this._interPoint, this._rLocal2, this._dragDir ); + const iFaces = sm.getFacesFromVertices( iVertsDyn ); + sm._updateGeometry( iFaces, iVertsDyn ); + + } + + this._lastMouseX = mouseX; + this._lastMouseY = mouseY; + this._syncGeometry(); + + } + + _sculptStrokeScale( mouseX, mouseY ) { + + this._scaleDelta = mouseX - this._scalePrevX; + this._scalePrevX = mouseX; + this._applyStroke(); + this._syncGeometry(); + + } + + // ---- Sync back to Three.js ---- + + _syncGeometry() { + + const sm = this._sculptMesh; + const geometry = this._mesh.geometry; + const nbVerts = sm.getNbVertices(); + const nbTris = sm.getNbTriangles(); + + const posAttr = geometry.getAttribute( 'position' ); + const normAttr = geometry.getAttribute( 'normal' ); + const indexAttr = geometry.getIndex(); + + const positions = sm.getVertices().slice( 0, nbVerts * 3 ); + const normals = sm.getNormals().slice( 0, nbVerts * 3 ); + const indices = sm.getTriangles().slice( 0, nbTris * 3 ); + + if ( posAttr && posAttr.count === nbVerts ) { + + posAttr.array.set( positions ); + posAttr.needsUpdate = true; + normAttr.array.set( normals ); + normAttr.needsUpdate = true; + + } else { + + geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) ); + geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); + + } + + if ( indexAttr && indexAttr.count === nbTris * 3 ) { + + indexAttr.array.set( indices ); + indexAttr.needsUpdate = true; + + } else { + + const newIndex = new BufferAttribute( indices, 1 ); + newIndex.version = indexAttr ? indexAttr.version + 1 : 0; + geometry.setIndex( newIndex ); + + } + + geometry.computeBoundingSphere(); + + } + + // ---- Event handling ---- + + _onPointerDown( event ) { + + if ( event.button !== 0 ) return; + + this._cachedRect = null; + + const mouseX = event.clientX; + const mouseY = event.clientY; + + if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return; + + this._sculpting = true; + this._lastMouseX = mouseX; + this._lastMouseY = mouseY; + try { this._domElement.setPointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ } + + if ( this.tool === 'scale' ) { + + this._scalePrevX = mouseX; + + } else if ( this.tool !== 'drag' ) { + + // Do first stroke immediately + this._makeStroke( mouseX, mouseY ); + this._syncGeometry(); + + } + + } + + _onPointerMove( event ) { + + if ( ! this._sculpting ) return; + + this._cachedRect = null; + + const mouseX = event.clientX; + const mouseY = event.clientY; + + if ( this.tool === 'drag' ) { + + this._sculptStrokeDrag( mouseX, mouseY ); + + } else if ( this.tool === 'scale' ) { + + this._sculptStrokeScale( mouseX, mouseY ); + + } else { + + this._sculptStroke( mouseX, mouseY ); + + } + + } + + _onPointerUp( event ) { + + if ( ! this._sculpting ) return; + this._sculpting = false; + try { this._domElement.releasePointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ } + + // Balance octree after stroke + this._sculptMesh.balanceOctree(); + + } + + get isSculpting() { + + return this._sculpting; + + } + + get hitPoint() { + + return this._interPoint; + + } + + get hitNormal() { + + return this._pickedNormal; + + } + + pickFromMouse( mouseX, mouseY ) { + + this._cachedRect = null; + + if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return false; + this._computePickedNormal(); + return true; + + } + +} + +export { Sculpt }; diff --git a/examples/jsm/sculpt/SculptMesh.js b/examples/jsm/sculpt/SculptMesh.js new file mode 100644 index 00000000000000..8b2967d7bdb955 --- /dev/null +++ b/examples/jsm/sculpt/SculptMesh.js @@ -0,0 +1,980 @@ +import { + TRI_INDEX, + Flags, + getMemory +} from './SculptUtils.js'; + +// ---- OctreeCell ---- + +const OCTREE_MAX_DEPTH = 8; +const OCTREE_MAX_FACES = 100; +const OCTREE_STACK = new Array( 1 + 7 * OCTREE_MAX_DEPTH ).fill( null ); + +class OctreeCell { + + constructor( parent ) { + + this._parent = parent || null; + this._depth = parent ? parent._depth + 1 : 0; + this._children = []; + this._aabbLoose = [ Infinity, Infinity, Infinity, - Infinity, - Infinity, - Infinity ]; + this._aabbSplit = [ Infinity, Infinity, Infinity, - Infinity, - Infinity, - Infinity ]; + this._iFaces = []; + + } + + resetNbFaces( nbFaces ) { + + const f = this._iFaces; + f.length = nbFaces; + for ( let i = 0; i < nbFaces; ++ i ) f[ i ] = i; + + } + + build( mesh ) { + + const stack = OCTREE_STACK; + stack[ 0 ] = this; + let curStack = 1; + const leaves = []; + while ( curStack > 0 ) { + + const cell = stack[ -- curStack ]; + const nbFaces = cell._iFaces.length; + if ( nbFaces > OCTREE_MAX_FACES && cell._depth < OCTREE_MAX_DEPTH ) { + + cell._constructChildren( mesh ); + const children = cell._children; + for ( let i = 0; i < 8; ++ i ) stack[ curStack + i ] = children[ i ]; + curStack += 8; + + } else if ( nbFaces > 0 ) { + + leaves.push( cell ); + + } + + } + + for ( let i = 0, l = leaves.length; i < l; ++ i ) leaves[ i ]._constructLeaf( mesh ); + + } + + _constructLeaf( mesh ) { + + const iFaces = this._iFaces; + const nbFaces = iFaces.length; + let bxmin = Infinity, bymin = Infinity, bzmin = Infinity; + let bxmax = - Infinity, bymax = - Infinity, bzmax = - Infinity; + const faceBoxes = mesh._faceBoxes; + const facePosInLeaf = mesh._facePosInLeaf; + const faceLeaf = mesh._faceLeaf; + for ( let i = 0; i < nbFaces; ++ i ) { + + const id = iFaces[ i ]; + faceLeaf[ id ] = this; + facePosInLeaf[ id ] = i; + const id6 = id * 6; + if ( faceBoxes[ id6 ] < bxmin ) bxmin = faceBoxes[ id6 ]; + if ( faceBoxes[ id6 + 1 ] < bymin ) bymin = faceBoxes[ id6 + 1 ]; + if ( faceBoxes[ id6 + 2 ] < bzmin ) bzmin = faceBoxes[ id6 + 2 ]; + if ( faceBoxes[ id6 + 3 ] > bxmax ) bxmax = faceBoxes[ id6 + 3 ]; + if ( faceBoxes[ id6 + 4 ] > bymax ) bymax = faceBoxes[ id6 + 4 ]; + if ( faceBoxes[ id6 + 5 ] > bzmax ) bzmax = faceBoxes[ id6 + 5 ]; + + } + + this._expandsAabbLoose( bxmin, bymin, bzmin, bxmax, bymax, bzmax ); + + } + + _constructChildren( mesh ) { + + const split = this._aabbSplit; + const xmin = split[ 0 ], ymin = split[ 1 ], zmin = split[ 2 ]; + const xmax = split[ 3 ], ymax = split[ 4 ], zmax = split[ 5 ]; + const dX = ( xmax - xmin ) * 0.5, dY = ( ymax - ymin ) * 0.5, dZ = ( zmax - zmin ) * 0.5; + const xcen = ( xmax + xmin ) * 0.5, ycen = ( ymax + ymin ) * 0.5, zcen = ( zmax + zmin ) * 0.5; + + const children = []; + for ( let i = 0; i < 8; ++ i ) children.push( new OctreeCell( this ) ); + + const faceCenters = mesh._faceCenters; + const iFaces = this._iFaces; + for ( let i = 0, l = iFaces.length; i < l; ++ i ) { + + const iFace = iFaces[ i ]; + const id = iFace * 3; + const cx = faceCenters[ id ], cy = faceCenters[ id + 1 ], cz = faceCenters[ id + 2 ]; + if ( cx > xcen ) { + + if ( cy > ycen ) children[ cz > zcen ? 6 : 5 ]._iFaces.push( iFace ); + else children[ cz > zcen ? 2 : 1 ]._iFaces.push( iFace ); + + } else { + + if ( cy > ycen ) children[ cz > zcen ? 7 : 4 ]._iFaces.push( iFace ); + else children[ cz > zcen ? 3 : 0 ]._iFaces.push( iFace ); + + } + + } + + children[ 0 ]._setAabbSplit( xmin, ymin, zmin, xcen, ycen, zcen ); + children[ 1 ]._setAabbSplit( xmin + dX, ymin, zmin, xcen + dX, ycen, zcen ); + children[ 2 ]._setAabbSplit( xcen, ycen - dY, zcen, xmax, ymax - dY, zmax ); + children[ 3 ]._setAabbSplit( xmin, ymin, zmin + dZ, xcen, ycen, zcen + dZ ); + children[ 4 ]._setAabbSplit( xmin, ymin + dY, zmin, xcen, ycen + dY, zcen ); + children[ 5 ]._setAabbSplit( xcen, ycen, zcen - dZ, xmax, ymax, zmax - dZ ); + children[ 6 ]._setAabbSplit( xcen, ycen, zcen, xmax, ymax, zmax ); + children[ 7 ]._setAabbSplit( xcen - dX, ycen, zcen, xmax - dX, ymax, zmax ); + + this._children = children; + this._iFaces.length = 0; + + } + + _setAabbSplit( xmin, ymin, zmin, xmax, ymax, zmax ) { + + const a = this._aabbSplit; + a[ 0 ] = xmin; a[ 1 ] = ymin; a[ 2 ] = zmin; + a[ 3 ] = xmax; a[ 4 ] = ymax; a[ 5 ] = zmax; + + } + + collectIntersectRay( vNear, eyeDir, collectFaces, leavesHit ) { + + const vx = vNear[ 0 ], vy = vNear[ 1 ], vz = vNear[ 2 ]; + const irx = 1.0 / eyeDir[ 0 ], iry = 1.0 / eyeDir[ 1 ], irz = 1.0 / eyeDir[ 2 ]; + let acc = 0; + const stack = OCTREE_STACK; + stack[ 0 ] = this; + let curStack = 1; + while ( curStack > 0 ) { + + const cell = stack[ -- curStack ]; + const loose = cell._aabbLoose; + const t1 = ( loose[ 0 ] - vx ) * irx, t2 = ( loose[ 3 ] - vx ) * irx; + const t3 = ( loose[ 1 ] - vy ) * iry, t4 = ( loose[ 4 ] - vy ) * iry; + const t5 = ( loose[ 2 ] - vz ) * irz, t6 = ( loose[ 5 ] - vz ) * irz; + const tmin = Math.max( Math.min( t1, t2 ), Math.min( t3, t4 ), Math.min( t5, t6 ) ); + const tmax = Math.min( Math.max( t1, t2 ), Math.max( t3, t4 ), Math.max( t5, t6 ) ); + if ( tmax < 0 || tmin > tmax ) continue; + const children = cell._children; + if ( children.length === 8 ) { + + for ( let i = 0; i < 8; ++ i ) stack[ curStack + i ] = children[ i ]; + curStack += 8; + + } else { + + if ( leavesHit ) leavesHit.push( cell ); + const iFaces = cell._iFaces; + collectFaces.set( iFaces, acc ); + acc += iFaces.length; + + } + + } + + return new Uint32Array( collectFaces.subarray( 0, acc ) ); + + } + + collectIntersectSphere( vert, radiusSquared, collectFaces, leavesHit ) { + + const vx = vert[ 0 ], vy = vert[ 1 ], vz = vert[ 2 ]; + let acc = 0; + const stack = OCTREE_STACK; + stack[ 0 ] = this; + let curStack = 1; + while ( curStack > 0 ) { + + const cell = stack[ -- curStack ]; + const loose = cell._aabbLoose; + let dx = 0, dy = 0, dz = 0; + if ( loose[ 0 ] > vx ) dx = loose[ 0 ] - vx; + else if ( loose[ 3 ] < vx ) dx = loose[ 3 ] - vx; + if ( loose[ 1 ] > vy ) dy = loose[ 1 ] - vy; + else if ( loose[ 4 ] < vy ) dy = loose[ 4 ] - vy; + if ( loose[ 2 ] > vz ) dz = loose[ 2 ] - vz; + else if ( loose[ 5 ] < vz ) dz = loose[ 5 ] - vz; + if ( dx * dx + dy * dy + dz * dz > radiusSquared ) continue; + const children = cell._children; + if ( children.length === 8 ) { + + for ( let i = 0; i < 8; ++ i ) stack[ curStack + i ] = children[ i ]; + curStack += 8; + + } else { + + if ( leavesHit ) leavesHit.push( cell ); + const iFaces = cell._iFaces; + collectFaces.set( iFaces, acc ); + acc += iFaces.length; + + } + + } + + return new Uint32Array( collectFaces.subarray( 0, acc ) ); + + } + + addFace( faceId, bxmin, bymin, bzmin, bxmax, bymax, bzmax, cx, cy, cz ) { + + const stack = OCTREE_STACK; + stack[ 0 ] = this; + let curStack = 1; + while ( curStack > 0 ) { + + const cell = stack[ -- curStack ]; + const s = cell._aabbSplit; + if ( cx <= s[ 0 ] || cy <= s[ 1 ] || cz <= s[ 2 ] || cx > s[ 3 ] || cy > s[ 4 ] || cz > s[ 5 ] ) continue; + const loose = cell._aabbLoose; + if ( bxmin < loose[ 0 ] ) loose[ 0 ] = bxmin; + if ( bymin < loose[ 1 ] ) loose[ 1 ] = bymin; + if ( bzmin < loose[ 2 ] ) loose[ 2 ] = bzmin; + if ( bxmax > loose[ 3 ] ) loose[ 3 ] = bxmax; + if ( bymax > loose[ 4 ] ) loose[ 4 ] = bymax; + if ( bzmax > loose[ 5 ] ) loose[ 5 ] = bzmax; + const children = cell._children; + if ( children.length === 8 ) { + + for ( let i = 0; i < 8; ++ i ) stack[ curStack + i ] = children[ i ]; + curStack += 8; + + } else { + + cell._iFaces.push( faceId ); + return cell; + + } + + } + + } + + _expandsAabbLoose( bxmin, bymin, bzmin, bxmax, bymax, bzmax ) { + + let parent = this; + while ( parent ) { + + const p = parent._aabbLoose; + let proceed = false; + if ( bxmin < p[ 0 ] ) { p[ 0 ] = bxmin; proceed = true; } + if ( bymin < p[ 1 ] ) { p[ 1 ] = bymin; proceed = true; } + if ( bzmin < p[ 2 ] ) { p[ 2 ] = bzmin; proceed = true; } + if ( bxmax > p[ 3 ] ) { p[ 3 ] = bxmax; proceed = true; } + if ( bymax > p[ 4 ] ) { p[ 4 ] = bymax; proceed = true; } + if ( bzmax > p[ 5 ] ) { p[ 5 ] = bzmax; proceed = true; } + parent = proceed ? parent._parent : null; + + } + + } + + pruneIfPossible() { + + let cell = this; + while ( cell._parent ) { + + const parent = cell._parent; + const children = parent._children; + if ( children.length === 0 ) return; + for ( let i = 0; i < 8; ++ i ) { + + if ( children[ i ]._iFaces.length > 0 || children[ i ]._children.length === 8 ) return; + + } + + children.length = 0; + cell = parent; + + } + + } + +} + +// ---- Internal Mesh Data ---- +// This class wraps all the internal sculpting data structures. +// It mirrors SculptGL's MeshData + MeshDynamic in a single object. + +class SculptMesh { + + constructor() { + + this._nbVertices = 0; + this._nbFaces = 0; + + this._verticesXYZ = null; + this._normalsXYZ = null; + this._colorsRGB = null; + this._materialsPBR = null; + + this._facesABCD = null; + this._trianglesABC = null; + + this._vertRingVert = []; + this._vertRingFace = []; + this._vertOnEdge = null; + + this._faceNormals = null; + this._faceBoxes = null; + this._faceCenters = null; + this._facePosInLeaf = null; + this._faceLeaf = []; + + this._vertTagFlags = null; + this._vertSculptFlags = null; + this._vertStateFlags = null; + this._facesTagFlags = null; + this._facesStateFlags = null; + + this._octree = null; + + this.isDynamic = true; + + } + + // ---- Accessors matching SculptGL's Mesh interface ---- + + getNbVertices() { return this._nbVertices; } + getNbFaces() { return this._nbFaces; } + getNbTriangles() { return this._nbFaces; } + getVertices() { return this._verticesXYZ; } + getNormals() { return this._normalsXYZ; } + getColors() { return this._colorsRGB; } + getMaterials() { return this._materialsPBR; } + getFaces() { return this._facesABCD; } + getTriangles() { return this._trianglesABC; } + getVerticesRingVert() { return this._vertRingVert; } + getVerticesRingFace() { return this._vertRingFace; } + getVerticesOnEdge() { return this._vertOnEdge; } + getVerticesTagFlags() { return this._vertTagFlags; } + getVerticesSculptFlags() { return this._vertSculptFlags; } + getVerticesStateFlags() { return this._vertStateFlags; } + getVerticesProxy() { return this._verticesXYZ; } + getFaceNormals() { return this._faceNormals; } + getFaceBoxes() { return this._faceBoxes; } + getFaceCenters() { return this._faceCenters; } + getFacePosInLeaf() { return this._facePosInLeaf; } + getFaceLeaf() { return this._faceLeaf; } + getFacesTagFlags() { return this._facesTagFlags; } + getFacesStateFlags() { return this._facesStateFlags; } + + addNbVertice( nb ) { this._nbVertices += nb; } + addNbFace( nb ) { this._nbFaces += nb; } + + // ---- Init from Three.js BufferGeometry ---- + + initFromGeometry( geometry ) { + + const posAttr = geometry.getAttribute( 'position' ); + const index = geometry.getIndex(); + const srcPositions = posAttr.array; + const srcCount = posAttr.count; + + // Merge duplicate vertices by position + // Build a map from original vertex index to merged vertex index + const precision = 1e-6; + const vertexMap = new Uint32Array( srcCount ); // old index -> merged index + const mergedPositions = []; + const hashMap = new Map(); + + for ( let i = 0; i < srcCount; ++ i ) { + + const x = srcPositions[ i * 3 ]; + const y = srcPositions[ i * 3 + 1 ]; + const z = srcPositions[ i * 3 + 2 ]; + + // Quantize to grid for hashing + const kx = Math.round( x / precision ); + const ky = Math.round( y / precision ); + const kz = Math.round( z / precision ); + const key = kx + ',' + ky + ',' + kz; + + if ( hashMap.has( key ) ) { + + vertexMap[ i ] = hashMap.get( key ); + + } else { + + const newIdx = mergedPositions.length / 3; + hashMap.set( key, newIdx ); + vertexMap[ i ] = newIdx; + mergedPositions.push( x, y, z ); + + } + + } + + const nbVertices = mergedPositions.length / 3; + this._nbVertices = nbVertices; + + this._verticesXYZ = new Float32Array( mergedPositions ); + this._normalsXYZ = new Float32Array( nbVertices * 3 ); + this._colorsRGB = new Float32Array( nbVertices * 3 ); + this._materialsPBR = new Float32Array( nbVertices * 3 ); + + for ( let i = 0; i < nbVertices; ++ i ) { + + this._colorsRGB[ i * 3 ] = 1.0; + this._colorsRGB[ i * 3 + 1 ] = 1.0; + this._colorsRGB[ i * 3 + 2 ] = 1.0; + this._materialsPBR[ i * 3 ] = 0.18; + this._materialsPBR[ i * 3 + 1 ] = 0.08; + this._materialsPBR[ i * 3 + 2 ] = 1.0; + + } + + // Build faces using merged vertex indices + let nbTriangles; + if ( index ) { + + nbTriangles = index.count / 3; + + } else { + + nbTriangles = srcCount / 3; + + } + + this._nbFaces = nbTriangles; + this._facesABCD = new Uint32Array( nbTriangles * 4 ); + this._trianglesABC = new Uint32Array( nbTriangles * 3 ); + + for ( let i = 0; i < nbTriangles; ++ i ) { + + let a, b, c; + if ( index ) { + + a = vertexMap[ index.array[ i * 3 ] ]; + b = vertexMap[ index.array[ i * 3 + 1 ] ]; + c = vertexMap[ index.array[ i * 3 + 2 ] ]; + + } else { + + a = vertexMap[ i * 3 ]; + b = vertexMap[ i * 3 + 1 ]; + c = vertexMap[ i * 3 + 2 ]; + + } + + this._facesABCD[ i * 4 ] = a; + this._facesABCD[ i * 4 + 1 ] = b; + this._facesABCD[ i * 4 + 2 ] = c; + this._facesABCD[ i * 4 + 3 ] = TRI_INDEX; + this._trianglesABC[ i * 3 ] = a; + this._trianglesABC[ i * 3 + 1 ] = b; + this._trianglesABC[ i * 3 + 2 ] = c; + + } + + // Allocate arrays + this._vertOnEdge = new Uint8Array( nbVertices ); + this._vertTagFlags = new Int32Array( nbVertices ); + this._vertSculptFlags = new Int32Array( nbVertices ); + this._vertStateFlags = new Int32Array( nbVertices ); + this._facesTagFlags = new Int32Array( nbTriangles ); + this._facesStateFlags = new Int32Array( nbTriangles ); + this._faceBoxes = new Float32Array( nbTriangles * 6 ); + this._faceNormals = new Float32Array( nbTriangles * 3 ); + this._faceCenters = new Float32Array( nbTriangles * 3 ); + this._facePosInLeaf = new Uint32Array( nbTriangles ); + this._faceLeaf = new Array( nbTriangles ).fill( null ); + + // Init topology (Array of Arrays for dynamic topo) + this._initTopology(); + + // Compute geometry (normals, aabbs, octree) + this._updateGeometry(); + + } + + _initTopology() { + + const vrings = this._vertRingVert; + const frings = this._vertRingFace; + const nbVertices = this._nbVertices; + vrings.length = frings.length = nbVertices; + for ( let i = 0; i < nbVertices; ++ i ) { + + vrings[ i ] = []; + frings[ i ] = []; + + } + + const nbTriangles = this._nbFaces; + const tAr = this._trianglesABC; + for ( let i = 0; i < nbTriangles; ++ i ) { + + const j = i * 3; + frings[ tAr[ j ] ].push( i ); + frings[ tAr[ j + 1 ] ].push( i ); + frings[ tAr[ j + 2 ] ].push( i ); + + } + + const vOnEdge = this._vertOnEdge; + for ( let i = 0; i < nbVertices; ++ i ) { + + this._computeRingVertices( i ); + vOnEdge[ i ] = frings[ i ].length !== vrings[ i ].length ? 1 : 0; + + } + + } + + _computeRingVertices( iVert ) { + + const tagFlag = ++ Flags.TAG; + const fAr = this._facesABCD; + const vflags = this._vertTagFlags; + const vring = this._vertRingVert[ iVert ]; + const fring = this._vertRingFace[ iVert ]; + vring.length = 0; + for ( let i = 0, l = fring.length; i < l; ++ i ) { + + const ind = fring[ i ] * 4; + let iVer1 = fAr[ ind ]; + let iVer2 = fAr[ ind + 1 ]; + if ( iVer1 === iVert ) iVer1 = fAr[ ind + 2 ]; + else if ( iVer2 === iVert ) iVer2 = fAr[ ind + 2 ]; + if ( vflags[ iVer1 ] !== tagFlag ) { vflags[ iVer1 ] = tagFlag; vring.push( iVer1 ); } + if ( vflags[ iVer2 ] !== tagFlag ) { vflags[ iVer2 ] = tagFlag; vring.push( iVer2 ); } + + } + + } + + _updateGeometry( iFaces, iVerts ) { + + this._updateFacesAabbAndNormal( iFaces ); + this._updateVerticesNormal( iVerts ); + this._updateOctree( iFaces ); + + } + + _updateFacesAabbAndNormal( iFaces ) { + + const faceNormals = this._faceNormals; + const faceBoxes = this._faceBoxes; + const faceCenters = this._faceCenters; + const vAr = this._verticesXYZ; + const fAr = this._facesABCD; + const full = iFaces === undefined; + const nbFaces = full ? this._nbFaces : iFaces.length; + + for ( let i = 0; i < nbFaces; ++ i ) { + + const ind = full ? i : iFaces[ i ]; + const idTri = ind * 3; + const idFace = ind * 4; + const idBox = ind * 6; + const ind1 = fAr[ idFace ] * 3; + const ind2 = fAr[ idFace + 1 ] * 3; + const ind3 = fAr[ idFace + 2 ] * 3; + + const v1x = vAr[ ind1 ], v1y = vAr[ ind1 + 1 ], v1z = vAr[ ind1 + 2 ]; + const v2x = vAr[ ind2 ], v2y = vAr[ ind2 + 1 ], v2z = vAr[ ind2 + 2 ]; + const v3x = vAr[ ind3 ], v3y = vAr[ ind3 + 1 ], v3z = vAr[ ind3 + 2 ]; + + const ax = v2x - v1x, ay = v2y - v1y, az = v2z - v1z; + const bx = v3x - v1x, by = v3y - v1y, bz = v3z - v1z; + faceNormals[ idTri ] = ay * bz - az * by; + faceNormals[ idTri + 1 ] = az * bx - ax * bz; + faceNormals[ idTri + 2 ] = ax * by - ay * bx; + + const xmin = v1x < v2x ? ( v1x < v3x ? v1x : v3x ) : ( v2x < v3x ? v2x : v3x ); + const xmax = v1x > v2x ? ( v1x > v3x ? v1x : v3x ) : ( v2x > v3x ? v2x : v3x ); + const ymin = v1y < v2y ? ( v1y < v3y ? v1y : v3y ) : ( v2y < v3y ? v2y : v3y ); + const ymax = v1y > v2y ? ( v1y > v3y ? v1y : v3y ) : ( v2y > v3y ? v2y : v3y ); + const zmin = v1z < v2z ? ( v1z < v3z ? v1z : v3z ) : ( v2z < v3z ? v2z : v3z ); + const zmax = v1z > v2z ? ( v1z > v3z ? v1z : v3z ) : ( v2z > v3z ? v2z : v3z ); + + faceBoxes[ idBox ] = xmin; faceBoxes[ idBox + 1 ] = ymin; faceBoxes[ idBox + 2 ] = zmin; + faceBoxes[ idBox + 3 ] = xmax; faceBoxes[ idBox + 4 ] = ymax; faceBoxes[ idBox + 5 ] = zmax; + faceCenters[ idTri ] = ( xmin + xmax ) * 0.5; + faceCenters[ idTri + 1 ] = ( ymin + ymax ) * 0.5; + faceCenters[ idTri + 2 ] = ( zmin + zmax ) * 0.5; + + } + + } + + _updateVerticesNormal( iVerts ) { + + const nAr = this._normalsXYZ; + const faceNormals = this._faceNormals; + const ringFaces = this._vertRingFace; + const full = iVerts === undefined; + const nbVerts = full ? this._nbVertices : iVerts.length; + + for ( let i = 0; i < nbVerts; ++ i ) { + + const ind = full ? i : iVerts[ i ]; + const vrf = ringFaces[ ind ]; + let nx = 0, ny = 0, nz = 0; + for ( let j = 0, l = vrf.length; j < l; ++ j ) { + + const id = vrf[ j ] * 3; + nx += faceNormals[ id ]; + ny += faceNormals[ id + 1 ]; + nz += faceNormals[ id + 2 ]; + + } + + let len = Math.sqrt( nx * nx + ny * ny + nz * nz ); + if ( len > 0 ) len = 1.0 / len; + const ind3 = ind * 3; + nAr[ ind3 ] = nx * len; + nAr[ ind3 + 1 ] = ny * len; + nAr[ ind3 + 2 ] = nz * len; + + } + + } + + _updateOctree( iFaces ) { + + if ( iFaces === undefined ) { + + // Full rebuild + const octree = new OctreeCell(); + octree.resetNbFaces( this._nbFaces ); + + // Compute world bounds + const vAr = this._verticesXYZ; + let bxmin = Infinity, bymin = Infinity, bzmin = Infinity; + let bxmax = - Infinity, bymax = - Infinity, bzmax = - Infinity; + for ( let i = 0, l = this._nbVertices * 3; i < l; i += 3 ) { + + if ( vAr[ i ] < bxmin ) bxmin = vAr[ i ]; + if ( vAr[ i ] > bxmax ) bxmax = vAr[ i ]; + if ( vAr[ i + 1 ] < bymin ) bymin = vAr[ i + 1 ]; + if ( vAr[ i + 1 ] > bymax ) bymax = vAr[ i + 1 ]; + if ( vAr[ i + 2 ] < bzmin ) bzmin = vAr[ i + 2 ]; + if ( vAr[ i + 2 ] > bzmax ) bzmax = vAr[ i + 2 ]; + + } + + const rangeX = bxmax - bxmin, rangeY = bymax - bymin, rangeZ = bzmax - bzmin; + const margin = Math.max( rangeX, rangeY, rangeZ, 0.1 ) * 0.5; + octree._setAabbSplit( bxmin - margin, bymin - margin, bzmin - margin, bxmax + margin, bymax + margin, bzmax + margin ); + octree.build( this ); + this._octree = octree; + + } else { + + // Partial update: reinsert modified faces + const faceBoxes = this._faceBoxes; + const faceCenters = this._faceCenters; + const faceLeaf = this._faceLeaf; + const facePosInLeaf = this._facePosInLeaf; + const nbFaces = iFaces.length; + + const leavesToUpdate = []; + + for ( let i = 0; i < nbFaces; ++ i ) { + + const iFace = iFaces[ i ]; + const leaf = faceLeaf[ iFace ]; + if ( ! leaf ) continue; + const pos = facePosInLeaf[ iFace ]; + const iTrisLeaf = leaf._iFaces; + const last = iTrisLeaf[ iTrisLeaf.length - 1 ]; + if ( iFace !== last ) { + + iTrisLeaf[ pos ] = last; + facePosInLeaf[ last ] = pos; + + } + + iTrisLeaf.pop(); + leavesToUpdate.push( leaf ); + + } + + for ( let i = 0; i < nbFaces; ++ i ) { + + const iFace = iFaces[ i ]; + const id6 = iFace * 6; + const id3 = iFace * 3; + const newLeaf = this._octree.addFace( + iFace, + faceBoxes[ id6 ], faceBoxes[ id6 + 1 ], faceBoxes[ id6 + 2 ], + faceBoxes[ id6 + 3 ], faceBoxes[ id6 + 4 ], faceBoxes[ id6 + 5 ], + faceCenters[ id3 ], faceCenters[ id3 + 1 ], faceCenters[ id3 + 2 ] + ); + if ( newLeaf ) { + + faceLeaf[ iFace ] = newLeaf; + facePosInLeaf[ iFace ] = newLeaf._iFaces.length - 1; + + } + + } + + for ( let i = 0, l = leavesToUpdate.length; i < l; ++ i ) { + + if ( leavesToUpdate[ i ]._iFaces.length === 0 ) leavesToUpdate[ i ].pruneIfPossible(); + + } + + } + + } + + // ---- Mesh queries (used by Picking, SculptBase, etc.) ---- + + intersectRay( vNear, eyeDir ) { + + const nbFaces = this._nbFaces; + const collectBuffer = new Uint32Array( getMemory( nbFaces * 4 ), 0, nbFaces ); + return this._octree.collectIntersectRay( vNear, eyeDir, collectBuffer ); + + } + + intersectSphere( center, radiusSq ) { + + const nbFaces = this._nbFaces; + const collectBuffer = new Uint32Array( getMemory( nbFaces * 4 ), 0, nbFaces ); + return this._octree.collectIntersectSphere( center, radiusSq, collectBuffer ); + + } + + getVerticesFromFaces( iFaces ) { + + const tagFlag = ++ Flags.TAG; + const nbFaces = iFaces.length; + const vtf = this._vertTagFlags; + const fAr = this._facesABCD; + let acc = 0; + const verts = new Uint32Array( getMemory( 4 * nbFaces * 4 ), 0, nbFaces * 4 ); + for ( let i = 0; i < nbFaces; ++ i ) { + + const ind = iFaces[ i ] * 4; + const iv1 = fAr[ ind ], iv2 = fAr[ ind + 1 ], iv3 = fAr[ ind + 2 ]; + if ( vtf[ iv1 ] !== tagFlag ) { vtf[ iv1 ] = tagFlag; verts[ acc ++ ] = iv1; } + if ( vtf[ iv2 ] !== tagFlag ) { vtf[ iv2 ] = tagFlag; verts[ acc ++ ] = iv2; } + if ( vtf[ iv3 ] !== tagFlag ) { vtf[ iv3 ] = tagFlag; verts[ acc ++ ] = iv3; } + + } + + return new Uint32Array( verts.subarray( 0, acc ) ); + + } + + getFacesFromVertices( iVerts ) { + + const tagFlag = ++ Flags.TAG; + const ftf = this._facesTagFlags; + const frings = this._vertRingFace; + const nbVerts = iVerts.length; + const faces = new Uint32Array( getMemory( 4 * this._nbFaces ), 0, this._nbFaces ); + let acc = 0; + for ( let i = 0; i < nbVerts; ++ i ) { + + const fring = frings[ iVerts[ i ] ]; + for ( let j = 0, l = fring.length; j < l; ++ j ) { + + const iFace = fring[ j ]; + if ( ftf[ iFace ] !== tagFlag ) { + + ftf[ iFace ] = tagFlag; + faces[ acc ++ ] = iFace; + + } + + } + + } + + return new Uint32Array( faces.subarray( 0, acc ) ); + + } + + expandsFaces( iFaces, nRing ) { + + const tagFlag = ++ Flags.TAG; + let nbFaces = iFaces.length; + const ftf = this._facesTagFlags; + const fAr = this._facesABCD; + const ringFaces = this._vertRingFace; + let acc = nbFaces; + const iFacesExpanded = new Uint32Array( getMemory( 4 * this._nbFaces ), 0, this._nbFaces ); + iFacesExpanded.set( iFaces ); + for ( let i = 0; i < nbFaces; ++ i ) ftf[ iFacesExpanded[ i ] ] = tagFlag; + let iBegin = 0; + while ( nRing ) { + + -- nRing; + for ( let i = iBegin; i < nbFaces; ++ i ) { + + const ind = iFacesExpanded[ i ] * 4; + for ( let j = 0; j < 3; ++ j ) { + + const idv = fAr[ ind + j ]; + const vrf = ringFaces[ idv ]; + for ( let k = 0, l = vrf.length; k < l; ++ k ) { + + const id = vrf[ k ]; + if ( ftf[ id ] === tagFlag ) continue; + ftf[ id ] = tagFlag; + iFacesExpanded[ acc ++ ] = id; + + } + + } + + } + + iBegin = nbFaces; + nbFaces = acc; + + } + + return new Uint32Array( iFacesExpanded.subarray( 0, acc ) ); + + } + + expandsVertices( iVerts, nRing ) { + + const tagFlag = ++ Flags.TAG; + let nbVerts = iVerts.length; + const vrings = this._vertRingVert; + const vtf = this._vertTagFlags; + let acc = nbVerts; + const nbVertices = this._nbVertices; + const iVertsExpanded = new Uint32Array( getMemory( 4 * nbVertices ), 0, nbVertices ); + iVertsExpanded.set( iVerts ); + for ( let i = 0; i < nbVerts; ++ i ) vtf[ iVertsExpanded[ i ] ] = tagFlag; + let iBegin = 0; + while ( nRing ) { + + -- nRing; + for ( let i = iBegin; i < nbVerts; ++ i ) { + + const ring = vrings[ iVertsExpanded[ i ] ]; + for ( let j = 0, l = ring.length; j < l; ++ j ) { + + const id = ring[ j ]; + if ( vtf[ id ] === tagFlag ) continue; + vtf[ id ] = tagFlag; + iVertsExpanded[ acc ++ ] = id; + + } + + } + + iBegin = nbVerts; + nbVerts = acc; + + } + + return new Uint32Array( iVertsExpanded.subarray( 0, acc ) ); + + } + + // ---- Dynamic topology helpers ---- + + updateRenderTriangles( iFaces ) { + + const tAr = this._trianglesABC; + const fAr = this._facesABCD; + const full = iFaces === undefined; + const nbFaces = full ? this._nbFaces : iFaces.length; + for ( let i = 0; i < nbFaces; ++ i ) { + + const id = full ? i : iFaces[ i ]; + const idt = id * 3; + const idf = id * 4; + tAr[ idt ] = fAr[ idf ]; + tAr[ idt + 1 ] = fAr[ idf + 1 ]; + tAr[ idt + 2 ] = fAr[ idf + 2 ]; + + } + + } + + updateVerticesOnEdge( iVerts ) { + + const vOnEdge = this._vertOnEdge; + const vrings = this._vertRingVert; + const frings = this._vertRingFace; + const full = iVerts === undefined; + const nbVerts = full ? this._nbVertices : iVerts.length; + for ( let i = 0; i < nbVerts; ++ i ) { + + const id = full ? i : iVerts[ i ]; + vOnEdge[ id ] = vrings[ id ].length !== frings[ id ].length ? 1 : 0; + + } + + } + + updateTopology( iFaces, iVerts ) { + + this.updateRenderTriangles( iFaces ); + this.updateVerticesOnEdge( iVerts ); + + } + + _resizeArray( orig, targetSize ) { + + if ( ! orig ) return null; + if ( orig.length >= targetSize ) return orig.subarray( 0, targetSize * 2 ); + const tmp = new orig.constructor( targetSize * 2 ); + tmp.set( orig ); + return tmp; + + } + + reAllocateArrays( nbAddElements ) { + + let nbDyna = this._facesStateFlags.length; + const nbTriangles = this._nbFaces; + let len = nbTriangles + nbAddElements; + if ( nbDyna < len || nbDyna > len * 4 ) { + + this._facesStateFlags = this._resizeArray( this._facesStateFlags, len ); + this._facesABCD = this._resizeArray( this._facesABCD, len * 4 ); + this._trianglesABC = this._resizeArray( this._trianglesABC, len * 3 ); + this._faceBoxes = this._resizeArray( this._faceBoxes, len * 6 ); + this._faceNormals = this._resizeArray( this._faceNormals, len * 3 ); + this._faceCenters = this._resizeArray( this._faceCenters, len * 3 ); + this._facesTagFlags = this._resizeArray( this._facesTagFlags, len ); + this._facePosInLeaf = this._resizeArray( this._facePosInLeaf, len ); + + } + + nbDyna = this._verticesXYZ.length / 3; + const nbVertices = this._nbVertices; + len = nbVertices + nbAddElements; + if ( nbDyna < len || nbDyna > len * 4 ) { + + this._verticesXYZ = this._resizeArray( this._verticesXYZ, len * 3 ); + this._normalsXYZ = this._resizeArray( this._normalsXYZ, len * 3 ); + this._colorsRGB = this._resizeArray( this._colorsRGB, len * 3 ); + this._materialsPBR = this._resizeArray( this._materialsPBR, len * 3 ); + this._vertOnEdge = this._resizeArray( this._vertOnEdge, len ); + this._vertTagFlags = this._resizeArray( this._vertTagFlags, len ); + this._vertSculptFlags = this._resizeArray( this._vertSculptFlags, len ); + this._vertStateFlags = this._resizeArray( this._vertStateFlags, len ); + + } + + } + + balanceOctree() { + + // Rebuild octree from scratch + this._updateOctree(); + + } + +} + +export { SculptMesh }; diff --git a/examples/jsm/sculpt/SculptTools.js b/examples/jsm/sculpt/SculptTools.js new file mode 100644 index 00000000000000..4c9f36c0bedb50 --- /dev/null +++ b/examples/jsm/sculpt/SculptTools.js @@ -0,0 +1,1099 @@ +import { + TRI_INDEX, + Flags, + getMemory, + replaceElement, + removeElement, + tidy, + intersectionArrays, + sqrDist, + triangleInsideSphere, + pointInsideTriangle, + falloff +} from './SculptUtils.js'; + +// ---- Subdivision ---- + +const SubData = { + _mesh: null, + _linear: false, + _verticesMap: new Map(), + _center: [ 0, 0, 0 ], + _radius2: 0, + _edgeMax2: 0 +}; + +function subFillTriangle( iTri, iv1, iv2, iv3, ivMid ) { + + const mesh = SubData._mesh; + const vrv = mesh.getVerticesRingVert(); + const vrf = mesh.getVerticesRingFace(); + const pil = mesh.getFacePosInLeaf(); + const fleaf = mesh.getFaceLeaf(); + const fstf = mesh.getFacesStateFlags(); + const fAr = mesh.getFaces(); + + let j = iTri * 4; + fAr[ j ] = iv1; fAr[ j + 1 ] = ivMid; fAr[ j + 2 ] = iv3; fAr[ j + 3 ] = TRI_INDEX; + const leaf = fleaf[ iTri ]; + const iTrisLeaf = leaf._iFaces; + + vrv[ ivMid ].push( iv3 ); + vrv[ iv3 ].push( ivMid ); + + const iNewTri = mesh.getNbTriangles(); + vrf[ ivMid ].push( iTri, iNewTri ); + + j = iNewTri * 4; + fAr[ j ] = ivMid; fAr[ j + 1 ] = iv2; fAr[ j + 2 ] = iv3; fAr[ j + 3 ] = TRI_INDEX; + fstf[ iNewTri ] = Flags.STATE; + fleaf[ iNewTri ] = leaf; + pil[ iNewTri ] = iTrisLeaf.length; + vrf[ iv3 ].push( iNewTri ); + replaceElement( vrf[ iv2 ], iTri, iNewTri ); + iTrisLeaf.push( iNewTri ); + mesh.addNbFace( 1 ); + +} + +function subFillTriangles( iTris ) { + + const mesh = SubData._mesh; + const vrv = mesh.getVerticesRingVert(); + const fAr = mesh.getFaces(); + const nbTris = iTris.length; + const iTrisNext = new Uint32Array( getMemory( 4 * 2 * nbTris ), 0, 2 * nbTris ); + let nbNext = 0; + const vMap = SubData._verticesMap; + + for ( let i = 0; i < nbTris; ++ i ) { + + const iTri = iTris[ i ]; + const j = iTri * 4; + const iv1 = fAr[ j ], iv2 = fAr[ j + 1 ], iv3 = fAr[ j + 2 ]; + const val1 = vMap.get( Math.min( iv1, iv2 ) + '+' + Math.max( iv1, iv2 ) ); + const val2 = vMap.get( Math.min( iv2, iv3 ) + '+' + Math.max( iv2, iv3 ) ); + const val3 = vMap.get( Math.min( iv1, iv3 ) + '+' + Math.max( iv1, iv3 ) ); + const num1 = vrv[ iv1 ].length, num2 = vrv[ iv2 ].length, num3 = vrv[ iv3 ].length; + let split = 0; + if ( val1 ) { + + if ( val2 ) { + + if ( val3 ) { if ( num1 < num2 && num1 < num3 ) split = 2; else if ( num2 < num3 ) split = 3; else split = 1; } + else if ( num1 < num3 ) split = 2; else split = 1; + + } else if ( val3 && num2 < num3 ) split = 3; + else split = 1; + + } else if ( val2 ) { + + if ( val3 && num2 < num1 ) split = 3; else split = 2; + + } else if ( val3 ) split = 3; + + if ( split === 1 ) subFillTriangle( iTri, iv1, iv2, iv3, val1 ); + else if ( split === 2 ) subFillTriangle( iTri, iv2, iv3, iv1, val2 ); + else if ( split === 3 ) subFillTriangle( iTri, iv3, iv1, iv2, val3 ); + else continue; + iTrisNext[ nbNext ++ ] = iTri; + iTrisNext[ nbNext ++ ] = mesh.getNbTriangles() - 1; + + } + + return new Uint32Array( iTrisNext.subarray( 0, nbNext ) ); + +} + +function halfEdgeSplit( iTri, iv1, iv2, iv3 ) { + + const mesh = SubData._mesh; + const vAr = mesh.getVertices(); + const nAr = mesh.getNormals(); + const cAr = mesh.getColors(); + const mAr = mesh.getMaterials(); + const fAr = mesh.getFaces(); + const pil = mesh.getFacePosInLeaf(); + const fleaf = mesh.getFaceLeaf(); + const vrv = mesh.getVerticesRingVert(); + const vrf = mesh.getVerticesRingFace(); + const fstf = mesh.getFacesStateFlags(); + const vstf = mesh.getVerticesStateFlags(); + + const vMap = SubData._verticesMap; + const key = Math.min( iv1, iv2 ) + '+' + Math.max( iv1, iv2 ); + let isNewVertex = false; + let ivMid = vMap.get( key ); + if ( ivMid === undefined ) { + + ivMid = mesh.getNbVertices(); + isNewVertex = true; + vMap.set( key, ivMid ); + + } + + vrv[ iv3 ].push( ivMid ); + let id = iTri * 4; + fAr[ id ] = iv1; fAr[ id + 1 ] = ivMid; fAr[ id + 2 ] = iv3; fAr[ id + 3 ] = TRI_INDEX; + + const iNewTri = mesh.getNbTriangles(); + id = iNewTri * 4; + fAr[ id ] = ivMid; fAr[ id + 1 ] = iv2; fAr[ id + 2 ] = iv3; fAr[ id + 3 ] = TRI_INDEX; + fstf[ iNewTri ] = Flags.STATE; + + vrf[ iv3 ].push( iNewTri ); + replaceElement( vrf[ iv2 ], iTri, iNewTri ); + const leaf = fleaf[ iTri ]; + const iTrisLeaf = leaf._iFaces; + fleaf[ iNewTri ] = leaf; + pil[ iNewTri ] = iTrisLeaf.length; + iTrisLeaf.push( iNewTri ); + + if ( ! isNewVertex ) { + + vrv[ ivMid ].push( iv3 ); + vrf[ ivMid ].push( iTri, iNewTri ); + mesh.addNbFace( 1 ); + return; + + } + + const id1 = iv1 * 3, id2 = iv2 * 3; + const v1x = vAr[ id1 ], v1y = vAr[ id1 + 1 ], v1z = vAr[ id1 + 2 ]; + const n1x = nAr[ id1 ], n1y = nAr[ id1 + 1 ], n1z = nAr[ id1 + 2 ]; + const v2x = vAr[ id2 ], v2y = vAr[ id2 + 1 ], v2z = vAr[ id2 + 2 ]; + const n2x = nAr[ id2 ], n2y = nAr[ id2 + 1 ], n2z = nAr[ id2 + 2 ]; + + const n1n2x = n1x + n2x, n1n2y = n1y + n2y, n1n2z = n1z + n2z; + id = ivMid * 3; + nAr[ id ] = n1n2x * 0.5; nAr[ id + 1 ] = n1n2y * 0.5; nAr[ id + 2 ] = n1n2z * 0.5; + cAr[ id ] = ( cAr[ id1 ] + cAr[ id2 ] ) * 0.5; + cAr[ id + 1 ] = ( cAr[ id1 + 1 ] + cAr[ id2 + 1 ] ) * 0.5; + cAr[ id + 2 ] = ( cAr[ id1 + 2 ] + cAr[ id2 + 2 ] ) * 0.5; + mAr[ id ] = ( mAr[ id1 ] + mAr[ id2 ] ) * 0.5; + mAr[ id + 1 ] = ( mAr[ id1 + 1 ] + mAr[ id2 + 1 ] ) * 0.5; + mAr[ id + 2 ] = ( mAr[ id1 + 2 ] + mAr[ id2 + 2 ] ) * 0.5; + + if ( SubData._linear ) { + + vAr[ id ] = ( v1x + v2x ) * 0.5; + vAr[ id + 1 ] = ( v1y + v2y ) * 0.5; + vAr[ id + 2 ] = ( v1z + v2z ) * 0.5; + + } else { + + let nn1x = n1x, nn1y = n1y, nn1z = n1z; + let len = nn1x * nn1x + nn1y * nn1y + nn1z * nn1z; + if ( len === 0 ) { nn1x = 1; } else { len = 1 / Math.sqrt( len ); nn1x *= len; nn1y *= len; nn1z *= len; } + let nn2x = n2x, nn2y = n2y, nn2z = n2z; + len = nn2x * nn2x + nn2y * nn2y + nn2z * nn2z; + if ( len === 0 ) { nn2x = 1; } else { len = 1 / Math.sqrt( len ); nn2x *= len; nn2y *= len; nn2z *= len; } + let d = nn1x * nn2x + nn1y * nn2y + nn1z * nn2z; + let angle = 0; + if ( d <= - 1 ) angle = Math.PI; + else if ( d >= 1 ) angle = 0; + else angle = Math.acos( d ); + + const ex = v1x - v2x, ey = v1y - v2y, ez = v1z - v2z; + let offset = angle * 0.12 * Math.sqrt( ex * ex + ey * ey + ez * ez ); + len = n1n2x * n1n2x + n1n2y * n1n2y + n1n2z * n1n2z; + if ( len > 0 ) offset /= Math.sqrt( len ); + if ( ( ex * ( n1x - n2x ) + ey * ( n1y - n2y ) + ez * ( n1z - n2z ) ) < 0 ) offset = - offset; + vAr[ id ] = ( v1x + v2x ) * 0.5 + n1n2x * offset; + vAr[ id + 1 ] = ( v1y + v2y ) * 0.5 + n1n2y * offset; + vAr[ id + 2 ] = ( v1z + v2z ) * 0.5 + n1n2z * offset; + + } + + vstf[ ivMid ] = Flags.STATE; + vrv[ ivMid ] = [ iv1, iv2, iv3 ]; + vrf[ ivMid ] = [ iTri, iNewTri ]; + replaceElement( vrv[ iv1 ], iv2, ivMid ); + replaceElement( vrv[ iv2 ], iv1, ivMid ); + mesh.addNbVertice( 1 ); + mesh.addNbFace( 1 ); + +} + +function subFindSplit( iTri, checkInsideSphere ) { + + const mesh = SubData._mesh; + const vAr = mesh.getVertices(); + const fAr = mesh.getFaces(); + const mAr = mesh.getMaterials(); + const id = iTri * 4; + const ind1 = fAr[ id ] * 3, ind2 = fAr[ id + 1 ] * 3, ind3 = fAr[ id + 2 ] * 3; + const v1 = [ vAr[ ind1 ], vAr[ ind1 + 1 ], vAr[ ind1 + 2 ] ]; + const v2 = [ vAr[ ind2 ], vAr[ ind2 + 1 ], vAr[ ind2 + 2 ] ]; + const v3 = [ vAr[ ind3 ], vAr[ ind3 + 1 ], vAr[ ind3 + 2 ] ]; + + if ( checkInsideSphere && ! triangleInsideSphere( SubData._center, SubData._radius2, v1, v2, v3 ) && ! pointInsideTriangle( SubData._center, v1, v2, v3 ) ) + return 0; + + const m1 = mAr[ ind1 + 2 ], m2 = mAr[ ind2 + 2 ], m3 = mAr[ ind3 + 2 ]; + const length1 = sqrDist( v1, v2 ), length2 = sqrDist( v2, v3 ), length3 = sqrDist( v1, v3 ); + if ( length1 > length2 && length1 > length3 ) return ( m1 + m2 ) * 0.5 * length1 > SubData._edgeMax2 ? 1 : 0; + else if ( length2 > length3 ) return ( m2 + m3 ) * 0.5 * length2 > SubData._edgeMax2 ? 2 : 0; + else return ( m1 + m3 ) * 0.5 * length3 > SubData._edgeMax2 ? 3 : 0; + +} + +function subdivide( iTris ) { + + const mesh = SubData._mesh; + const nbVertsInit = mesh.getNbVertices(); + const nbTrisInit = mesh.getNbTriangles(); + SubData._verticesMap = new Map(); + + // Init split + let nbTris = iTris.length; + let buffer = getMemory( ( 4 + 1 ) * nbTris ); + let iTrisSubd = new Uint32Array( buffer, 0, nbTris ); + let splitArr = new Uint8Array( buffer, 4 * nbTris, nbTris ); + let acc = 0; + for ( let i = 0; i < nbTris; ++ i ) { + + const iTri = iTris[ i ]; + const splitNum = subFindSplit( iTri, true ); + if ( splitNum === 0 ) continue; + splitArr[ acc ] = splitNum; + iTrisSubd[ acc ++ ] = iTri; + + } + + iTrisSubd = new Uint32Array( iTrisSubd.subarray( 0, acc ) ); + splitArr = new Uint8Array( splitArr.subarray( 0, acc ) ); + + if ( iTrisSubd.length > 5 ) { + + iTrisSubd = mesh.expandsFaces( iTrisSubd, 3 ); + const newSplit = new Uint8Array( iTrisSubd.length ); + newSplit.set( splitArr ); + splitArr = newSplit; + + } + + // Subdivide triangles + const fAr = mesh.getFaces(); + mesh.reAllocateArrays( splitArr.length ); + for ( let i = 0, l = iTrisSubd.length; i < l; ++ i ) { + + const iTri = iTrisSubd[ i ]; + let splitNum = splitArr[ i ]; + if ( splitNum === 0 ) splitNum = subFindSplit( iTri ); + const ind = iTri * 4; + if ( splitNum === 1 ) halfEdgeSplit( iTri, fAr[ ind ], fAr[ ind + 1 ], fAr[ ind + 2 ] ); + else if ( splitNum === 2 ) halfEdgeSplit( iTri, fAr[ ind + 1 ], fAr[ ind + 2 ], fAr[ ind ] ); + else if ( splitNum === 3 ) halfEdgeSplit( iTri, fAr[ ind + 2 ], fAr[ ind ], fAr[ ind + 1 ] ); + + } + + // Gather new triangles and fill cracks + let nbNewTris = mesh.getNbTriangles() - nbTrisInit; + let newTriangles = new Uint32Array( nbNewTris ); + for ( let i = 0; i < nbNewTris; ++ i ) newTriangles[ i ] = nbTrisInit + i; + newTriangles = mesh.expandsFaces( newTriangles, 1 ); + + let temp = iTris; + nbTris = iTris.length; + iTris = new Uint32Array( nbTris + newTriangles.length ); + iTris.set( temp ); + iTris.set( newTriangles, nbTris ); + + // De-duplicate + const ftf = mesh.getFacesTagFlags(); + const tagFlag = ++ Flags.TAG; + const iTrisMask = new Uint32Array( getMemory( iTris.length * 4 ), 0, iTris.length ); + let nbTriMask = 0; + for ( let i = 0, l = iTris.length; i < l; ++ i ) { + + const iTri = iTris[ i ]; + if ( ftf[ iTri ] === tagFlag ) continue; + ftf[ iTri ] = tagFlag; + iTrisMask[ nbTriMask ++ ] = iTri; + + } + + let resultTris = new Uint32Array( iTrisMask.subarray( 0, nbTriMask ) ); + + const nbTrianglesOld = mesh.getNbTriangles(); + while ( newTriangles.length > 0 ) { + + mesh.reAllocateArrays( newTriangles.length ); + newTriangles = subFillTriangles( newTriangles ); + + } + + nbNewTris = mesh.getNbTriangles() - nbTrianglesOld; + temp = resultTris; + resultTris = new Uint32Array( nbTriMask + nbNewTris ); + resultTris.set( temp ); + for ( let i = 0; i < nbNewTris; ++ i ) resultTris[ nbTriMask + i ] = nbTrianglesOld + i; + + // Smooth new vertices and tag sculpt flag + const nbVNew = mesh.getNbVertices() - nbVertsInit; + let vNew = new Uint32Array( nbVNew ); + for ( let i = 0; i < nbVNew; ++ i ) vNew[ i ] = nbVertsInit + i; + vNew = mesh.expandsVertices( vNew, 1 ); + + if ( ! SubData._linear ) { + + const expV = vNew.subarray( nbVNew ); + smoothTangentVerts( mesh, expV, 1.0 ); + + } + + const vAr = mesh.getVertices(); + const vscf = mesh.getVerticesSculptFlags(); + const cx = SubData._center[ 0 ], cy = SubData._center[ 1 ], cz = SubData._center[ 2 ]; + const sculptMask = Flags.SCULPT; + for ( let i = 0, l = vNew.length; i < l; ++ i ) { + + const ind = vNew[ i ]; + const j = ind * 3; + const dx = vAr[ j ] - cx, dy = vAr[ j + 1 ] - cy, dz = vAr[ j + 2 ] - cz; + vscf[ ind ] = ( dx * dx + dy * dy + dz * dz ) < SubData._radius2 ? sculptMask : sculptMask - 1; + + } + + return resultTris; + +} + +function subdivisionPass( mesh, iTris, center, radius2, detail2 ) { + + SubData._mesh = mesh; + SubData._linear = false; + SubData._center[ 0 ] = center[ 0 ]; SubData._center[ 1 ] = center[ 1 ]; SubData._center[ 2 ] = center[ 2 ]; + SubData._radius2 = radius2; + SubData._edgeMax2 = detail2; + + let nbTriangles = 0; + while ( nbTriangles !== mesh.getNbTriangles() ) { + + nbTriangles = mesh.getNbTriangles(); + iTris = subdivide( iTris ); + + } + + return iTris; + +} + +// ---- Decimation ---- + +const DecData = { + _mesh: null, + _iTrisToDelete: [], + _iVertsToDelete: [], + _iVertsDecimated: [] +}; + +function decDeleteTriangle( iTri ) { + + const mesh = DecData._mesh; + const vrf = mesh.getVerticesRingFace(); + const ftf = mesh.getFacesTagFlags(); + const fAr = mesh.getFaces(); + const pil = mesh.getFacePosInLeaf(); + const fleaf = mesh.getFaceLeaf(); + const fstf = mesh.getFacesStateFlags(); + + const oldPos = pil[ iTri ]; + const iTrisLeaf = fleaf[ iTri ]._iFaces; + const lastTri = iTrisLeaf[ iTrisLeaf.length - 1 ]; + if ( iTri !== lastTri ) { iTrisLeaf[ oldPos ] = lastTri; pil[ lastTri ] = oldPos; } + iTrisLeaf.pop(); + + const lastPos = mesh.getNbTriangles() - 1; + if ( lastPos === iTri ) { mesh.addNbFace( - 1 ); return; } + const id = lastPos * 4; + const iv1 = fAr[ id ], iv2 = fAr[ id + 1 ], iv3 = fAr[ id + 2 ]; + replaceElement( vrf[ iv1 ], lastPos, iTri ); + replaceElement( vrf[ iv2 ], lastPos, iTri ); + replaceElement( vrf[ iv3 ], lastPos, iTri ); + + const leafLast = fleaf[ lastPos ]; + const pilLast = pil[ lastPos ]; + leafLast._iFaces[ pilLast ] = iTri; + fleaf[ iTri ] = leafLast; + pil[ iTri ] = pilLast; + ftf[ iTri ] = ftf[ lastPos ]; + fstf[ iTri ] = fstf[ lastPos ]; + const j = iTri * 4; + fAr[ j ] = iv1; fAr[ j + 1 ] = iv2; fAr[ j + 2 ] = iv3; fAr[ j + 3 ] = TRI_INDEX; + DecData._iVertsDecimated.push( iv1, iv2, iv3 ); + mesh.addNbFace( - 1 ); + +} + +function decDeleteVertex( iVert ) { + + const mesh = DecData._mesh; + const vrv = mesh.getVerticesRingVert(); + const vrf = mesh.getVerticesRingFace(); + const vAr = mesh.getVertices(); + const nAr = mesh.getNormals(); + const cAr = mesh.getColors(); + const mAr = mesh.getMaterials(); + const fAr = mesh.getFaces(); + const vtf = mesh.getVerticesTagFlags(); + const vstf = mesh.getVerticesStateFlags(); + const vsctf = mesh.getVerticesSculptFlags(); + + const lastPos = mesh.getNbVertices() - 1; + if ( iVert === lastPos ) { mesh.addNbVertice( - 1 ); return; } + + const iTris = vrf[ lastPos ]; + const ring = vrv[ lastPos ]; + for ( let i = 0, l = iTris.length; i < l; ++ i ) { + + const id = iTris[ i ] * 4; + if ( fAr[ id ] === lastPos ) fAr[ id ] = iVert; + else if ( fAr[ id + 1 ] === lastPos ) fAr[ id + 1 ] = iVert; + else fAr[ id + 2 ] = iVert; + + } + + for ( let i = 0, l = ring.length; i < l; ++ i ) replaceElement( vrv[ ring[ i ] ], lastPos, iVert ); + + vrv[ iVert ] = vrv[ lastPos ].slice(); + vrf[ iVert ] = vrf[ lastPos ].slice(); + vtf[ iVert ] = vtf[ lastPos ]; + vstf[ iVert ] = vstf[ lastPos ]; + vsctf[ iVert ] = vsctf[ lastPos ]; + const idLast = lastPos * 3, id = iVert * 3; + vAr[ id ] = vAr[ idLast ]; vAr[ id + 1 ] = vAr[ idLast + 1 ]; vAr[ id + 2 ] = vAr[ idLast + 2 ]; + nAr[ id ] = nAr[ idLast ]; nAr[ id + 1 ] = nAr[ idLast + 1 ]; nAr[ id + 2 ] = nAr[ idLast + 2 ]; + cAr[ id ] = cAr[ idLast ]; cAr[ id + 1 ] = cAr[ idLast + 1 ]; cAr[ id + 2 ] = cAr[ idLast + 2 ]; + mAr[ id ] = mAr[ idLast ]; mAr[ id + 1 ] = mAr[ idLast + 1 ]; mAr[ id + 2 ] = mAr[ idLast + 2 ]; + mesh.addNbVertice( - 1 ); + +} + +function decEdgeCollapse( iTri1, iTri2, iv1, iv2, ivOpp1, ivOpp2, iTris ) { + + const mesh = DecData._mesh; + const vAr = mesh.getVertices(); + const nAr = mesh.getNormals(); + const cAr = mesh.getColors(); + const mAr = mesh.getMaterials(); + const fAr = mesh.getFaces(); + const vtf = mesh.getVerticesTagFlags(); + const ftf = mesh.getFacesTagFlags(); + const vrv = mesh.getVerticesRingVert(); + const vrf = mesh.getVerticesRingFace(); + + const ring1 = vrv[ iv1 ], ring2 = vrv[ iv2 ]; + const tris1 = vrf[ iv1 ], tris2 = vrf[ iv2 ]; + + if ( ring1.length !== tris1.length || ring2.length !== tris2.length ) return; + const ringOpp1 = vrv[ ivOpp1 ], ringOpp2 = vrv[ ivOpp2 ]; + const trisOpp1 = vrf[ ivOpp1 ], trisOpp2 = vrf[ ivOpp2 ]; + if ( ringOpp1.length !== trisOpp1.length || ringOpp2.length !== trisOpp2.length ) return; + + DecData._iVertsDecimated.push( iv1, iv2 ); + const sortFunc = ( a, b ) => a - b; + ring1.sort( sortFunc ); + ring2.sort( sortFunc ); + + if ( intersectionArrays( ring1, ring2 ).length >= 3 ) { + + // Edge flip + removeElement( tris1, iTri2 ); + removeElement( tris2, iTri1 ); + trisOpp1.push( iTri2 ); + trisOpp2.push( iTri1 ); + let id = iTri1 * 4; + if ( fAr[ id ] === iv2 ) fAr[ id ] = ivOpp2; + else if ( fAr[ id + 1 ] === iv2 ) fAr[ id + 1 ] = ivOpp2; + else fAr[ id + 2 ] = ivOpp2; + id = iTri2 * 4; + if ( fAr[ id ] === iv1 ) fAr[ id ] = ivOpp1; + else if ( fAr[ id + 1 ] === iv1 ) fAr[ id + 1 ] = ivOpp1; + else fAr[ id + 2 ] = ivOpp1; + mesh._computeRingVertices( iv1 ); + mesh._computeRingVertices( iv2 ); + mesh._computeRingVertices( ivOpp1 ); + mesh._computeRingVertices( ivOpp2 ); + return; + + } + + let id = iv1 * 3; + const id2 = iv2 * 3; + let nx = nAr[ id ] + nAr[ id2 ], ny = nAr[ id + 1 ] + nAr[ id2 + 1 ], nz = nAr[ id + 2 ] + nAr[ id2 + 2 ]; + let len = nx * nx + ny * ny + nz * nz; + if ( len === 0 ) { nx = 1; } else { len = 1 / Math.sqrt( len ); nx *= len; ny *= len; nz *= len; } + nAr[ id ] = nx; nAr[ id + 1 ] = ny; nAr[ id + 2 ] = nz; + cAr[ id ] = ( cAr[ id ] + cAr[ id2 ] ) * 0.5; + cAr[ id + 1 ] = ( cAr[ id + 1 ] + cAr[ id2 + 1 ] ) * 0.5; + cAr[ id + 2 ] = ( cAr[ id + 2 ] + cAr[ id2 + 2 ] ) * 0.5; + mAr[ id ] = ( mAr[ id ] + mAr[ id2 ] ) * 0.5; + mAr[ id + 1 ] = ( mAr[ id + 1 ] + mAr[ id2 + 1 ] ) * 0.5; + mAr[ id + 2 ] = ( mAr[ id + 2 ] + mAr[ id2 + 2 ] ) * 0.5; + + removeElement( tris1, iTri1 ); removeElement( tris1, iTri2 ); + removeElement( tris2, iTri1 ); removeElement( tris2, iTri2 ); + removeElement( trisOpp1, iTri1 ); removeElement( trisOpp2, iTri2 ); + + for ( let i = 0, l = tris2.length; i < l; ++ i ) { + + const tri2 = tris2[ i ]; + tris1.push( tri2 ); + const idx = tri2 * 4; + if ( fAr[ idx ] === iv2 ) fAr[ idx ] = iv1; + else if ( fAr[ idx + 1 ] === iv2 ) fAr[ idx + 1 ] = iv1; + else fAr[ idx + 2 ] = iv1; + + } + + for ( let i = 0, l = ring2.length; i < l; ++ i ) ring1.push( ring2[ i ] ); + + mesh._computeRingVertices( iv1 ); + + // Flat smooth + let meanX = 0, meanY = 0, meanZ = 0; + const nbRing1 = ring1.length; + for ( let i = 0; i < nbRing1; ++ i ) { + + const ivRing = ring1[ i ]; + mesh._computeRingVertices( ivRing ); + const ivr3 = ivRing * 3; + meanX += vAr[ ivr3 ]; meanY += vAr[ ivr3 + 1 ]; meanZ += vAr[ ivr3 + 2 ]; + + } + + meanX /= nbRing1; meanY /= nbRing1; meanZ /= nbRing1; + const dotN = nx * ( meanX - vAr[ id ] ) + ny * ( meanY - vAr[ id + 1 ] ) + nz * ( meanZ - vAr[ id + 2 ] ); + vAr[ id ] = meanX - nx * dotN; + vAr[ id + 1 ] = meanY - ny * dotN; + vAr[ id + 2 ] = meanZ - nz * dotN; + + vtf[ iv2 ] = ftf[ iTri1 ] = ftf[ iTri2 ] = - 1; + DecData._iVertsToDelete.push( iv2 ); + DecData._iTrisToDelete.push( iTri1, iTri2 ); + + for ( let i = 0, l = tris1.length; i < l; ++ i ) iTris.push( tris1[ i ] ); + +} + +function decDecimateTriangles( iTri1, iTri2, iTris ) { + + if ( iTri2 === - 1 ) return; + const fAr = DecData._mesh.getFaces(); + const id1 = iTri1 * 4, id2 = iTri2 * 4; + const iv11 = fAr[ id1 ], iv21 = fAr[ id1 + 1 ], iv31 = fAr[ id1 + 2 ]; + const iv12 = fAr[ id2 ], iv22 = fAr[ id2 + 1 ], iv32 = fAr[ id2 + 2 ]; + + if ( iv11 === iv12 ) { + + if ( iv21 === iv32 ) decEdgeCollapse( iTri1, iTri2, iv11, iv21, iv31, iv22, iTris ); + else decEdgeCollapse( iTri1, iTri2, iv11, iv31, iv21, iv32, iTris ); + + } else if ( iv11 === iv22 ) { + + if ( iv21 === iv12 ) decEdgeCollapse( iTri1, iTri2, iv11, iv21, iv31, iv32, iTris ); + else decEdgeCollapse( iTri1, iTri2, iv11, iv31, iv21, iv12, iTris ); + + } else if ( iv11 === iv32 ) { + + if ( iv21 === iv22 ) decEdgeCollapse( iTri1, iTri2, iv11, iv21, iv31, iv12, iTris ); + else decEdgeCollapse( iTri1, iTri2, iv11, iv31, iv21, iv22, iTris ); + + } else if ( iv21 === iv12 ) decEdgeCollapse( iTri1, iTri2, iv31, iv21, iv11, iv22, iTris ); + else if ( iv21 === iv22 ) decEdgeCollapse( iTri1, iTri2, iv31, iv21, iv11, iv32, iTris ); + else decEdgeCollapse( iTri1, iTri2, iv31, iv21, iv11, iv12, iTris ); + +} + +function decFindOppositeTriangle( iTri, iv1, iv2 ) { + + const vrf = DecData._mesh.getVerticesRingFace(); + const iTris1 = vrf[ iv1 ].slice().sort( ( a, b ) => a - b ); + const iTris2 = vrf[ iv2 ].slice().sort( ( a, b ) => a - b ); + const res = intersectionArrays( iTris1, iTris2 ); + if ( res.length !== 2 ) return - 1; + return res[ 0 ] === iTri ? res[ 1 ] : res[ 0 ]; + +} + +function decimationPass( mesh, iTris, center, radius2, detail2 ) { + + DecData._mesh = mesh; + DecData._iVertsDecimated.length = 0; + DecData._iTrisToDelete.length = 0; + DecData._iVertsToDelete.length = 0; + + const radius = Math.sqrt( radius2 ); + const ftf = mesh.getFacesTagFlags(); + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const fAr = mesh.getFaces(); + const cenx = center[ 0 ], ceny = center[ 1 ], cenz = center[ 2 ]; + + const nbInit = iTris.length; + const dynArr = new Array( nbInit ); + for ( let i = 0; i < nbInit; ++ i ) dynArr[ i ] = iTris[ i ]; + + for ( let i = 0; i < dynArr.length; ++ i ) { + + const iTri = dynArr[ i ]; + if ( ftf[ iTri ] < 0 ) continue; + const id = iTri * 4; + const iv1 = fAr[ id ], iv2 = fAr[ id + 1 ], iv3 = fAr[ id + 2 ]; + const ind1 = iv1 * 3, ind2 = iv2 * 3, ind3 = iv3 * 3; + const v1x = vAr[ ind1 ], v1y = vAr[ ind1 + 1 ], v1z = vAr[ ind1 + 2 ]; + const v2x = vAr[ ind2 ], v2y = vAr[ ind2 + 1 ], v2z = vAr[ ind2 + 2 ]; + const v3x = vAr[ ind3 ], v3y = vAr[ ind3 + 1 ], v3z = vAr[ ind3 + 2 ]; + + let dx = ( v1x + v2x + v3x ) / 3.0 - cenx; + let dy = ( v1y + v2y + v3y ) / 3.0 - ceny; + let dz = ( v1z + v2z + v3z ) / 3.0 - cenz; + let fallOff = dx * dx + dy * dy + dz * dz; + + if ( fallOff < radius2 ) fallOff = 1.0; + else if ( fallOff < radius2 * 2.0 ) { + + fallOff = ( Math.sqrt( fallOff ) - radius ) / ( radius * Math.SQRT2 - radius ); + const f2 = fallOff * fallOff; + fallOff = 3.0 * f2 * f2 - 4.0 * f2 * fallOff + 1.0; + + } else continue; + + dx = v2x - v1x; dy = v2y - v1y; dz = v2z - v1z; + const len1 = dx * dx + dy * dy + dz * dz; + dx = v2x - v3x; dy = v2y - v3y; dz = v2z - v3z; + const len2 = dx * dx + dy * dy + dz * dz; + dx = v1x - v3x; dy = v1y - v3y; dz = v1z - v3z; + const len3 = dx * dx + dy * dy + dz * dz; + + const m1 = mAr[ ind1 + 2 ], m2 = mAr[ ind2 + 2 ], m3 = mAr[ ind3 + 2 ]; + if ( len1 < len2 && len1 < len3 ) { + + if ( len1 < detail2 * fallOff * ( m1 + m2 ) * 0.5 ) + decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv1, iv2 ), dynArr ); + + } else if ( len2 < len3 ) { + + if ( len2 < detail2 * fallOff * ( m2 + m3 ) * 0.5 ) + decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv2, iv3 ), dynArr ); + + } else { + + if ( len3 < detail2 * fallOff * ( m1 + m3 ) * 0.5 ) + decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv1, iv3 ), dynArr ); + + } + + } + + // Apply deletion + tidy( DecData._iTrisToDelete ); + for ( let i = DecData._iTrisToDelete.length - 1; i >= 0; -- i ) decDeleteTriangle( DecData._iTrisToDelete[ i ] ); + tidy( DecData._iVertsToDelete ); + for ( let i = DecData._iVertsToDelete.length - 1; i >= 0; -- i ) decDeleteVertex( DecData._iVertsToDelete[ i ] ); + + // Get valid modified triangles + const iVertsDecimated = DecData._iVertsDecimated; + const nbVertices = mesh.getNbVertices(); + const vtfDec = mesh.getVerticesTagFlags(); + let tagFlag = ++ Flags.TAG; + const validVertices = new Uint32Array( getMemory( iVertsDecimated.length * 4 ), 0, iVertsDecimated.length ); + let nbValid = 0; + for ( let i = 0, l = iVertsDecimated.length; i < l; ++ i ) { + + const iVert = iVertsDecimated[ i ]; + if ( iVert >= nbVertices || vtfDec[ iVert ] === tagFlag ) continue; + vtfDec[ iVert ] = tagFlag; + validVertices[ nbValid ++ ] = iVert; + + } + + const newTris = mesh.getFacesFromVertices( new Uint32Array( validVertices.subarray( 0, nbValid ) ) ); + const temp = dynArr; + const nbTris = temp.length; + const combined = new Uint32Array( nbTris + newTris.length ); + for ( let i = 0; i < nbTris; ++ i ) combined[ i ] = temp[ i ]; + combined.set( newTris, nbTris ); + + tagFlag = ++ Flags.TAG; + const nbTriangles = mesh.getNbTriangles(); + const validTris = new Uint32Array( getMemory( combined.length * 4 ), 0, combined.length ); + let nbValidTris = 0; + for ( let i = 0, l = combined.length; i < l; ++ i ) { + + const t = combined[ i ]; + if ( t >= nbTriangles || ftf[ t ] === tagFlag ) continue; + ftf[ t ] = tagFlag; + validTris[ nbValidTris ++ ] = t; + + } + + return new Uint32Array( validTris.subarray( 0, nbValidTris ) ); + +} + +// ---- Tool Helpers (shared across all tools) ---- + +function laplacianSmooth( mesh, iVerts, smoothVerts, vField ) { + + const vrings = mesh.getVerticesRingVert(); + const vertOnEdge = mesh.getVerticesOnEdge(); + const vAr = vField || mesh.getVertices(); + const nbVerts = iVerts.length; + + for ( let i = 0; i < nbVerts; ++ i ) { + + const i3 = i * 3; + const id = iVerts[ i ]; + const ring = vrings[ id ]; + const vcount = ring.length; + if ( vcount <= 2 ) { + + const idv = id * 3; + smoothVerts[ i3 ] = vAr[ idv ]; smoothVerts[ i3 + 1 ] = vAr[ idv + 1 ]; smoothVerts[ i3 + 2 ] = vAr[ idv + 2 ]; + continue; + + } + + let avx = 0, avy = 0, avz = 0; + if ( vertOnEdge[ id ] === 1 ) { + + let nbVertEdge = 0; + for ( let j = 0, l = vcount; j < l; ++ j ) { + + const idv = ring[ j ]; + if ( vertOnEdge[ idv ] === 1 ) { + + const idv3 = idv * 3; + avx += vAr[ idv3 ]; avy += vAr[ idv3 + 1 ]; avz += vAr[ idv3 + 2 ]; + ++ nbVertEdge; + + } + + } + + if ( nbVertEdge >= 2 ) { + + smoothVerts[ i3 ] = avx / nbVertEdge; smoothVerts[ i3 + 1 ] = avy / nbVertEdge; smoothVerts[ i3 + 2 ] = avz / nbVertEdge; + continue; + + } + + avx = avy = avz = 0; + + } + + for ( let j = 0; j < vcount; ++ j ) { + + const idv = ring[ j ] * 3; + avx += vAr[ idv ]; avy += vAr[ idv + 1 ]; avz += vAr[ idv + 2 ]; + + } + + smoothVerts[ i3 ] = avx / vcount; smoothVerts[ i3 + 1 ] = avy / vcount; smoothVerts[ i3 + 2 ] = avz / vcount; + + } + +} + +function smoothTangentVerts( mesh, iVerts, intensity ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const nAr = mesh.getNormals(); + const nbVerts = iVerts.length; + const smoothVerts = new Float32Array( getMemory( nbVerts * 4 * 3 ), 0, nbVerts * 3 ); + laplacianSmooth( mesh, iVerts, smoothVerts ); + for ( let i = 0; i < nbVerts; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + let nx = nAr[ ind ], ny = nAr[ ind + 1 ], nz = nAr[ ind + 2 ]; + let len = nx * nx + ny * ny + nz * nz; + if ( len === 0 ) continue; + len = 1 / Math.sqrt( len ); + nx *= len; ny *= len; nz *= len; + const i3 = i * 3; + const smx = smoothVerts[ i3 ], smy = smoothVerts[ i3 + 1 ], smz = smoothVerts[ i3 + 2 ]; + const d = nx * ( smx - vx ) + ny * ( smy - vy ) + nz * ( smz - vz ); + const mI = Math.min( intensity * mAr[ ind + 2 ], 1.0 ); + vAr[ ind ] = vx + ( smx - nx * d - vx ) * mI; + vAr[ ind + 1 ] = vy + ( smy - ny * d - vy ) * mI; + vAr[ ind + 2 ] = vz + ( smz - nz * d - vz ) * mI; + + } + +} + +function getFrontVertices( mesh, iVertsInRadius, eyeDir ) { + + const nbVerts = iVertsInRadius.length; + const iVertsFront = new Uint32Array( getMemory( 4 * nbVerts ), 0, nbVerts ); + let acc = 0; + const nAr = mesh.getNormals(); + const ex = eyeDir[ 0 ], ey = eyeDir[ 1 ], ez = eyeDir[ 2 ]; + for ( let i = 0; i < nbVerts; ++ i ) { + + const id = iVertsInRadius[ i ]; + const j = id * 3; + if ( nAr[ j ] * ex + nAr[ j + 1 ] * ey + nAr[ j + 2 ] * ez <= 0 ) iVertsFront[ acc ++ ] = id; + + } + + return new Uint32Array( iVertsFront.subarray( 0, acc ) ); + +} + +function areaNormal( mesh, iVerts ) { + + const nAr = mesh.getNormals(); + const mAr = mesh.getMaterials(); + let anx = 0, any = 0, anz = 0; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const f = mAr[ ind + 2 ]; + anx += nAr[ ind ] * f; any += nAr[ ind + 1 ] * f; anz += nAr[ ind + 2 ] * f; + + } + + const len = Math.sqrt( anx * anx + any * any + anz * anz ); + if ( len === 0 ) return null; + const inv = 1.0 / len; + return [ anx * inv, any * inv, anz * inv ]; + +} + +function areaCenter( mesh, iVerts ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + let ax = 0, ay = 0, az = 0, acc = 0; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const f = mAr[ ind + 2 ]; + acc += f; + ax += vAr[ ind ] * f; ay += vAr[ ind + 1 ] * f; az += vAr[ ind + 2 ] * f; + + } + + return [ ax / acc, ay / acc, az / acc ]; + +} + +// ---- Tool implementations ---- + +function toolBrush( mesh, iVerts, aNormal, center, radiusSq, intensity, negative ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + let deform = intensity * radius * 0.1; + if ( negative ) deform = - deform; + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + const anx = aNormal[ 0 ], any = aNormal[ 1 ], anz = aNormal[ 2 ]; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const dx = vAr[ ind ] - cx, dy = vAr[ ind + 1 ] - cy, dz = vAr[ ind + 2 ] - cz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + if ( dist >= 1.0 ) continue; + const fallOff = falloff( dist ) * mAr[ ind + 2 ] * deform; + vAr[ ind ] += anx * fallOff; + vAr[ ind + 1 ] += any * fallOff; + vAr[ ind + 2 ] += anz * fallOff; + + } + +} + +function toolFlatten( mesh, iVerts, aNormal, aCenter2, center, radiusSq, intensity, negative ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + const ax = aCenter2[ 0 ], ay = aCenter2[ 1 ], az = aCenter2[ 2 ]; + const anx = aNormal[ 0 ], any = aNormal[ 1 ], anz = aNormal[ 2 ]; + const comp = negative ? - 1 : 1; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const distToPlane = ( vx - ax ) * anx + ( vy - ay ) * any + ( vz - az ) * anz; + if ( distToPlane * comp > 0 ) continue; + const dx = vx - cx, dy = vy - cy, dz = vz - cz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + if ( dist >= 1.0 ) continue; + const fallOff = falloff( dist ) * distToPlane * intensity * mAr[ ind + 2 ]; + vAr[ ind ] -= anx * fallOff; + vAr[ ind + 1 ] -= any * fallOff; + vAr[ ind + 2 ] -= anz * fallOff; + + } + +} + +function toolInflate( mesh, iVerts, center, radiusSq, intensity, negative ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const nAr = mesh.getNormals(); + const radius = Math.sqrt( radiusSq ); + let deform = intensity * radius * 0.1; + if ( negative ) deform = - deform; + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const dx = vAr[ ind ] - cx, dy = vAr[ ind + 1 ] - cy, dz = vAr[ ind + 2 ] - cz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + if ( dist >= 1.0 ) continue; + let fallOff = falloff( dist ) * deform; + const nx = nAr[ ind ], ny = nAr[ ind + 1 ], nz = nAr[ ind + 2 ]; + const nLen = Math.sqrt( nx * nx + ny * ny + nz * nz ); + if ( nLen > 0 ) fallOff /= nLen; + fallOff *= mAr[ ind + 2 ]; + vAr[ ind ] += nx * fallOff; + vAr[ ind + 1 ] += ny * fallOff; + vAr[ ind + 2 ] += nz * fallOff; + + } + +} + +function toolSmooth( mesh, iVerts, intensity ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const nbVerts = iVerts.length; + const smoothVerts = new Float32Array( getMemory( nbVerts * 4 * 3 ), 0, nbVerts * 3 ); + laplacianSmooth( mesh, iVerts, smoothVerts ); + for ( let i = 0; i < nbVerts; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const i3 = i * 3; + const mI = Math.min( intensity * mAr[ ind + 2 ], 1.0 ); + const intComp = 1.0 - mI; + vAr[ ind ] = vx * intComp + smoothVerts[ i3 ] * mI; + vAr[ ind + 1 ] = vy * intComp + smoothVerts[ i3 + 1 ] * mI; + vAr[ ind + 2 ] = vz * intComp + smoothVerts[ i3 + 2 ] * mI; + + } + +} + +function toolPinch( mesh, iVerts, center, radiusSq, intensity, negative ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + let deform = intensity * 0.05; + if ( negative ) deform = - deform; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const dx = cx - vx, dy = cy - vy, dz = cz - vz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + const fallOff = falloff( dist ) * deform * mAr[ ind + 2 ]; + vAr[ ind ] = vx + dx * fallOff; + vAr[ ind + 1 ] = vy + dy * fallOff; + vAr[ ind + 2 ] = vz + dz * fallOff; + + } + +} + +function toolCrease( mesh, iVerts, aNormal, center, radiusSq, intensity, negative ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + const anx = aNormal[ 0 ], any = aNormal[ 1 ], anz = aNormal[ 2 ]; + const deform = intensity * 0.07; + let brushFactor = deform * radius; + if ( negative ) brushFactor = - brushFactor; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const dx = cx - vAr[ ind ], dy = cy - vAr[ ind + 1 ], dz = cz - vAr[ ind + 2 ]; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + if ( dist >= 1.0 ) continue; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const fallOff = falloff( dist ) * mAr[ ind + 2 ]; + const brushMod = Math.pow( fallOff, 5 ) * brushFactor; + const pinchF = fallOff * deform; + vAr[ ind ] = vx + dx * pinchF + anx * brushMod; + vAr[ ind + 1 ] = vy + dy * pinchF + any * brushMod; + vAr[ ind + 2 ] = vz + dz * pinchF + anz * brushMod; + + } + +} + +function toolDrag( mesh, iVerts, center, radiusSq, dragDir ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + const dirx = dragDir[ 0 ], diry = dragDir[ 1 ], dirz = dragDir[ 2 ]; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const dx = vx - cx, dy = vy - cy, dz = vz - cz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + const fallOff = falloff( dist ) * mAr[ ind + 2 ]; + vAr[ ind ] = vx + dirx * fallOff; + vAr[ ind + 1 ] = vy + diry * fallOff; + vAr[ ind + 2 ] = vz + dirz * fallOff; + + } + +} + +function toolScale( mesh, iVerts, center, radiusSq, deltaScale ) { + + const vAr = mesh.getVertices(); + const mAr = mesh.getMaterials(); + const radius = Math.sqrt( radiusSq ); + const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; + const scale = deltaScale * 0.01; + for ( let i = 0, l = iVerts.length; i < l; ++ i ) { + + const ind = iVerts[ i ] * 3; + const vx = vAr[ ind ], vy = vAr[ ind + 1 ], vz = vAr[ ind + 2 ]; + const dx = vx - cx, dy = vy - cy, dz = vz - cz; + const dist = Math.sqrt( dx * dx + dy * dy + dz * dz ) / radius; + const fallOff = falloff( dist ) * scale * mAr[ ind + 2 ]; + vAr[ ind ] = vx + dx * fallOff; + vAr[ ind + 1 ] = vy + dy * fallOff; + vAr[ ind + 2 ] = vz + dz * fallOff; + + } + +} + +export { + subdivisionPass, + decimationPass, + getFrontVertices, + areaNormal, + areaCenter, + toolBrush, + toolFlatten, + toolInflate, + toolSmooth, + toolPinch, + toolCrease, + toolDrag, + toolScale +}; diff --git a/examples/jsm/sculpt/SculptUtils.js b/examples/jsm/sculpt/SculptUtils.js new file mode 100644 index 00000000000000..86bc2acdbb3ec0 --- /dev/null +++ b/examples/jsm/sculpt/SculptUtils.js @@ -0,0 +1,249 @@ +// ---- Constants & Utilities ---- + +const TRI_INDEX = 4294967295; + +// Mutable flags shared across modules +const Flags = { + TAG: 1, + SCULPT: 1, + STATE: 1 +}; + +const _memoryPool = { buffer: new ArrayBuffer( 100000 ) }; + +function getMemory( nbBytes ) { + + if ( _memoryPool.buffer.byteLength >= nbBytes ) return _memoryPool.buffer; + _memoryPool.buffer = new ArrayBuffer( nbBytes ); + return _memoryPool.buffer; + +} + +function replaceElement( array, oldValue, newValue ) { + + for ( let i = 0, l = array.length; i < l; ++ i ) { + + if ( array[ i ] === oldValue ) { + + array[ i ] = newValue; + return; + + } + + } + +} + +function removeElement( array, remValue ) { + + for ( let i = 0, l = array.length; i < l; ++ i ) { + + if ( array[ i ] === remValue ) { + + array[ i ] = array[ l - 1 ]; + array.pop(); + return; + + } + + } + +} + +function tidy( array ) { + + array.sort( ( a, b ) => a - b ); + const len = array.length; + let j = 0; + for ( let i = 1; i < len; ++ i ) { + + if ( array[ j ] !== array[ i ] ) array[ ++ j ] = array[ i ]; + + } + + if ( len > 1 ) array.length = j + 1; + +} + +function intersectionArrays( a, b ) { + + let ai = 0, bi = 0; + const result = []; + const aLen = a.length, bLen = b.length; + while ( ai < aLen && bi < bLen ) { + + if ( a[ ai ] < b[ bi ] ) ai ++; + else if ( a[ ai ] > b[ bi ] ) bi ++; + else { + + result.push( a[ ai ] ); + ++ ai; + ++ bi; + + } + + } + + return result; + +} + +// ---- Geometry Helpers ---- + +const _edge1 = [ 0, 0, 0 ]; +const _edge2 = [ 0, 0, 0 ]; +const _pvec = [ 0, 0, 0 ]; +const _tvec = [ 0, 0, 0 ]; +const _qvec = [ 0, 0, 0 ]; + +function cross( out, a, b ) { + + out[ 0 ] = a[ 1 ] * b[ 2 ] - a[ 2 ] * b[ 1 ]; + out[ 1 ] = a[ 2 ] * b[ 0 ] - a[ 0 ] * b[ 2 ]; + out[ 2 ] = a[ 0 ] * b[ 1 ] - a[ 1 ] * b[ 0 ]; + return out; + +} + +function dot( a, b ) { + + return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ]; + +} + +function sub( out, a, b ) { + + out[ 0 ] = a[ 0 ] - b[ 0 ]; + out[ 1 ] = a[ 1 ] - b[ 1 ]; + out[ 2 ] = a[ 2 ] - b[ 2 ]; + return out; + +} + +function sqrLen( a ) { + + return a[ 0 ] * a[ 0 ] + a[ 1 ] * a[ 1 ] + a[ 2 ] * a[ 2 ]; + +} + +function sqrDist( a, b ) { + + const dx = a[ 0 ] - b[ 0 ], dy = a[ 1 ] - b[ 1 ], dz = a[ 2 ] - b[ 2 ]; + return dx * dx + dy * dy + dz * dz; + +} + +function intersectionRayTriangle( orig, dir, v1, v2, v3, vertInter ) { + + sub( _edge1, v2, v1 ); + sub( _edge2, v3, v1 ); + cross( _pvec, dir, _edge2 ); + const det = dot( _edge1, _pvec ); + const EPSILON = 1e-15; + if ( det > - EPSILON && det < EPSILON ) return - 1.0; + const invDet = 1.0 / det; + sub( _tvec, orig, v1 ); + const u = dot( _tvec, _pvec ) * invDet; + if ( u < - EPSILON || u > 1.0 + EPSILON ) return - 1.0; + cross( _qvec, _tvec, _edge1 ); + const v = dot( dir, _qvec ) * invDet; + if ( v < - EPSILON || u + v > 1.0 + EPSILON ) return - 1.0; + const t = dot( _edge2, _qvec ) * invDet; + if ( t < - EPSILON ) return - 1.0; + if ( vertInter ) { + + vertInter[ 0 ] = orig[ 0 ] + dir[ 0 ] * t; + vertInter[ 1 ] = orig[ 1 ] + dir[ 1 ] * t; + vertInter[ 2 ] = orig[ 2 ] + dir[ 2 ] * t; + + } + + return t; + +} + +function distanceSqToSegment( point, v1, v2 ) { + + const ptx = point[ 0 ] - v1[ 0 ], pty = point[ 1 ] - v1[ 1 ], ptz = point[ 2 ] - v1[ 2 ]; + const vx = v2[ 0 ] - v1[ 0 ], vy = v2[ 1 ] - v1[ 1 ], vz = v2[ 2 ] - v1[ 2 ]; + const len = vx * vx + vy * vy + vz * vz; + const t2 = ( ptx * vx + pty * vy + ptz * vz ) / len; + if ( t2 < 0.0 ) return ptx * ptx + pty * pty + ptz * ptz; + if ( t2 > 1.0 ) { + + const dx = point[ 0 ] - v2[ 0 ], dy = point[ 1 ] - v2[ 1 ], dz = point[ 2 ] - v2[ 2 ]; + return dx * dx + dy * dy + dz * dz; + + } + + const rx = point[ 0 ] - v1[ 0 ] - t2 * vx; + const ry = point[ 1 ] - v1[ 1 ] - t2 * vy; + const rz = point[ 2 ] - v1[ 2 ] - t2 * vz; + return rx * rx + ry * ry + rz * rz; + +} + +function triangleInsideSphere( point, radiusSq, v1, v2, v3 ) { + + if ( distanceSqToSegment( point, v1, v2 ) < radiusSq ) return true; + if ( distanceSqToSegment( point, v2, v3 ) < radiusSq ) return true; + if ( distanceSqToSegment( point, v1, v3 ) < radiusSq ) return true; + return false; + +} + +const _pit1 = [ 0, 0, 0 ]; +const _pit2 = [ 0, 0, 0 ]; +const _pitp1 = [ 0, 0, 0 ]; +const _pitp2 = [ 0, 0, 0 ]; +const _pitx = [ 0, 0, 0 ]; + +function pointInsideTriangle( point, v1, v2, v3 ) { + + _pit1[ 0 ] = v1[ 0 ] - v2[ 0 ]; _pit1[ 1 ] = v1[ 1 ] - v2[ 1 ]; _pit1[ 2 ] = v1[ 2 ] - v2[ 2 ]; + _pit2[ 0 ] = v1[ 0 ] - v3[ 0 ]; _pit2[ 1 ] = v1[ 1 ] - v3[ 1 ]; _pit2[ 2 ] = v1[ 2 ] - v3[ 2 ]; + _pitp1[ 0 ] = point[ 0 ] - v2[ 0 ]; _pitp1[ 1 ] = point[ 1 ] - v2[ 1 ]; _pitp1[ 2 ] = point[ 2 ] - v2[ 2 ]; + _pitp2[ 0 ] = point[ 0 ] - v3[ 0 ]; _pitp2[ 1 ] = point[ 1 ] - v3[ 1 ]; _pitp2[ 2 ] = point[ 2 ] - v3[ 2 ]; + const total = Math.sqrt( sqrLen( cross( _pitx, _pit1, _pit2 ) ) ); + const area1 = Math.sqrt( sqrLen( cross( _pitx, _pit1, _pitp1 ) ) ); + const area2 = Math.sqrt( sqrLen( cross( _pitx, _pit2, _pitp2 ) ) ); + const area3 = Math.sqrt( sqrLen( cross( _pitx, _pitp1, _pitp2 ) ) ); + return Math.abs( total - ( area1 + area2 + area3 ) ) < 1e-20; + +} + +function falloff( dist ) { + + const d2 = dist * dist; + return 3.0 * d2 * d2 - 4.0 * d2 * dist + 1.0; + +} + +function vertexOnLine( vertex, vNear, vFar ) { + + const abx = vFar[ 0 ] - vNear[ 0 ], aby = vFar[ 1 ] - vNear[ 1 ], abz = vFar[ 2 ] - vNear[ 2 ]; + const px = vertex[ 0 ] - vNear[ 0 ], py = vertex[ 1 ] - vNear[ 1 ], pz = vertex[ 2 ] - vNear[ 2 ]; + const d = ( abx * px + aby * py + abz * pz ) / ( abx * abx + aby * aby + abz * abz ); + return [ vNear[ 0 ] + abx * d, vNear[ 1 ] + aby * d, vNear[ 2 ] + abz * d ]; + +} + +export { + TRI_INDEX, + Flags, + getMemory, + replaceElement, + removeElement, + tidy, + intersectionArrays, + cross, + dot, + sub, + sqrLen, + sqrDist, + intersectionRayTriangle, + triangleInsideSphere, + pointInsideTriangle, + falloff, + vertexOnLine +}; diff --git a/examples/screenshots/webgl_sculpt.jpg b/examples/screenshots/webgl_sculpt.jpg new file mode 100644 index 00000000000000..f5c2a0cacd3152 Binary files /dev/null and b/examples/screenshots/webgl_sculpt.jpg differ diff --git a/examples/screenshots/webxr_xr_sculpt.jpg b/examples/screenshots/webxr_xr_sculpt.jpg new file mode 100644 index 00000000000000..60b523858fc60a Binary files /dev/null and b/examples/screenshots/webxr_xr_sculpt.jpg differ diff --git a/examples/webgl_sculpt.html b/examples/webgl_sculpt.html new file mode 100644 index 00000000000000..7381700a8b2a12 --- /dev/null +++ b/examples/webgl_sculpt.html @@ -0,0 +1,308 @@ + + + + three.js webgl - sculpt + + + + + + + +
+ three.js webgl - sculpt +
+ + + + + + diff --git a/examples/webxr_xr_sculpt.html b/examples/webxr_xr_sculpt.html new file mode 100644 index 00000000000000..c00538978d3fd0 --- /dev/null +++ b/examples/webxr_xr_sculpt.html @@ -0,0 +1,273 @@ + + + + three.js xr - sculpt + + + + + + +
+ three.js xr - sculpt
+ Trigger to sculpt. Squeeze to toggle negative. Thumbstick to cycle tools. +
+ + + + + +