From 49d68d8d4d956ba7a26f8b80ed3befdd6f16181a Mon Sep 17 00:00:00 2001 From: Avatarsia Date: Tue, 7 Apr 2026 15:51:07 +0200 Subject: [PATCH 1/3] perf: integrate position lookup into dedup pass, eliminate redundant iteration Co-Authored-By: Claude Opus 4.6 (1M context) --- js/displacement.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/js/displacement.js b/js/displacement.js index 5406f01..9f85f89 100644 --- a/js/displacement.js +++ b/js/displacement.js @@ -66,11 +66,20 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett const _dedupMap = new Map(); let _nextId = 0; const vertexId = new Uint32Array(count); + const _idPosX = []; + const _idPosY = []; + const _idPosZ = []; for (let i = 0; i < count; i++) { const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i); const key = `${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`; let id = _dedupMap.get(key); - if (id === undefined) { id = _nextId++; _dedupMap.set(key, id); } + if (id === undefined) { + id = _nextId++; + _dedupMap.set(key, id); + _idPosX.push(x); + _idPosY.push(y); + _idPosZ.push(z); + } vertexId[i] = id; } const uniqueCount = _nextId; @@ -191,21 +200,6 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett let falloffArr = null; if (boundaryFalloff > 0) { - // Build position lookup per unique vertex ID (first occurrence) - const idPosX = new Float64Array(uniqueCount); - const idPosY = new Float64Array(uniqueCount); - const idPosZ = new Float64Array(uniqueCount); - const idPosSeen = new Uint8Array(uniqueCount); - for (let i = 0; i < count; i++) { - const vid = vertexId[i]; - if (!idPosSeen[vid]) { - idPosSeen[vid] = 1; - idPosX[vid] = posAttr.getX(i); - idPosY[vid] = posAttr.getY(i); - idPosZ[vid] = posAttr.getZ(i); - } - } - const boundaryPositions = []; // [[x, y, z], ...] // Collect boundary positions: vertices where maskedFrac is between 0 and 1, @@ -215,7 +209,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett const maskedFrac = mfTotal > 0 ? maskedFracMasked[id] / mfTotal : 0; const isOnExclBoundary = excludedPos && excludedPos[id] === 1; if (isOnExclBoundary || (maskedFrac > 0 && maskedFrac < 1)) { - boundaryPositions.push([idPosX[id], idPosY[id], idPosZ[id]]); + boundaryPositions.push([_idPosX[id], _idPosY[id], _idPosZ[id]]); } } @@ -262,7 +256,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett // Only compute falloff for fully-textured, non-boundary positions if (maskedFrac > 0 || isOnExclBoundary) continue; - const px = idPosX[id], py = idPosY[id], pz = idPosZ[id]; + const px = _idPosX[id], py = _idPosY[id], pz = _idPosZ[id]; const cix = Math.max(0, Math.min(gRes - 1, Math.floor((px - gMinX) / gDx))); const ciy = Math.max(0, Math.min(gRes - 1, Math.floor((py - gMinY) / gDy))); const ciz = Math.max(0, Math.min(gRes - 1, Math.floor((pz - gMinZ) / gDz))); From 21d0d7bc2bd47455018810c078acd443e222c133 Mon Sep 17 00:00:00 2001 From: Avatarsia Date: Tue, 7 Apr 2026 15:51:56 +0200 Subject: [PATCH 2/3] perf: replace array-of-arrays with flat SoA layout for boundary positions Co-Authored-By: Claude Opus 4.6 (1M context) --- js/displacement.js | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/js/displacement.js b/js/displacement.js index 9f85f89..b2c0fd3 100644 --- a/js/displacement.js +++ b/js/displacement.js @@ -200,46 +200,55 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett let falloffArr = null; if (boundaryFalloff > 0) { - const boundaryPositions = []; // [[x, y, z], ...] - - // Collect boundary positions: vertices where maskedFrac is between 0 and 1, - // or that sit on the user-exclusion seam. + // Count boundary positions first, then allocate flat SoA arrays + let bpCount = 0; + for (let id = 0; id < uniqueCount; id++) { + const mfTotal = maskedFracTotal[id]; + const maskedFrac = mfTotal > 0 ? maskedFracMasked[id] / mfTotal : 0; + const isOnExclBoundary = excludedPos && excludedPos[id] === 1; + if (isOnExclBoundary || (maskedFrac > 0 && maskedFrac < 1)) bpCount++; + } + const bpX = new Float64Array(bpCount); + const bpY = new Float64Array(bpCount); + const bpZ = new Float64Array(bpCount); + let bpIdx = 0; for (let id = 0; id < uniqueCount; id++) { const mfTotal = maskedFracTotal[id]; const maskedFrac = mfTotal > 0 ? maskedFracMasked[id] / mfTotal : 0; const isOnExclBoundary = excludedPos && excludedPos[id] === 1; if (isOnExclBoundary || (maskedFrac > 0 && maskedFrac < 1)) { - boundaryPositions.push([_idPosX[id], _idPosY[id], _idPosZ[id]]); + bpX[bpIdx] = _idPosX[id]; bpY[bpIdx] = _idPosY[id]; bpZ[bpIdx] = _idPosZ[id]; + bpIdx++; } } - if (boundaryPositions.length > 0) { + if (bpCount > 0) { // Build a spatial grid of boundary positions for fast nearest-neighbor lookup let gMinX = Infinity, gMinY = Infinity, gMinZ = Infinity; let gMaxX = -Infinity, gMaxY = -Infinity, gMaxZ = -Infinity; - for (const bp of boundaryPositions) { - if (bp[0] < gMinX) gMinX = bp[0]; if (bp[0] > gMaxX) gMaxX = bp[0]; - if (bp[1] < gMinY) gMinY = bp[1]; if (bp[1] > gMaxY) gMaxY = bp[1]; - if (bp[2] < gMinZ) gMinZ = bp[2]; if (bp[2] > gMaxZ) gMaxZ = bp[2]; + for (let i = 0; i < bpCount; i++) { + if (bpX[i] < gMinX) gMinX = bpX[i]; if (bpX[i] > gMaxX) gMaxX = bpX[i]; + if (bpY[i] < gMinY) gMinY = bpY[i]; if (bpY[i] > gMaxY) gMaxY = bpY[i]; + if (bpZ[i] < gMinZ) gMinZ = bpZ[i]; if (bpZ[i] > gMaxZ) gMaxZ = bpZ[i]; } const gPad = boundaryFalloff + 1e-3; gMinX -= gPad; gMinY -= gPad; gMinZ -= gPad; gMaxX += gPad; gMaxY += gPad; gMaxZ += gPad; - const gRes = Math.max(4, Math.min(128, Math.ceil(Math.cbrt(boundaryPositions.length) * 2))); + const gRes = Math.max(4, Math.min(128, Math.ceil(Math.cbrt(bpCount) * 2))); const gDx = (gMaxX - gMinX) / gRes || 1; const gDy = (gMaxY - gMinY) / gRes || 1; const gDz = (gMaxZ - gMinZ) / gRes || 1; const bGrid = new Map(); const bCellKey = (ix, iy, iz) => (ix * gRes + iy) * gRes + iz; - for (const bp of boundaryPositions) { - const ix = Math.max(0, Math.min(gRes - 1, Math.floor((bp[0] - gMinX) / gDx))); - const iy = Math.max(0, Math.min(gRes - 1, Math.floor((bp[1] - gMinY) / gDy))); - const iz = Math.max(0, Math.min(gRes - 1, Math.floor((bp[2] - gMinZ) / gDz))); + for (let i = 0; i < bpCount; i++) { + const ix = Math.max(0, Math.min(gRes - 1, Math.floor((bpX[i] - gMinX) / gDx))); + const iy = Math.max(0, Math.min(gRes - 1, Math.floor((bpY[i] - gMinY) / gDy))); + const iz = Math.max(0, Math.min(gRes - 1, Math.floor((bpZ[i] - gMinZ) / gDz))); const ck = bCellKey(ix, iy, iz); const cell = bGrid.get(ck); - if (cell) cell.push(bp); else bGrid.set(ck, [bp]); + if (cell) cell.push(i); else bGrid.set(ck, [i]); } // How many grid cells to search in each direction to cover boundaryFalloff distance @@ -273,8 +282,8 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett if (niz < 0 || niz >= gRes) continue; const cell = bGrid.get(bCellKey(nix, niy, niz)); if (!cell) continue; - for (const bp of cell) { - const dx = px - bp[0], dy = py - bp[1], dz = pz - bp[2]; + for (const idx of cell) { + const dx = px - bpX[idx], dy = py - bpY[idx], dz = pz - bpZ[idx]; const d2 = dx * dx + dy * dy + dz * dz; if (d2 < minDist2) minDist2 = d2; } From 198c2fffbba20c964972af9db9b82fcd6f7d763e Mon Sep 17 00:00:00 2001 From: Avatarsia Date: Tue, 7 Apr 2026 15:52:17 +0200 Subject: [PATCH 3/3] perf: replace Map with flat array for spatial grid in boundary falloff Co-Authored-By: Claude Opus 4.6 (1M context) --- js/displacement.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/displacement.js b/js/displacement.js index b2c0fd3..af40343 100644 --- a/js/displacement.js +++ b/js/displacement.js @@ -239,7 +239,8 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett const gDx = (gMaxX - gMinX) / gRes || 1; const gDy = (gMaxY - gMinY) / gRes || 1; const gDz = (gMaxZ - gMinZ) / gRes || 1; - const bGrid = new Map(); + const gridSize = gRes * gRes * gRes; + const bGrid = new Array(gridSize); const bCellKey = (ix, iy, iz) => (ix * gRes + iy) * gRes + iz; for (let i = 0; i < bpCount; i++) { @@ -247,8 +248,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett const iy = Math.max(0, Math.min(gRes - 1, Math.floor((bpY[i] - gMinY) / gDy))); const iz = Math.max(0, Math.min(gRes - 1, Math.floor((bpZ[i] - gMinZ) / gDz))); const ck = bCellKey(ix, iy, iz); - const cell = bGrid.get(ck); - if (cell) cell.push(i); else bGrid.set(ck, [i]); + if (bGrid[ck]) bGrid[ck].push(i); else bGrid[ck] = [i]; } // How many grid cells to search in each direction to cover boundaryFalloff distance @@ -280,7 +280,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett for (let diz = -searchZ; diz <= searchZ; diz++) { const niz = ciz + diz; if (niz < 0 || niz >= gRes) continue; - const cell = bGrid.get(bCellKey(nix, niy, niz)); + const cell = bGrid[bCellKey(nix, niy, niz)]; if (!cell) continue; for (const idx of cell) { const dx = px - bpX[idx], dy = py - bpY[idx], dz = pz - bpZ[idx];