@playcanvas/splat-transform 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -11854,6 +11854,16 @@ class Progress {
11854
11854
  if (!quiet)
11855
11855
  impl.onProgress(this.currentNode);
11856
11856
  }
11857
+ /**
11858
+ * Cancel the current progress node, popping it from the stack without
11859
+ * completing remaining steps. Use this before early exits (e.g. break)
11860
+ * to keep the progress stack balanced.
11861
+ */
11862
+ cancel() {
11863
+ if (!this.currentNode)
11864
+ return;
11865
+ this.currentNode = this.currentNode.parent;
11866
+ }
11857
11867
  /**
11858
11868
  * Advance to the next step. Auto-increments the step counter.
11859
11869
  * Auto-ends when all steps are complete.
@@ -12041,33 +12051,74 @@ const sortMortonOrder = (dataTable, indices) => {
12041
12051
  generate(indices);
12042
12052
  };
12043
12053
 
12054
+ const nthElement = (arr, lo, hi, k, values) => {
12055
+ while (lo < hi) {
12056
+ const mid = (lo + hi) >> 1;
12057
+ const va = values[arr[lo]], vb = values[arr[mid]], vc = values[arr[hi]];
12058
+ let pivotIdx;
12059
+ if ((vb - va) * (vc - vb) >= 0)
12060
+ pivotIdx = mid;
12061
+ else if ((va - vb) * (vc - va) >= 0)
12062
+ pivotIdx = lo;
12063
+ else
12064
+ pivotIdx = hi;
12065
+ const pivotVal = values[arr[pivotIdx]];
12066
+ let tmp = arr[pivotIdx];
12067
+ arr[pivotIdx] = arr[hi];
12068
+ arr[hi] = tmp;
12069
+ let store = lo;
12070
+ for (let i = lo; i < hi; i++) {
12071
+ if (values[arr[i]] < pivotVal) {
12072
+ tmp = arr[i];
12073
+ arr[i] = arr[store];
12074
+ arr[store] = tmp;
12075
+ store++;
12076
+ }
12077
+ }
12078
+ tmp = arr[store];
12079
+ arr[store] = arr[hi];
12080
+ arr[hi] = tmp;
12081
+ if (store === k)
12082
+ return;
12083
+ else if (store < k)
12084
+ lo = store + 1;
12085
+ else
12086
+ hi = store - 1;
12087
+ }
12088
+ };
12044
12089
  class KdTree {
12045
12090
  centroids;
12046
12091
  root;
12092
+ colData;
12047
12093
  constructor(centroids) {
12048
- const build = (indices, depth) => {
12049
- const { centroids } = this;
12050
- const values = centroids.columns[depth % centroids.numColumns].data;
12051
- indices.sort((a, b) => values[a] - values[b]);
12052
- if (indices.length === 1) {
12053
- return {
12054
- index: indices[0],
12055
- count: 1
12056
- };
12094
+ const numCols = centroids.numColumns;
12095
+ const colData = centroids.columns.map(c => c.data);
12096
+ const indices = new Uint32Array(centroids.numRows);
12097
+ for (let i = 0; i < indices.length; ++i) {
12098
+ indices[i] = i;
12099
+ }
12100
+ const build = (lo, hi, depth) => {
12101
+ const count = hi - lo + 1;
12102
+ if (count === 1) {
12103
+ return { index: indices[lo], count: 1 };
12057
12104
  }
12058
- else if (indices.length === 2) {
12105
+ const values = colData[depth % numCols];
12106
+ if (count === 2) {
12107
+ if (values[indices[lo]] > values[indices[hi]]) {
12108
+ const tmp = indices[lo];
12109
+ indices[lo] = indices[hi];
12110
+ indices[hi] = tmp;
12111
+ }
12059
12112
  return {
12060
- index: indices[0],
12113
+ index: indices[lo],
12061
12114
  count: 2,
12062
- right: {
12063
- index: indices[1],
12064
- count: 1
12065
- }
12115
+ right: { index: indices[hi], count: 1 }
12066
12116
  };
12067
12117
  }
12068
- const mid = indices.length >> 1;
12069
- const left = build(indices.subarray(0, mid), depth + 1);
12070
- const right = build(indices.subarray(mid + 1), depth + 1);
12118
+ const mid = lo + (count >> 1);
12119
+ nthElement(indices, lo, hi, mid, values);
12120
+ const left = build(lo, mid - 1, depth + 1);
12121
+ const right = build(mid + 1, hi, depth + 1);
12071
12122
  return {
12072
12123
  index: indices[mid],
12073
12124
  count: 1 + left.count + right.count,
@@ -12075,48 +12126,39 @@ class KdTree {
12075
12126
  right
12076
12127
  };
12077
12128
  };
12078
- const indices = new Uint32Array(centroids.numRows);
12079
- for (let i = 0; i < indices.length; ++i) {
12080
- indices[i] = i;
12081
- }
12082
12129
  this.centroids = centroids;
12083
- this.root = build(indices, 0);
12130
+ this.colData = colData;
12131
+ this.root = build(0, indices.length - 1, 0);
12084
12132
  }
12085
12133
  findNearest(point, filterFunc) {
12086
- const { centroids } = this;
12087
- const { numColumns } = centroids;
12088
- const calcDistance = (index) => {
12089
- let l = 0;
12090
- for (let i = 0; i < numColumns; ++i) {
12091
- const v = centroids.columns[i].data[index] - point[i];
12092
- l += v * v;
12093
- }
12094
- return l;
12095
- };
12134
+ const colData = this.colData;
12135
+ const numCols = colData.length;
12096
12136
  let mind = Infinity;
12097
12137
  let mini = -1;
12098
12138
  let cnt = 0;
12099
- const recurse = (node, depth) => {
12100
- const axis = depth % numColumns;
12101
- const distance = point[axis] - centroids.columns[axis].data[node.index];
12139
+ const recurse = (node, axis) => {
12140
+ const distance = point[axis] - colData[axis][node.index];
12102
12141
  const next = (distance > 0) ? node.right : node.left;
12142
+ const nextAxis = axis + 1 < numCols ? axis + 1 : 0;
12103
12143
  cnt++;
12104
12144
  if (next) {
12105
- recurse(next, depth + 1);
12145
+ recurse(next, nextAxis);
12106
12146
  }
12107
- // check index
12108
12147
  if (!filterFunc || filterFunc(node.index)) {
12109
- const thisd = calcDistance(node.index);
12148
+ let thisd = 0;
12149
+ for (let c = 0; c < numCols; c++) {
12150
+ const v = colData[c][node.index] - point[c];
12151
+ thisd += v * v;
12152
+ }
12110
12153
  if (thisd < mind) {
12111
12154
  mind = thisd;
12112
12155
  mini = node.index;
12113
12156
  }
12114
12157
  }
12115
- // check the other side
12116
12158
  if (distance * distance < mind) {
12117
12159
  const other = next === node.right ? node.left : node.right;
12118
12160
  if (other) {
12119
- recurse(other, depth + 1);
12161
+ recurse(other, nextAxis);
12120
12162
  }
12121
12163
  }
12122
12164
  };
@@ -12128,16 +12170,8 @@ class KdTree {
12128
12170
  return { indices: new Int32Array(0), distances: new Float32Array(0) };
12129
12171
  }
12130
12172
  k = Math.min(k, this.centroids.numRows);
12131
- const { centroids } = this;
12132
- const { numColumns } = centroids;
12133
- const calcDistance = (index) => {
12134
- let l = 0;
12135
- for (let i = 0; i < numColumns; ++i) {
12136
- const v = centroids.columns[i].data[index] - point[i];
12137
- l += v * v;
12138
- }
12139
- return l;
12140
- };
12173
+ const colData = this.colData;
12174
+ const numCols = colData.length;
12141
12175
  // Bounded max-heap: stores (distance, index) pairs sorted so the
12142
12176
  // farthest element is at position 0, enabling O(1) pruning bound.
12143
12177
  const heapDist = new Float32Array(k).fill(Infinity);
@@ -12145,14 +12179,12 @@ class KdTree {
12145
12179
  let heapSize = 0;
12146
12180
  const heapPush = (dist, idx) => {
12147
12181
  if (heapSize < k) {
12148
- // Heap not full yet -- insert via sift-up
12149
12182
  let pos = heapSize++;
12150
12183
  heapDist[pos] = dist;
12151
12184
  heapIdx[pos] = idx;
12152
12185
  while (pos > 0) {
12153
12186
  const parent = (pos - 1) >> 1;
12154
12187
  if (heapDist[parent] < heapDist[pos]) {
12155
- // swap
12156
12188
  const td = heapDist[parent];
12157
12189
  heapDist[parent] = heapDist[pos];
12158
12190
  heapDist[pos] = td;
@@ -12167,7 +12199,6 @@ class KdTree {
12167
12199
  }
12168
12200
  }
12169
12201
  else if (dist < heapDist[0]) {
12170
- // Replace root (farthest) and sift-down
12171
12202
  heapDist[0] = dist;
12172
12203
  heapIdx[0] = idx;
12173
12204
  let pos = 0;
@@ -12191,22 +12222,26 @@ class KdTree {
12191
12222
  }
12192
12223
  }
12193
12224
  };
12194
- const recurse = (node, depth) => {
12195
- const axis = depth % numColumns;
12196
- const distance = point[axis] - centroids.columns[axis].data[node.index];
12225
+ const recurse = (node, axis) => {
12226
+ const distance = point[axis] - colData[axis][node.index];
12197
12227
  const next = (distance > 0) ? node.right : node.left;
12228
+ const nextAxis = axis + 1 < numCols ? axis + 1 : 0;
12198
12229
  if (next) {
12199
- recurse(next, depth + 1);
12230
+ recurse(next, nextAxis);
12200
12231
  }
12201
12232
  if (!filterFunc || filterFunc(node.index)) {
12202
- const thisd = calcDistance(node.index);
12233
+ let thisd = 0;
12234
+ for (let c = 0; c < numCols; c++) {
12235
+ const v = colData[c][node.index] - point[c];
12236
+ thisd += v * v;
12237
+ }
12203
12238
  heapPush(thisd, node.index);
12204
12239
  }
12205
12240
  const bound = heapSize < k ? Infinity : heapDist[0];
12206
12241
  if (distance * distance < bound) {
12207
12242
  const other = next === node.right ? node.left : node.right;
12208
12243
  if (other) {
12209
- recurse(other, depth + 1);
12244
+ recurse(other, nextAxis);
12210
12245
  }
12211
12246
  }
12212
12247
  };
@@ -12241,6 +12276,54 @@ const OPACITY_PRUNE_THRESHOLD = 0.1;
12241
12276
  const KNN_K = 16;
12242
12277
  const MC_SAMPLES = 1;
12243
12278
  const EPS_COV = 1e-8;
12279
+ const PROGRESS_TICKS = 100;
12280
+ // Radix sort edge indices by their Float32 costs.
12281
+ // Converts floats to sortable uint32 keys (preserving order), then does
12282
+ // 4-pass LSD radix sort with 8-bit radix. Returns the number of valid
12283
+ // (finite-cost) edges written to `out`.
12284
+ const radixSortIndicesByFloat = (out, count, keys) => {
12285
+ const keyBits = new Uint32Array(keys.buffer, keys.byteOffset, keys.length);
12286
+ const sortKeys = new Uint32Array(count);
12287
+ let validCount = 0;
12288
+ for (let i = 0; i < count; i++) {
12289
+ const bits = keyBits[i];
12290
+ if ((bits & 0x7F800000) === 0x7F800000)
12291
+ continue;
12292
+ sortKeys[validCount] = (bits & 0x80000000) ? ~bits >>> 0 : (bits | 0x80000000) >>> 0;
12293
+ out[validCount] = i;
12294
+ validCount++;
12295
+ }
12296
+ if (validCount <= 1)
12297
+ return validCount;
12298
+ const n = validCount;
12299
+ const temp = new Uint32Array(n);
12300
+ const tempKeys = new Uint32Array(n);
12301
+ const counts = new Uint32Array(256);
12302
+ for (let pass = 0; pass < 4; pass++) {
12303
+ const shift = pass << 3;
12304
+ const srcIdx = (pass & 1) ? temp : out;
12305
+ const dstIdx = (pass & 1) ? out : temp;
12306
+ const srcK = (pass & 1) ? tempKeys : sortKeys;
12307
+ const dstK = (pass & 1) ? sortKeys : tempKeys;
12308
+ counts.fill(0);
12309
+ for (let i = 0; i < n; i++) {
12310
+ counts[(srcK[i] >>> shift) & 0xFF]++;
12311
+ }
12312
+ let sum = 0;
12313
+ for (let b = 0; b < 256; b++) {
12314
+ const c = counts[b];
12315
+ counts[b] = sum;
12316
+ sum += c;
12317
+ }
12318
+ for (let i = 0; i < n; i++) {
12319
+ const bucket = (srcK[i] >>> shift) & 0xFF;
12320
+ const pos = counts[bucket]++;
12321
+ dstIdx[pos] = srcIdx[i];
12322
+ dstK[pos] = srcK[i];
12323
+ }
12324
+ }
12325
+ return validCount;
12326
+ };
12244
12327
  // ---------- sigmoid / logit ----------
12245
12328
  const sigmoid$1 = (x) => 1 / (1 + Math.exp(-x));
12246
12329
  const logit = (p) => {
@@ -12728,7 +12811,6 @@ const simplifyGaussians = (dataTable, targetCount) => {
12728
12811
  let current;
12729
12812
  if (keptIndices.length < N && keptIndices.length > targetCount) {
12730
12813
  current = dataTable.permuteRows(keptIndices);
12731
- logger.debug(`opacity pruning: ${N} -> ${current.numRows} splats (threshold=${pruneThreshold.toFixed(4)})`);
12732
12814
  }
12733
12815
  else {
12734
12816
  current = dataTable;
@@ -12739,7 +12821,8 @@ const simplifyGaussians = (dataTable, targetCount) => {
12739
12821
  while (current.numRows > targetCount) {
12740
12822
  const n = current.numRows;
12741
12823
  const kEff = Math.min(Math.max(1, KNN_K), Math.max(1, n - 1));
12742
- logger.debug(`merging iteration: ${n} -> ${targetCount} splats`);
12824
+ logger.progress.begin(5);
12825
+ logger.progress.step('Building KD-tree');
12743
12826
  const cx = current.getColumnByName('x').data;
12744
12827
  const cy = current.getColumnByName('y').data;
12745
12828
  const cz = current.getColumnByName('z').data;
@@ -12752,16 +12835,21 @@ const simplifyGaussians = (dataTable, targetCount) => {
12752
12835
  const cr2 = current.getColumnByName('rot_2').data;
12753
12836
  const cr3 = current.getColumnByName('rot_3').data;
12754
12837
  const cache = buildPerSplatCache(n, cx, cy, cz, cop, cs0, cs1, cs2, cr0, cr1, cr2, cr3);
12755
- // Build KNN graph
12756
12838
  const posTable = new DataTable([
12757
12839
  new Column('x', cx instanceof Float32Array ? cx : new Float32Array(cx)),
12758
12840
  new Column('y', cy instanceof Float32Array ? cy : new Float32Array(cy)),
12759
12841
  new Column('z', cz instanceof Float32Array ? cz : new Float32Array(cz))
12760
12842
  ]);
12761
12843
  const kdTree = new KdTree(posTable);
12762
- const edgeSet = new Set();
12763
- const edges = [];
12844
+ logger.progress.step('Finding nearest neighbors');
12845
+ let edgeCapacity = Math.ceil(n * kEff / 2);
12846
+ let edgeU = new Uint32Array(edgeCapacity);
12847
+ let edgeV = new Uint32Array(edgeCapacity);
12848
+ let edgeCount = 0;
12764
12849
  const queryPoint = new Float32Array(3);
12850
+ const knnInterval = Math.max(1, Math.ceil(n / PROGRESS_TICKS));
12851
+ const knnTicks = Math.ceil(n / knnInterval);
12852
+ logger.progress.begin(knnTicks);
12765
12853
  for (let i = 0; i < n; i++) {
12766
12854
  queryPoint[0] = cx[i];
12767
12855
  queryPoint[1] = cy[i];
@@ -12769,46 +12857,58 @@ const simplifyGaussians = (dataTable, targetCount) => {
12769
12857
  const knn = kdTree.findKNearest(queryPoint, kEff + 1);
12770
12858
  for (let ki = 0; ki < knn.indices.length; ki++) {
12771
12859
  const j = knn.indices[ki];
12772
- if (j === i || j < 0)
12860
+ if (j <= i)
12773
12861
  continue;
12774
- const u = Math.min(i, j);
12775
- const v = Math.max(i, j);
12776
- const key = `${u},${v}`;
12777
- if (!edgeSet.has(key)) {
12778
- edgeSet.add(key);
12779
- edges.push([u, v]);
12862
+ if (edgeCount === edgeCapacity) {
12863
+ edgeCapacity *= 2;
12864
+ const newU = new Uint32Array(edgeCapacity);
12865
+ const newV = new Uint32Array(edgeCapacity);
12866
+ newU.set(edgeU);
12867
+ newV.set(edgeV);
12868
+ edgeU = newU;
12869
+ edgeV = newV;
12780
12870
  }
12871
+ edgeU[edgeCount] = i;
12872
+ edgeV[edgeCount] = j;
12873
+ edgeCount++;
12781
12874
  }
12875
+ if ((i + 1) % knnInterval === 0)
12876
+ logger.progress.step();
12782
12877
  }
12783
- if (edges.length === 0)
12878
+ if (n % knnInterval !== 0)
12879
+ logger.progress.step();
12880
+ if (edgeCount === 0) {
12881
+ logger.progress.cancel();
12784
12882
  break;
12785
- // Compute edge costs
12883
+ }
12884
+ logger.progress.step('Computing edge costs');
12786
12885
  const appData = [];
12787
12886
  for (let ai = 0; ai < allAppearanceCols.length; ai++) {
12788
12887
  const col = current.getColumnByName(allAppearanceCols[ai]);
12789
12888
  if (col)
12790
12889
  appData.push(col.data);
12791
12890
  }
12792
- const costs = new Float32Array(edges.length);
12793
- for (let e = 0; e < edges.length; e++) {
12794
- costs[e] = computeEdgeCost(edges[e][0], edges[e][1], cx, cy, cz, cache, Z, appData, appData.length);
12795
- }
12796
- // Greedy disjoint pair selection
12797
- const valid = [];
12798
- for (let i = 0; i < edges.length; i++) {
12799
- if (Number.isFinite(costs[i]))
12800
- valid.push(i);
12801
- }
12802
- valid.sort((a, b) => {
12803
- const d = costs[a] - costs[b];
12804
- return d !== 0 ? d : a - b;
12805
- });
12806
12891
  const mergesNeeded = n - targetCount;
12892
+ const costs = new Float32Array(edgeCount);
12893
+ const costInterval = Math.max(1, Math.ceil(edgeCount / PROGRESS_TICKS));
12894
+ const costTicks = Math.ceil(edgeCount / costInterval);
12895
+ logger.progress.begin(costTicks);
12896
+ for (let e = 0; e < edgeCount; e++) {
12897
+ costs[e] = computeEdgeCost(edgeU[e], edgeV[e], cx, cy, cz, cache, Z, appData, appData.length);
12898
+ if ((e + 1) % costInterval === 0)
12899
+ logger.progress.step();
12900
+ }
12901
+ if (edgeCount % costInterval !== 0)
12902
+ logger.progress.step();
12903
+ logger.progress.step('Merging splats');
12904
+ // Sort and greedy disjoint pair selection
12905
+ const sorted = new Uint32Array(edgeCount);
12906
+ const validCount = radixSortIndicesByFloat(sorted, edgeCount, costs);
12807
12907
  const used = new Uint8Array(n);
12808
12908
  const pairs = [];
12809
- for (let t = 0; t < valid.length; t++) {
12810
- const e = valid[t];
12811
- const u = edges[e][0], v = edges[e][1];
12909
+ for (let t = 0; t < validCount; t++) {
12910
+ const e = sorted[t];
12911
+ const u = edgeU[e], v = edgeV[e];
12812
12912
  if (used[u] || used[v])
12813
12913
  continue;
12814
12914
  used[u] = 1;
@@ -12817,9 +12917,10 @@ const simplifyGaussians = (dataTable, targetCount) => {
12817
12917
  if (pairs.length >= mergesNeeded)
12818
12918
  break;
12819
12919
  }
12820
- if (pairs.length === 0)
12920
+ if (pairs.length === 0) {
12921
+ logger.progress.cancel();
12821
12922
  break;
12822
- logger.debug(`selected ${pairs.length} merge pairs from ${edges.length} edges`);
12923
+ }
12823
12924
  // Mark which indices are consumed by merging
12824
12925
  const usedSet = new Uint8Array(n);
12825
12926
  for (let p = 0; p < pairs.length; p++) {
@@ -12847,7 +12948,7 @@ const simplifyGaussians = (dataTable, targetCount) => {
12847
12948
  newTable.columns[c].data[dst] = cols[c].data[src];
12848
12949
  }
12849
12950
  }
12850
- // Merge pairs -- cache column refs and handled set once
12951
+ // Merge pairs
12851
12952
  const mergeOut = {
12852
12953
  mu: new Float64Array(3),
12853
12954
  sc: new Float64Array(3),
@@ -12875,6 +12976,9 @@ const simplifyGaussians = (dataTable, targetCount) => {
12875
12976
  .filter(col => !handledCols.has(col.name))
12876
12977
  .map(col => ({ src: col, dst: newTable.getColumnByName(col.name) }))
12877
12978
  .filter(pair => pair.dst);
12979
+ const mergeInterval = Math.max(1, Math.ceil(pairs.length / PROGRESS_TICKS));
12980
+ const mergeTicks = Math.ceil(pairs.length / mergeInterval);
12981
+ logger.progress.begin(mergeTicks);
12878
12982
  for (let p = 0; p < pairs.length; p++, dst++) {
12879
12983
  const pi = pairs[p][0], pj = pairs[p][1];
12880
12984
  momentMatch(pi, pj, cx, cy, cz, cop, cs0, cs1, cs2, cr0, cr1, cr2, cr3, mergeOut, appData, appData.length);
@@ -12897,7 +13001,12 @@ const simplifyGaussians = (dataTable, targetCount) => {
12897
13001
  for (let u = 0; u < unhandledColPairs.length; u++) {
12898
13002
  unhandledColPairs[u].dst.data[dst] = unhandledColPairs[u].src.data[dominant];
12899
13003
  }
13004
+ if ((p + 1) % mergeInterval === 0)
13005
+ logger.progress.step();
12900
13006
  }
13007
+ if (pairs.length % mergeInterval !== 0)
13008
+ logger.progress.step();
13009
+ logger.progress.step('Finalizing');
12901
13010
  current = newTable;
12902
13011
  }
12903
13012
  return current;
@@ -16330,7 +16439,7 @@ class CompressedChunk {
16330
16439
  }
16331
16440
  }
16332
16441
 
16333
- var version = "1.9.0";
16442
+ var version = "1.9.1";
16334
16443
 
16335
16444
  const generatedByString = `Generated by splat-transform ${version}`;
16336
16445
  const chunkProps = [
@@ -20720,15 +20829,17 @@ const main = async () => {
20720
20829
  if (node.stepName) {
20721
20830
  console.error(`[${node.step}/${node.totalSteps}] ${node.stepName}`);
20722
20831
  }
20832
+ else if (node.step === 0) {
20833
+ start = hrtime();
20834
+ }
20723
20835
  else {
20724
- if (node.step === 0) {
20725
- start = hrtime();
20726
- }
20727
- else if (node.step === node.totalSteps) {
20728
- process.stderr.write(`# done in ${hrtimeDelta(start, hrtime()).toFixed(3)}s 🎉\n`);
20729
- }
20730
- else {
20731
- process.stderr.write('#');
20836
+ const displaySteps = 10;
20837
+ const curr = Math.round(displaySteps * node.step / node.totalSteps);
20838
+ const prev = Math.round(displaySteps * (node.step - 1) / node.totalSteps);
20839
+ if (curr > prev)
20840
+ process.stderr.write('#'.repeat(curr - prev));
20841
+ if (node.step === node.totalSteps) {
20842
+ process.stderr.write(` done in ${hrtimeDelta(start, hrtime()).toFixed(3)}s 🎉\n`);
20732
20843
  }
20733
20844
  }
20734
20845
  }