UNPKG

three

Version:

JavaScript 3D library

1,181 lines (865 loc) 34.2 kB
/*! * Portions adapted from SculptGL by Stéphane Ginier. * Copyright (c) 2019 Stéphane GINIER * Licensed under the MIT License; see ./SculptGL.LICENSE.txt. */ import { TRI_INDEX, getMemory, replaceElement, removeElement, tidy, sqrDist, triangleInsideSphere, falloff } from './SculptorUtils.js'; // Subdivision const SubData = { _mesh: null, _verticesMap: new Map(), _edgeKeyStride: 0, _stringEdgeKeys: false, _center: [ 0, 0, 0 ], _radius2: 0, _edgeMax2: 0 }; const _subV1 = [ 0, 0, 0 ]; const _subV2 = [ 0, 0, 0 ]; const _subV3 = [ 0, 0, 0 ]; const MAX_EDGE_KEY_STRIDE = Math.floor( Math.sqrt( Number.MAX_SAFE_INTEGER ) ); function subEdgeKey( iv1, iv2 ) { const low = Math.min( iv1, iv2 ); const high = Math.max( iv1, iv2 ); return SubData._stringEdgeKeys ? low + '+' + high : low * SubData._edgeKeyStride + high; } 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 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; 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( subEdgeKey( iv1, iv2 ) ); const val2 = vMap.get( subEdgeKey( iv2, iv3 ) ); const val3 = vMap.get( subEdgeKey( 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 iTrisNext.slice( 0, nbNext ); } function halfEdgeSplit( iTri, iv1, iv2, iv3 ) { const mesh = SubData._mesh; const vAr = mesh.getVertices(); const nAr = mesh.getNormals(); const fAr = mesh.getFaces(); const pil = mesh.getFacePosInLeaf(); const fleaf = mesh.getFaceLeaf(); const vrv = mesh.getVerticesRingVert(); const vrf = mesh.getVerticesRingFace(); const vMap = SubData._verticesMap; const key = subEdgeKey( 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; 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; 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; } const 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 * ( nn1x - nn2x ) + ey * ( nn1y - nn2y ) + ez * ( nn1z - nn2z ) ) < 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; 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 id = iTri * 4; const ind1 = fAr[ id ] * 3, ind2 = fAr[ id + 1 ] * 3, ind3 = fAr[ id + 2 ] * 3; _subV1[ 0 ] = vAr[ ind1 ]; _subV1[ 1 ] = vAr[ ind1 + 1 ]; _subV1[ 2 ] = vAr[ ind1 + 2 ]; _subV2[ 0 ] = vAr[ ind2 ]; _subV2[ 1 ] = vAr[ ind2 + 1 ]; _subV2[ 2 ] = vAr[ ind2 + 2 ]; _subV3[ 0 ] = vAr[ ind3 ]; _subV3[ 1 ] = vAr[ ind3 + 1 ]; _subV3[ 2 ] = vAr[ ind3 + 2 ]; if ( checkInsideSphere && ! triangleInsideSphere( SubData._center, SubData._radius2, _subV1, _subV2, _subV3 ) ) return 0; const length1 = sqrDist( _subV1, _subV2 ), length2 = sqrDist( _subV2, _subV3 ), length3 = sqrDist( _subV1, _subV3 ); if ( length1 > length2 && length1 > length3 ) return length1 > SubData._edgeMax2 ? 1 : 0; else if ( length2 > length3 ) return length2 > SubData._edgeMax2 ? 2 : 0; else return length3 > SubData._edgeMax2 ? 3 : 0; } function subdivide( iTris ) { const mesh = SubData._mesh; const nbVertsInit = mesh.getNbVertices(); const nbTrisInit = mesh.getNbTriangles(); SubData._verticesMap.clear(); let nbTris = iTris.length; const 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; } if ( acc === 0 ) { mesh.reAllocateArrays( 0 ); return iTris; } iTrisSubd = iTrisSubd.slice( 0, acc ); splitArr = splitArr.slice( 0, acc ); if ( iTrisSubd.length > 5 ) { iTrisSubd = mesh.expandsFaces( iTrisSubd, 3 ); const newSplit = new Uint8Array( iTrisSubd.length ); newSplit.set( splitArr ); splitArr = newSplit; } // Reserve one new vertex per selected triangle for collision-free edge keys. // Pack vertex pairs into safe integers; use strings when the range is too large. SubData._edgeKeyStride = mesh.getNbVertices() + iTrisSubd.length + 1; SubData._stringEdgeKeys = SubData._edgeKeyStride > MAX_EDGE_KEY_STRIDE; 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 ] ); } 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 ); const ftf = mesh.getFacesTagFlags(); const tagFlag = mesh.nextTagFlag(); 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 = iTrisMask.slice( 0, nbTriMask ); // Split neighboring faces to close subdivision cracks. 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 neighboring vertices before updating sculpt flags. 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 ); 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 = mesh.getSculptFlag(); 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._center[ 0 ] = center[ 0 ]; SubData._center[ 1 ] = center[ 1 ]; SubData._center[ 2 ] = center[ 2 ]; SubData._radius2 = radius2; SubData._edgeMax2 = detail2; try { let nbTriangles = 0; while ( nbTriangles !== mesh.getNbTriangles() ) { nbTriangles = mesh.getNbTriangles(); iTris = subdivide( iTris ); } return iTris; } finally { SubData._mesh = null; SubData._verticesMap.clear(); } } // Decimation const DecData = { _mesh: null, _iTrisToDelete: [], _iVertsToDelete: [], _iVertsDecimated: [] }; const sortByIndex = ( a, b ) => a - b; function hasAtLeastThreeCommonElements( a, b ) { let ai = 0; let bi = 0; let count = 0; while ( ai < a.length && bi < b.length ) { if ( a[ ai ] < b[ bi ] ) ai ++; else if ( a[ ai ] > b[ bi ] ) bi ++; else { if ( ++ count === 3 ) return true; ai ++; bi ++; } } return false; } 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 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 ) { fleaf.length = lastPos; 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 ]; const j = iTri * 4; fAr[ j ] = iv1; fAr[ j + 1 ] = iv2; fAr[ j + 2 ] = iv3; fAr[ j + 3 ] = TRI_INDEX; fleaf.length = lastPos; 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 fAr = mesh.getFaces(); const vtf = mesh.getVerticesTagFlags(); const vsctf = mesh.getVerticesSculptFlags(); const lastPos = mesh.getNbVertices() - 1; if ( iVert === lastPos ) { vrv.length = lastPos; vrf.length = 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 ]; 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 ]; vrv.length = lastPos; vrf.length = lastPos; 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 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; // A tetrahedron cannot collapse without leaving coincident triangles. // Check local valence to protect disconnected shells. if ( ring1.length === 3 && ring2.length === 3 ) return; ring1.sort( sortByIndex ); ring2.sort( sortByIndex ); if ( hasAtLeastThreeCommonElements( ring1, ring2 ) ) { // Skip existing diagonals: a flip would leave four triangles sharing an edge. if ( ringOpp1.includes( ivOpp2 ) ) return; DecData._iVertsDecimated.push( iv1, iv2 ); 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 ); mesh.markTopologyChanged(); return; } DecData._iVertsDecimated.push( iv1, iv2 ); const 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; 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 ); // Project the neighbor average onto the tangent plane. 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 ]; const iTris2 = vrf[ iv2 ]; let count = 0; let opposite = - 1; for ( let i = 0, l = iTris1.length; i < l; ++ i ) { const candidate = iTris1[ i ]; for ( let j = 0, jl = iTris2.length; j < jl; ++ j ) { if ( candidate !== iTris2[ j ] ) continue; count ++; if ( candidate !== iTri ) opposite = candidate; break; } } return count === 2 ? opposite : - 1; } function decimationPass( mesh, iTris, center, radius2, detail2 ) { DecData._mesh = mesh; DecData._iVertsDecimated.length = 0; DecData._iTrisToDelete.length = 0; DecData._iVertsToDelete.length = 0; try { return decimate( mesh, iTris, center, radius2, detail2 ); } finally { DecData._mesh = null; } } function decimate( mesh, iTris, center, radius2, detail2 ) { const radius = Math.sqrt( radius2 ); const ftf = mesh.getFacesTagFlags(); const vAr = mesh.getVertices(); 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; if ( len1 < len2 && len1 < len3 ) { if ( len1 < detail2 * fallOff ) decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv1, iv2 ), dynArr ); } else if ( len2 < len3 ) { if ( len2 < detail2 * fallOff ) decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv2, iv3 ), dynArr ); } else { if ( len3 < detail2 * fallOff ) decDecimateTriangles( iTri, decFindOppositeTriangle( iTri, iv1, iv3 ), dynArr ); } } // Delete highest indices first so swaps preserve pending deletion indices. 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 ] ); const iVertsDecimated = DecData._iVertsDecimated; const nbVertices = mesh.getNbVertices(); const vtfDec = mesh.getVerticesTagFlags(); let tagFlag = mesh.nextTagFlag(); 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( validVertices.slice( 0, nbValid ) ); const nbTris = dynArr.length; const nbCombined = nbTris + newTris.length; tagFlag = mesh.nextTagFlag(); const nbTriangles = mesh.getNbTriangles(); const validTris = new Uint32Array( getMemory( nbCombined * 4 ), 0, nbCombined ); let nbValidTris = 0; for ( let i = 0; i < nbCombined; ++ i ) { const t = i < nbTris ? dynArr[ i ] : newTris[ i - nbTris ]; if ( t >= nbTriangles || ftf[ t ] === tagFlag ) continue; ftf[ t ] = tagFlag; validTris[ nbValidTris ++ ] = t; } return validTris.slice( 0, nbValidTris ); } // Tool helpers 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, strength ) { const vAr = mesh.getVertices(); const nAr = mesh.getNormals(); const intensity = Math.min( strength, 1.0 ); 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 ); vAr[ ind ] = vx + ( smx - nx * d - vx ) * intensity; vAr[ ind + 1 ] = vy + ( smy - ny * d - vy ) * intensity; vAr[ ind + 2 ] = vz + ( smz - nz * d - vz ) * intensity; } } 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 iVertsFront.slice( 0, acc ); } function areaNormal( mesh, iVerts ) { const nAr = mesh.getNormals(); let anx = 0, any = 0, anz = 0; for ( let i = 0, l = iVerts.length; i < l; ++ i ) { const ind = iVerts[ i ] * 3; anx += nAr[ ind ]; any += nAr[ ind + 1 ]; anz += nAr[ ind + 2 ]; } 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(); let ax = 0, ay = 0, az = 0; const acc = iVerts.length; for ( let i = 0, l = iVerts.length; i < l; ++ i ) { const ind = iVerts[ i ] * 3; ax += vAr[ ind ]; ay += vAr[ ind + 1 ]; az += vAr[ ind + 2 ]; } return [ ax / acc, ay / acc, az / acc ]; } // Tools function toolBrush( mesh, iVerts, aNormal, center, radiusSq, strength, negative ) { const vAr = mesh.getVertices(); const radius = Math.sqrt( radiusSq ); let deform = strength * 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 ) * deform; vAr[ ind ] += anx * fallOff; vAr[ ind + 1 ] += any * fallOff; vAr[ ind + 2 ] += anz * fallOff; } } function toolFlatten( mesh, iVerts, aNormal, aCenter2, center, radiusSq, strength, negative ) { const vAr = mesh.getVertices(); 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 * strength; vAr[ ind ] -= anx * fallOff; vAr[ ind + 1 ] -= any * fallOff; vAr[ ind + 2 ] -= anz * fallOff; } } function toolInflate( mesh, iVerts, center, radiusSq, strength, negative ) { const vAr = mesh.getVertices(); const nAr = mesh.getNormals(); const radius = Math.sqrt( radiusSq ); let deform = strength * 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; vAr[ ind ] += nx * fallOff; vAr[ ind + 1 ] += ny * fallOff; vAr[ ind + 2 ] += nz * fallOff; } } function toolSmooth( mesh, iVerts, strength ) { const vAr = mesh.getVertices(); const intensity = Math.min( strength, 1.0 ); const intComp = 1.0 - intensity; 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; vAr[ ind ] = vx * intComp + smoothVerts[ i3 ] * intensity; vAr[ ind + 1 ] = vy * intComp + smoothVerts[ i3 + 1 ] * intensity; vAr[ ind + 2 ] = vz * intComp + smoothVerts[ i3 + 2 ] * intensity; } } function toolPinch( mesh, iVerts, center, radiusSq, strength, negative ) { const vAr = mesh.getVertices(); const radius = Math.sqrt( radiusSq ); const cx = center[ 0 ], cy = center[ 1 ], cz = center[ 2 ]; let deform = strength * 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; vAr[ ind ] = vx + dx * fallOff; vAr[ ind + 1 ] = vy + dy * fallOff; vAr[ ind + 2 ] = vz + dz * fallOff; } } function toolCrease( mesh, iVerts, aNormal, center, radiusSq, strength, negative ) { const vAr = mesh.getVertices(); 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 = strength * 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 ); 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 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 ); 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 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; 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 };