three
Version:
JavaScript 3D library
1,505 lines (1,005 loc) • 37.3 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,
MAX_FLAG,
getMemory
} from './SculptorUtils.js';
const OCTREE_MAX_DEPTH = 8;
const OCTREE_MAX_FACES = 100;
// A depth-first walk retains at most seven siblings for each level.
const OCTREE_STACK = new Array( 1 + 7 * OCTREE_MAX_DEPTH ).fill( null );
const RELATIVE_WELD_TOLERANCE = 1e-7;
function queueLeaf( leaves, leaf ) {
if ( leaves === undefined || leaf._queuedForUpdate ) return;
leaf._queuedForUpdate = true;
leaves.push( leaf );
}
function hashPosition( x, y, z ) {
let hash = Math.imul( x, 0x85ebca6b );
hash = Math.imul( hash ^ ( hash >>> 13 ) ^ y, 0xc2b2ae35 );
hash = Math.imul( hash ^ ( hash >>> 16 ) ^ z, 0x27d4eb2d );
return ( hash ^ ( hash >>> 15 ) ) >>> 0;
}
function resetTagFlags( flags, activeCount ) {
for ( let i = 0; i < activeCount; i ++ ) {
if ( flags[ i ] >= 0 ) flags[ i ] = 0;
}
flags.fill( 0, activeCount );
}
function readSourceElements( index, sourceVertexCount ) {
const elementCount = index ? index.count : sourceVertexCount;
if ( index && ( index.itemSize !== 1 || index.normalized ) ) {
throw new Error( 'SculptorMesh: The index must be a non-normalized scalar attribute.' );
}
if ( index && typeof index.getX !== 'function' ) {
throw new Error( 'SculptorMesh: The index attribute must be CPU-accessible.' );
}
if ( elementCount === 0 || elementCount % 3 !== 0 ) {
throw new Error( 'SculptorMesh: The geometry must contain triangles with a complete, non-empty element list.' );
}
if ( index === null ) {
return { elementCount, sourceIndices: null, referencedVertices: null };
}
const sourceIndices = new Uint32Array( elementCount );
const referencedVertices = new Uint8Array( sourceVertexCount );
for ( let i = 0; i < elementCount; i ++ ) {
const sourceIndex = index.getX( i );
if ( Number.isInteger( sourceIndex ) === false || sourceIndex < 0 || sourceIndex >= sourceVertexCount ) {
throw new Error( 'SculptorMesh: Triangle indices must reference valid positions.' );
}
sourceIndices[ i ] = sourceIndex;
referencedVertices[ sourceIndex ] = 1;
}
return { elementCount, sourceIndices, referencedVertices };
}
function readSourcePositions( position, referencedVertices ) {
const sourceVertexCount = position.count;
const positions = new Float32Array( sourceVertexCount * 3 );
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
for ( let i = 0; i < sourceVertexCount; i ++ ) {
if ( referencedVertices !== null && referencedVertices[ i ] === 0 ) continue;
const sourceX = position.getX( i );
const sourceY = position.getY( i );
const sourceZ = position.getZ( i );
if ( Number.isFinite( sourceX ) === false || Number.isFinite( sourceY ) === false || Number.isFinite( sourceZ ) === false ) {
throw new Error( 'SculptorMesh: Position values must be finite.' );
}
const x = Math.fround( sourceX );
const y = Math.fround( sourceY );
const z = Math.fround( sourceZ );
if ( Number.isFinite( x ) === false || Number.isFinite( y ) === false || Number.isFinite( z ) === false ) {
throw new Error( 'SculptorMesh: Position values must fit in Float32 storage.' );
}
const offset = i * 3;
positions[ offset ] = x;
positions[ offset + 1 ] = y;
positions[ offset + 2 ] = z;
if ( x < minX ) minX = x;
if ( y < minY ) minY = y;
if ( z < minZ ) minZ = z;
if ( x > maxX ) maxX = x;
if ( y > maxY ) maxY = y;
if ( z > maxZ ) maxZ = z;
}
return { positions, bounds: [ minX, minY, minZ, maxX, maxY, maxZ ] };
}
function weldPositions( sourcePositions, referencedVertices, bounds ) {
const sourceVertexCount = sourcePositions.length / 3;
const extent = Math.max( bounds[ 3 ] - bounds[ 0 ], bounds[ 4 ] - bounds[ 1 ], bounds[ 5 ] - bounds[ 2 ] );
const tolerance = extent * RELATIVE_WELD_TOLERANCE;
const toleranceSquared = tolerance * tolerance;
// Cells twice the weld radius only need the current and nearest neighboring
// cell in each dimension. Exact distances resolve spatial-hash collisions.
const inverseCellSize = tolerance > 0 ? 0.5 / tolerance : 0;
const cellHeads = new Map();
const nextHashEntry = [];
const mergedPositions = [];
const vertexMap = new Uint32Array( sourceVertexCount );
const searchCellCount = inverseCellSize > 0 ? 2 : 1;
for ( let i = 0; i < sourceVertexCount; i ++ ) {
if ( referencedVertices !== null && referencedVertices[ i ] === 0 ) continue;
const offset = i * 3;
const x = sourcePositions[ offset ];
const y = sourcePositions[ offset + 1 ];
const z = sourcePositions[ offset + 2 ];
const gridX = ( x - bounds[ 0 ] ) * inverseCellSize;
const gridY = ( y - bounds[ 1 ] ) * inverseCellSize;
const gridZ = ( z - bounds[ 2 ] ) * inverseCellSize;
const cellX = Math.floor( gridX );
const cellY = Math.floor( gridY );
const cellZ = Math.floor( gridZ );
const neighborX = cellX + ( gridX - cellX < 0.5 ? - 1 : 1 );
const neighborY = cellY + ( gridY - cellY < 0.5 ? - 1 : 1 );
const neighborZ = cellZ + ( gridZ - cellZ < 0.5 ? - 1 : 1 );
let mergedIndex;
let closestDistanceSquared = Infinity;
searchCells: for ( let iz = 0; iz < searchCellCount; iz ++ ) {
const searchZ = iz === 0 ? cellZ : neighborZ;
for ( let iy = 0; iy < searchCellCount; iy ++ ) {
const searchY = iy === 0 ? cellY : neighborY;
for ( let ix = 0; ix < searchCellCount; ix ++ ) {
const searchX = ix === 0 ? cellX : neighborX;
let candidate = cellHeads.get( hashPosition( searchX, searchY, searchZ ) );
while ( candidate !== undefined ) {
const candidateOffset = candidate * 3;
const dx = mergedPositions[ candidateOffset ] - x;
const dy = mergedPositions[ candidateOffset + 1 ] - y;
const dz = mergedPositions[ candidateOffset + 2 ] - z;
const distanceSquared = dx * dx + dy * dy + dz * dz;
if ( distanceSquared === 0 ) {
mergedIndex = candidate;
break searchCells;
}
if ( distanceSquared <= toleranceSquared && distanceSquared < closestDistanceSquared ) {
mergedIndex = candidate;
closestDistanceSquared = distanceSquared;
}
candidate = nextHashEntry[ candidate ];
}
}
}
}
if ( mergedIndex === undefined ) {
mergedIndex = mergedPositions.length / 3;
const hash = hashPosition( cellX, cellY, cellZ );
nextHashEntry[ mergedIndex ] = cellHeads.get( hash );
cellHeads.set( hash, mergedIndex );
mergedPositions.push( x, y, z );
}
vertexMap[ i ] = mergedIndex;
}
return { positions: mergedPositions, vertexMap };
}
function buildTriangleBuffers( elementCount, sourceIndices, vertexMap, positions ) {
const triangleCount = elementCount / 3;
const faces = new Uint32Array( triangleCount * 4 );
const triangles = new Uint32Array( elementCount );
for ( let i = 0; i < triangleCount; i ++ ) {
const triangleOffset = i * 3;
let a, b, c;
if ( sourceIndices === null ) {
a = vertexMap[ triangleOffset ];
b = vertexMap[ triangleOffset + 1 ];
c = vertexMap[ triangleOffset + 2 ];
} else {
a = vertexMap[ sourceIndices[ triangleOffset ] ];
b = vertexMap[ sourceIndices[ triangleOffset + 1 ] ];
c = vertexMap[ sourceIndices[ triangleOffset + 2 ] ];
}
if ( a === b || b === c || c === a ) {
throw new Error( 'SculptorMesh: Welding produced a degenerate triangle.' );
}
const a3 = a * 3;
const b3 = b * 3;
const c3 = c * 3;
const abx = positions[ b3 ] - positions[ a3 ];
const aby = positions[ b3 + 1 ] - positions[ a3 + 1 ];
const abz = positions[ b3 + 2 ] - positions[ a3 + 2 ];
const acx = positions[ c3 ] - positions[ a3 ];
const acy = positions[ c3 + 1 ] - positions[ a3 + 1 ];
const acz = positions[ c3 + 2 ] - positions[ a3 + 2 ];
const normalX = Math.fround( aby * acz - abz * acy );
const normalY = Math.fround( abz * acx - abx * acz );
const normalZ = Math.fround( abx * acy - aby * acx );
if ( Number.isFinite( normalX ) === false || Number.isFinite( normalY ) === false || Number.isFinite( normalZ ) === false ) {
throw new Error( 'SculptorMesh: Triangle normals must fit in Float32 storage.' );
}
if ( normalX === 0 && normalY === 0 && normalZ === 0 ) {
throw new Error( 'SculptorMesh: The geometry contains a zero-area triangle at Float32 precision after welding.' );
}
const faceOffset = i * 4;
faces[ faceOffset ] = a;
faces[ faceOffset + 1 ] = b;
faces[ faceOffset + 2 ] = c;
faces[ faceOffset + 3 ] = TRI_INDEX;
triangles[ triangleOffset ] = a;
triangles[ triangleOffset + 1 ] = b;
triangles[ triangleOffset + 2 ] = c;
}
return { faces, triangles };
}
// Octree
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 = [];
this._queuedForUpdate = false;
}
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 );
stack.fill( null );
}
_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._expandAabbLoose( 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 xcen = ( xmax + xmin ) * 0.5, ycen = ( ymax + ymin ) * 0.5, zcen = ( zmax + zmin ) * 0.5;
const children = new Array( 8 );
for ( let i = 0; i < 8; i ++ ) children[ i ] = 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( xcen, ymin, zmin, xmax, ycen, zcen );
children[ 2 ]._setAabbSplit( xcen, ymin, zcen, xmax, ycen, zmax );
children[ 3 ]._setAabbSplit( xmin, ymin, zcen, xcen, ycen, zmax );
children[ 4 ]._setAabbSplit( xmin, ycen, zmin, xcen, ymax, zcen );
children[ 5 ]._setAabbSplit( xcen, ycen, zmin, xmax, ymax, zcen );
children[ 6 ]._setAabbSplit( xcen, ycen, zcen, xmax, ymax, zmax );
children[ 7 ]._setAabbSplit( xmin, ycen, zcen, xcen, 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;
}
_setAabbLoose( xmin, ymin, zmin, xmax, ymax, zmax ) {
const a = this._aabbLoose;
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 {
queueLeaf( leavesHit, cell );
const iFaces = cell._iFaces;
collectFaces.set( iFaces, acc );
acc += iFaces.length;
}
}
stack.fill( null );
return collectFaces.slice( 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 {
queueLeaf( leavesHit, cell );
const iFaces = cell._iFaces;
collectFaces.set( iFaces, acc );
acc += iFaces.length;
}
}
stack.fill( null );
return collectFaces.slice( 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 );
stack.fill( null );
return cell;
}
}
stack.fill( null );
}
_expandAabbLoose( 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;
}
}
}
// Mesh topology and spatial queries
class SculptorMesh {
constructor() {
this._nbVertices = 0;
this._nbFaces = 0;
this._verticesXYZ = null;
this._normalsXYZ = null;
this._renderNormalsXYZ = 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._facesTagFlags = null;
this._octree = null;
this._leavesToUpdate = [];
this._topologyVersion = 0;
this._tagFlag = 1;
this._sculptFlag = 1;
}
getNbVertices() {
return this._nbVertices;
}
getNbFaces() {
return this._nbFaces;
}
getNbTriangles() {
return this._nbFaces;
}
getVertices() {
return this._verticesXYZ;
}
getNormals() {
return this._normalsXYZ;
}
getRenderNormals() {
return this._renderNormalsXYZ;
}
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;
}
getFaceNormals() {
return this._faceNormals;
}
getFaceBoxes() {
return this._faceBoxes;
}
getFaceCenters() {
return this._faceCenters;
}
getFacePosInLeaf() {
return this._facePosInLeaf;
}
getFaceLeaf() {
return this._faceLeaf;
}
getFacesTagFlags() {
return this._facesTagFlags;
}
getTopologyVersion() {
return this._topologyVersion;
}
getSculptFlag() {
return this._sculptFlag;
}
nextTagFlag() {
if ( this._tagFlag >= MAX_FLAG ) {
// Preserve pending deletion sentinels across tag rollover.
resetTagFlags( this._vertTagFlags, this._nbVertices );
resetTagFlags( this._facesTagFlags, this._nbFaces );
this._tagFlag = 1;
} else {
this._tagFlag ++;
}
return this._tagFlag;
}
nextSculptFlag() {
if ( this._sculptFlag >= MAX_FLAG ) {
this._vertSculptFlags.fill( 0 );
this._sculptFlag = 1;
} else {
this._sculptFlag ++;
}
return this._sculptFlag;
}
addNbVertice( nb ) {
this._nbVertices += nb;
}
addNbFace( nb ) {
this._nbFaces += nb;
this._topologyVersion ++;
}
markTopologyChanged() {
this._topologyVersion ++;
}
initFromGeometry( geometry ) {
this._tagFlag = 1;
this._sculptFlag = 1;
const posAttr = geometry.getAttribute( 'position' );
if ( posAttr === undefined || posAttr.itemSize !== 3 || posAttr.count === 0 ) {
throw new Error( 'SculptorMesh: A non-empty position attribute with itemSize 3 is required.' );
}
if ( typeof posAttr.getX !== 'function' || typeof posAttr.getY !== 'function' || typeof posAttr.getZ !== 'function' ) {
throw new Error( 'SculptorMesh: The position attribute must be CPU-accessible.' );
}
const index = geometry.getIndex();
const sourceVertexCount = posAttr.count;
const { elementCount, sourceIndices, referencedVertices } = readSourceElements( index, sourceVertexCount );
const { positions: sourcePositions, bounds } = readSourcePositions( posAttr, referencedVertices );
const { positions: weldedPositions, vertexMap } = weldPositions( sourcePositions, referencedVertices, bounds );
const { faces, triangles } = buildTriangleBuffers( elementCount, sourceIndices, vertexMap, weldedPositions );
const vertexCount = weldedPositions.length / 3;
const triangleCount = elementCount / 3;
const vertexDataLength = vertexCount * 3;
this._nbVertices = vertexCount;
this._nbFaces = triangleCount;
this._topologyVersion = 0;
this._leavesToUpdate.length = 0;
this._verticesXYZ = new Float32Array( weldedPositions );
this._normalsXYZ = new Float32Array( vertexDataLength );
this._renderNormalsXYZ = new Float32Array( vertexDataLength );
this._facesABCD = faces;
this._trianglesABC = triangles;
this._vertOnEdge = new Uint8Array( vertexCount );
this._vertTagFlags = new Int32Array( vertexCount );
this._vertSculptFlags = new Int32Array( vertexCount );
this._facesTagFlags = new Int32Array( triangleCount );
this._faceBoxes = new Float32Array( triangleCount * 6 );
this._faceNormals = new Float32Array( triangleCount * 3 );
this._faceCenters = new Float32Array( triangleCount * 3 );
this._facePosInLeaf = new Uint32Array( triangleCount );
this._faceLeaf = new Array( triangleCount ).fill( null );
this._initTopology();
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 = this.nextTagFlag();
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 ) {
if ( iVerts === undefined && iFaces !== undefined ) iVerts = this.getVerticesFromFaces( iFaces );
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 renderNAr = this._renderNormalsXYZ;
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 ];
}
const inverseCount = vrf.length > 0 ? 1.0 / vrf.length : 0;
const ind3 = ind * 3;
nx *= inverseCount;
ny *= inverseCount;
nz *= inverseCount;
nAr[ ind3 ] = nx;
nAr[ ind3 + 1 ] = ny;
nAr[ ind3 + 2 ] = nz;
const length = Math.sqrt( nx * nx + ny * ny + nz * nz );
const invLength = length > 0 ? 1.0 / length : 0;
renderNAr[ ind3 ] = nx * invLength;
renderNAr[ ind3 + 1 ] = ny * invLength;
renderNAr[ ind3 + 2 ] = nz * invLength;
}
}
_updateOctree( iFaces ) {
if ( iFaces === undefined ) {
this._computeOctree();
} else {
this._updateOctreeAdd( this._updateOctreeRemove( iFaces ) );
}
}
_computeOctree() {
const vAr = this._verticesXYZ;
let xmin = Infinity, ymin = Infinity, zmin = Infinity;
let xmax = - Infinity, ymax = - Infinity, zmax = - Infinity;
for ( let i = 0, l = this._nbVertices * 3; i < l; i += 3 ) {
if ( vAr[ i ] < xmin ) xmin = vAr[ i ];
if ( vAr[ i ] > xmax ) xmax = vAr[ i ];
if ( vAr[ i + 1 ] < ymin ) ymin = vAr[ i + 1 ];
if ( vAr[ i + 1 ] > ymax ) ymax = vAr[ i + 1 ];
if ( vAr[ i + 2 ] < zmin ) zmin = vAr[ i + 2 ];
if ( vAr[ i + 2 ] > zmax ) zmax = vAr[ i + 2 ];
}
const dx = xmax - xmin;
const dy = ymax - ymin;
const dz = zmax - zmin;
const thickness = Math.hypot( dx, dy, dz ) * 0.2;
if ( dx === 0 ) {
xmin -= thickness;
xmax += thickness;
}
if ( dy === 0 ) {
ymin -= thickness;
ymax += thickness;
}
if ( dz === 0 ) {
zmin -= thickness;
zmax += thickness;
}
const octree = new OctreeCell();
octree.resetNbFaces( this._nbFaces );
octree._setAabbLoose( xmin, ymin, zmin, xmax, ymax, zmax );
octree._setAabbSplit(
xmin - dx * 0.3, ymin - dy * 0.3, zmin - dz * 0.3,
xmax + dx * 0.3, ymax + dy * 0.3, zmax + dz * 0.3
);
octree.build( this );
this._octree = octree;
for ( let i = 0, l = this._leavesToUpdate.length; i < l; i ++ ) this._leavesToUpdate[ i ]._queuedForUpdate = false;
this._leavesToUpdate.length = 0;
}
_updateOctreeRemove( iFaces ) {
const faceBoxes = this._faceBoxes;
const faceCenters = this._faceCenters;
const faceLeaf = this._faceLeaf;
const facePosInLeaf = this._facePosInLeaf;
const facesToMove = new Uint32Array( getMemory( iFaces.length * 4 ), 0, iFaces.length );
let count = 0;
for ( let i = 0, l = iFaces.length; i < l; ++ i ) {
const iFace = iFaces[ i ];
const leaf = faceLeaf[ iFace ];
const idCenter = iFace * 3;
if ( leaf === undefined || leaf === null ) {
facesToMove[ count ++ ] = iFace;
continue;
}
const split = leaf._aabbSplit;
const x = faceCenters[ idCenter ];
const y = faceCenters[ idCenter + 1 ];
const z = faceCenters[ idCenter + 2 ];
if ( x <= split[ 0 ] || y <= split[ 1 ] || z <= split[ 2 ] || x > split[ 3 ] || y > split[ 4 ] || z > split[ 5 ] ) {
facesToMove[ count ++ ] = iFace;
const facesInLeaf = leaf._iFaces;
const position = facePosInLeaf[ iFace ];
const lastFace = facesInLeaf[ facesInLeaf.length - 1 ];
facesInLeaf[ position ] = lastFace;
facePosInLeaf[ lastFace ] = position;
facesInLeaf.pop();
queueLeaf( this._leavesToUpdate, leaf );
} else {
const idBox = iFace * 6;
leaf._expandAabbLoose(
faceBoxes[ idBox ], faceBoxes[ idBox + 1 ], faceBoxes[ idBox + 2 ],
faceBoxes[ idBox + 3 ], faceBoxes[ idBox + 4 ], faceBoxes[ idBox + 5 ]
);
}
}
return facesToMove.subarray( 0, count );
}
_updateOctreeAdd( iFaces ) {
const faceBoxes = this._faceBoxes;
const faceCenters = this._faceCenters;
const faceLeaf = this._faceLeaf;
const facePosInLeaf = this._facePosInLeaf;
for ( let i = 0, l = iFaces.length; i < l; ++ i ) {
const iFace = iFaces[ i ];
const idBox = iFace * 6;
const idCenter = iFace * 3;
const newLeaf = this._octree.addFace(
iFace,
faceBoxes[ idBox ], faceBoxes[ idBox + 1 ], faceBoxes[ idBox + 2 ],
faceBoxes[ idBox + 3 ], faceBoxes[ idBox + 4 ], faceBoxes[ idBox + 5 ],
faceCenters[ idCenter ], faceCenters[ idCenter + 1 ], faceCenters[ idCenter + 2 ]
);
if ( newLeaf === undefined ) {
this._computeOctree();
return;
}
faceLeaf[ iFace ] = newLeaf;
facePosInLeaf[ iFace ] = newLeaf._iFaces.length - 1;
queueLeaf( this._leavesToUpdate, newLeaf );
}
}
// Mesh queries
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, collectLeaves = false ) {
const nbFaces = this._nbFaces;
const collectBuffer = new Uint32Array( getMemory( nbFaces * 4 ), 0, nbFaces );
return this._octree.collectIntersectSphere( center, radiusSq, collectBuffer, collectLeaves ? this._leavesToUpdate : undefined );
}
getVerticesFromFaces( iFaces ) {
const tagFlag = this.nextTagFlag();
const nbFaces = iFaces.length;
const vtf = this._vertTagFlags;
const fAr = this._facesABCD;
let acc = 0;
const verts = new Uint32Array( getMemory( 3 * nbFaces * 4 ), 0, nbFaces * 3 );
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 verts.slice( 0, acc );
}
getFacesFromVertices( iVerts ) {
const tagFlag = this.nextTagFlag();
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 faces.slice( 0, acc );
}
expandsFaces( iFaces, nRing ) {
const tagFlag = this.nextTagFlag();
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 iFacesExpanded.slice( 0, acc );
}
expandsVertices( iVerts, nRing ) {
const tagFlag = this.nextTagFlag();
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 iVertsExpanded.slice( 0, acc );
}
// Dynamic topology
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( array, requiredLength ) {
if ( array === null ) return null;
const resized = new array.constructor( requiredLength * 2 );
resized.set( array.subarray( 0, Math.min( array.length, resized.length ) ) );
return resized;
}
reAllocateArrays( nbAddElements ) {
let capacity = this._facesABCD.length / 4;
let requiredCount = this._nbFaces + nbAddElements;
if ( capacity < requiredCount || capacity > requiredCount * 4 ) {
this._facesABCD = this._resizeArray( this._facesABCD, requiredCount * 4 );
this._trianglesABC = this._resizeArray( this._trianglesABC, requiredCount * 3 );
this._faceBoxes = this._resizeArray( this._faceBoxes, requiredCount * 6 );
this._faceNormals = this._resizeArray( this._faceNormals, requiredCount * 3 );
this._faceCenters = this._resizeArray( this._faceCenters, requiredCount * 3 );
this._facesTagFlags = this._resizeArray( this._facesTagFlags, requiredCount );
this._facePosInLeaf = this._resizeArray( this._facePosInLeaf, requiredCount );
}
capacity = this._verticesXYZ.length / 3;
requiredCount = this._nbVertices + nbAddElements;
if ( capacity < requiredCount || capacity > requiredCount * 4 ) {
this._verticesXYZ = this._resizeArray( this._verticesXYZ, requiredCount * 3 );
this._normalsXYZ = this._resizeArray( this._normalsXYZ, requiredCount * 3 );
this._renderNormalsXYZ = this._resizeArray( this._renderNormalsXYZ, requiredCount * 3 );
this._vertOnEdge = this._resizeArray( this._vertOnEdge, requiredCount );
this._vertTagFlags = this._resizeArray( this._vertTagFlags, requiredCount );
this._vertSculptFlags = this._resizeArray( this._vertSculptFlags, requiredCount );
}
}
balanceOctree() {
const leaves = this._leavesToUpdate;
for ( let i = 0, l = leaves.length; i < l; ++ i ) {
const leaf = leaves[ i ];
leaf._queuedForUpdate = false;
if ( leaf._iFaces.length === 0 ) {
leaf.pruneIfPossible();
} else if ( leaf._iFaces.length > OCTREE_MAX_FACES && leaf._depth < OCTREE_MAX_DEPTH ) {
leaf.build( this );
}
}
leaves.length = 0;
}
}
export { SculptorMesh };