@playcanvas/splat-transform 1.4.0 → 1.5.0

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/README.md CHANGED
@@ -77,6 +77,8 @@ Actions can be repeated and applied in any order:
77
77
  -S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere (center, radius)
78
78
  -V, --filter-value <name,cmp,value> Keep splats where <name> <cmp> <value>
79
79
  cmp ∈ {lt,lte,gt,gte,eq,neq}
80
+ -F, --filter-visibility <n|n%> Keep the n most visible splats (by opacity * volume)
81
+ Use n% to keep a percentage of splats
80
82
  -p, --params <key=val,...> Pass parameters to .mjs generator script
81
83
  -l, --lod <n> Specify the level of detail of this model, n >= 0.
82
84
  -m, --summary Print per-column statistics to stdout
@@ -170,6 +172,12 @@ splat-transform input.ply -V opacity,gt,0.5 output.ply
170
172
 
171
173
  # Strip spherical harmonic bands higher than 2
172
174
  splat-transform input.ply --filter-harmonics 2 output.ply
175
+
176
+ # Keep only the 50000 most visible splats
177
+ splat-transform input.ply --filter-visibility 50000 output.ply
178
+
179
+ # Keep the top 25% most visible splats
180
+ splat-transform input.ply -F 25% output.ply
173
181
  ```
174
182
 
175
183
  ### Advanced Usage
@@ -275,6 +283,7 @@ import {
275
283
  | `processDataTable` | Apply a sequence of processing actions |
276
284
  | `computeSummary` | Generate statistical summary of data |
277
285
  | `sortMortonOrder` | Sort indices by Morton code for spatial locality |
286
+ | `sortByVisibility` | Sort indices by visibility score for filtering |
278
287
 
279
288
  ### File System Abstractions
280
289
 
@@ -351,6 +360,7 @@ type ProcessAction =
351
360
  | { kind: 'filterBands'; value: 0|1|2|3 }
352
361
  | { kind: 'filterBox'; min: Vec3; max: Vec3 }
353
362
  | { kind: 'filterSphere'; center: Vec3; radius: number }
363
+ | { kind: 'filterVisibility'; count: number | null; percent: number | null }
354
364
  | { kind: 'lod'; value: number }
355
365
  | { kind: 'summary' }
356
366
  | { kind: 'mortonOrder' };
package/dist/cli.mjs CHANGED
@@ -11029,42 +11029,38 @@ class DataTable {
11029
11029
  * After calling, row `i` will contain the data that was previously at row `indices[i]`.
11030
11030
  *
11031
11031
  * This is a memory-efficient alternative to `permuteRows` that modifies the table
11032
- * in-place rather than creating a copy.
11032
+ * in-place rather than creating a copy. It reuses ArrayBuffers between columns to
11033
+ * minimize memory allocations.
11033
11034
  *
11034
11035
  * @param indices - Array of indices defining the permutation. Must have the same
11035
11036
  * length as the number of rows, and must be a valid permutation
11036
11037
  * (each index 0 to n-1 appears exactly once).
11037
11038
  */
11038
11039
  permuteRowsInPlace(indices) {
11039
- const n = this.numRows;
11040
- const numCols = this.columns.length;
11041
- const visited = new Uint8Array(n);
11042
- const temps = new Array(numCols);
11043
- for (let i = 0; i < n; i++) {
11044
- if (visited[i] || indices[i] === i)
11045
- continue;
11046
- // Save values at position i
11047
- for (let c = 0; c < numCols; c++) {
11048
- temps[c] = this.columns[c].data[i];
11040
+ // Cache for reusing ArrayBuffers by size
11041
+ const cache = new Map();
11042
+ const getBuffer = (size) => {
11043
+ const cached = cache.get(size);
11044
+ if (cached) {
11045
+ cache.delete(size);
11046
+ return cached;
11049
11047
  }
11050
- // Walk the cycle
11051
- let j = i;
11052
- while (true) {
11053
- const next = indices[j];
11054
- visited[j] = 1;
11055
- if (next === i) {
11056
- // End of cycle - place saved values
11057
- for (let c = 0; c < numCols; c++) {
11058
- this.columns[c].data[j] = temps[c];
11059
- }
11060
- break;
11061
- }
11062
- // Move values from next to j
11063
- for (let c = 0; c < numCols; c++) {
11064
- this.columns[c].data[j] = this.columns[c].data[next];
11065
- }
11066
- j = next;
11048
+ return new ArrayBuffer(size);
11049
+ };
11050
+ const returnBuffer = (buffer) => {
11051
+ cache.set(buffer.byteLength, buffer);
11052
+ };
11053
+ const n = this.numRows;
11054
+ for (const column of this.columns) {
11055
+ const src = column.data;
11056
+ const constructor = src.constructor;
11057
+ const dst = new constructor(getBuffer(src.byteLength));
11058
+ // Sequential writes are cache-friendly
11059
+ for (let i = 0; i < n; i++) {
11060
+ dst[i] = src[indices[i]];
11067
11061
  }
11062
+ returnBuffer(src.buffer);
11063
+ column.data = dst;
11068
11064
  }
11069
11065
  }
11070
11066
  }
@@ -11706,6 +11702,62 @@ const sortMortonOrder = (dataTable, indices) => {
11706
11702
  generate(indices);
11707
11703
  };
11708
11704
 
11705
+ /**
11706
+ * Sorts the provided indices by visibility score (descending order).
11707
+ *
11708
+ * Visibility is computed as: linear_opacity * volume
11709
+ * where:
11710
+ * - linear_opacity = sigmoid(opacity) = 1 / (1 + exp(-opacity))
11711
+ * - volume = exp(scale_0) * exp(scale_1) * exp(scale_2)
11712
+ *
11713
+ * After calling this function, indices[0] will contain the index of the most
11714
+ * visible splat, indices[1] the second most visible, and so on.
11715
+ *
11716
+ * @param dataTable - The DataTable containing splat data.
11717
+ * @param indices - Array of indices to sort in-place.
11718
+ */
11719
+ const sortByVisibility = (dataTable, indices) => {
11720
+ const opacityCol = dataTable.getColumnByName('opacity');
11721
+ const scale0Col = dataTable.getColumnByName('scale_0');
11722
+ const scale1Col = dataTable.getColumnByName('scale_1');
11723
+ const scale2Col = dataTable.getColumnByName('scale_2');
11724
+ if (!opacityCol || !scale0Col || !scale1Col || !scale2Col) {
11725
+ logger.debug('missing required columns for visibility sorting (opacity, scale_0, scale_1, scale_2)');
11726
+ return;
11727
+ }
11728
+ if (indices.length === 0) {
11729
+ return;
11730
+ }
11731
+ const opacity = opacityCol.data;
11732
+ const scale0 = scale0Col.data;
11733
+ const scale1 = scale1Col.data;
11734
+ const scale2 = scale2Col.data;
11735
+ // Compute visibility scores for each splat
11736
+ const scores = new Float32Array(indices.length);
11737
+ for (let i = 0; i < indices.length; i++) {
11738
+ const ri = indices[i];
11739
+ // Convert logit opacity to linear using sigmoid
11740
+ const logitOpacity = opacity[ri];
11741
+ const linearOpacity = 1 / (1 + Math.exp(-logitOpacity));
11742
+ // Convert log scales to linear and compute volume
11743
+ // volume = exp(scale_0) * exp(scale_1) * exp(scale_2) = exp(scale_0 + scale_1 + scale_2)
11744
+ const volume = Math.exp(scale0[ri] + scale1[ri] + scale2[ri]);
11745
+ // Visibility score is opacity * volume
11746
+ scores[i] = linearOpacity * volume;
11747
+ }
11748
+ // Sort indices by score (descending - most visible first)
11749
+ const order = new Uint32Array(indices.length);
11750
+ for (let i = 0; i < order.length; i++) {
11751
+ order[i] = i;
11752
+ }
11753
+ order.sort((a, b) => scores[b] - scores[a]);
11754
+ // Apply the sorted order to indices
11755
+ const tmpIndices = indices.slice();
11756
+ for (let i = 0; i < indices.length; i++) {
11757
+ indices[i] = tmpIndices[order[i]];
11758
+ }
11759
+ };
11760
+
11709
11761
  /**
11710
11762
  * Abstract base class for streaming data from a source.
11711
11763
  * Uses a pull-based model where the consumer provides the buffer.
@@ -14299,7 +14351,7 @@ class CompressedChunk {
14299
14351
  }
14300
14352
  }
14301
14353
 
14302
- var version = "1.4.0";
14354
+ var version = "1.5.0";
14303
14355
 
14304
14356
  const generatedByString = `Generated by splat-transform ${version}`;
14305
14357
  const chunkProps = [
@@ -16257,6 +16309,24 @@ const processDataTable = (dataTable, processActions) => {
16257
16309
  result.permuteRowsInPlace(indices);
16258
16310
  break;
16259
16311
  }
16312
+ case 'filterVisibility': {
16313
+ const indices = new Uint32Array(result.numRows);
16314
+ for (let i = 0; i < indices.length; i++) {
16315
+ indices[i] = i;
16316
+ }
16317
+ sortByVisibility(result, indices);
16318
+ // Determine how many to keep
16319
+ let keepCount;
16320
+ if (processAction.count !== null) {
16321
+ keepCount = Math.min(processAction.count, result.numRows);
16322
+ }
16323
+ else {
16324
+ keepCount = Math.round(result.numRows * (processAction.percent ?? 100) / 100);
16325
+ }
16326
+ keepCount = Math.max(0, keepCount);
16327
+ result = result.permuteRows(indices.subarray(0, keepCount));
16328
+ break;
16329
+ }
16260
16330
  }
16261
16331
  }
16262
16332
  return result;
@@ -16534,6 +16604,7 @@ const parseArguments = async () => {
16534
16604
  'filter-harmonics': { type: 'string', short: 'H', multiple: true },
16535
16605
  'filter-box': { type: 'string', short: 'B', multiple: true },
16536
16606
  'filter-sphere': { type: 'string', short: 'S', multiple: true },
16607
+ 'filter-visibility': { type: 'string', short: 'F', multiple: true },
16537
16608
  params: { type: 'string', short: 'p', multiple: true },
16538
16609
  lod: { type: 'string', short: 'l', multiple: true },
16539
16610
  summary: { type: 'boolean', short: 'm', multiple: true },
@@ -16735,6 +16806,31 @@ const parseArguments = async () => {
16735
16806
  kind: 'mortonOrder'
16736
16807
  });
16737
16808
  break;
16809
+ case 'filter-visibility': {
16810
+ const value = t.value.trim();
16811
+ let count = null;
16812
+ let percent = null;
16813
+ if (value.endsWith('%')) {
16814
+ // Percentage mode
16815
+ percent = parseNumber(value.slice(0, -1));
16816
+ if (percent < 0 || percent > 100) {
16817
+ throw new Error(`Invalid filter-visibility percentage: ${value}. Must be between 0% and 100%.`);
16818
+ }
16819
+ }
16820
+ else {
16821
+ // Count mode
16822
+ count = parseInteger(value);
16823
+ if (count < 0) {
16824
+ throw new Error(`Invalid filter-visibility count: ${value}. Must be a non-negative integer.`);
16825
+ }
16826
+ }
16827
+ current.processActions.push({
16828
+ kind: 'filterVisibility',
16829
+ count,
16830
+ percent
16831
+ });
16832
+ break;
16833
+ }
16738
16834
  }
16739
16835
  }
16740
16836
  }
@@ -16767,6 +16863,8 @@ ACTIONS (can be repeated, in any order)
16767
16863
  -S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere (center, radius)
16768
16864
  -V, --filter-value <name,cmp,value> Keep Gaussians where <name> <cmp> <value>
16769
16865
  cmp ∈ {lt,lte,gt,gte,eq,neq}
16866
+ -F, --filter-visibility <n|n%> Keep the n most visible Gaussians (by opacity * volume)
16867
+ Use n% to keep a percentage of Gaussians
16770
16868
  -p, --params <key=val,...> Pass parameters to .mjs generator script
16771
16869
  -l, --lod <n> Specify the level of detail, n >= 0
16772
16870
  -m, --summary Print per-column statistics to stdout