three
Version:
JavaScript 3D library
1,181 lines (865 loc) • 34.2 kB
JavaScript
/*!
* 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
};