@playcanvas/splat-transform 1.4.1 → 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
@@ -11702,6 +11702,62 @@ const sortMortonOrder = (dataTable, indices) => {
11702
11702
  generate(indices);
11703
11703
  };
11704
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
+
11705
11761
  /**
11706
11762
  * Abstract base class for streaming data from a source.
11707
11763
  * Uses a pull-based model where the consumer provides the buffer.
@@ -14295,7 +14351,7 @@ class CompressedChunk {
14295
14351
  }
14296
14352
  }
14297
14353
 
14298
- var version = "1.4.1";
14354
+ var version = "1.5.0";
14299
14355
 
14300
14356
  const generatedByString = `Generated by splat-transform ${version}`;
14301
14357
  const chunkProps = [
@@ -16253,6 +16309,24 @@ const processDataTable = (dataTable, processActions) => {
16253
16309
  result.permuteRowsInPlace(indices);
16254
16310
  break;
16255
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
+ }
16256
16330
  }
16257
16331
  }
16258
16332
  return result;
@@ -16530,6 +16604,7 @@ const parseArguments = async () => {
16530
16604
  'filter-harmonics': { type: 'string', short: 'H', multiple: true },
16531
16605
  'filter-box': { type: 'string', short: 'B', multiple: true },
16532
16606
  'filter-sphere': { type: 'string', short: 'S', multiple: true },
16607
+ 'filter-visibility': { type: 'string', short: 'F', multiple: true },
16533
16608
  params: { type: 'string', short: 'p', multiple: true },
16534
16609
  lod: { type: 'string', short: 'l', multiple: true },
16535
16610
  summary: { type: 'boolean', short: 'm', multiple: true },
@@ -16731,6 +16806,31 @@ const parseArguments = async () => {
16731
16806
  kind: 'mortonOrder'
16732
16807
  });
16733
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
+ }
16734
16834
  }
16735
16835
  }
16736
16836
  }
@@ -16763,6 +16863,8 @@ ACTIONS (can be repeated, in any order)
16763
16863
  -S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere (center, radius)
16764
16864
  -V, --filter-value <name,cmp,value> Keep Gaussians where <name> <cmp> <value>
16765
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
16766
16868
  -p, --params <key=val,...> Pass parameters to .mjs generator script
16767
16869
  -l, --lod <n> Specify the level of detail, n >= 0
16768
16870
  -m, --summary Print per-column statistics to stdout