@playcanvas/splat-transform 2.0.4 → 2.0.6

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
@@ -69,7 +69,7 @@ splat-transform [GLOBAL] input [ACTIONS] ... output [ACTIONS]
69
69
 
70
70
  ## Actions
71
71
 
72
- Actions can be repeated and applied in any order:
72
+ Actions execute in the order specified and can be repeated. Any action may appear after any input or output file:
73
73
 
74
74
  ```none
75
75
  -t, --translate <x,y,z> Translate Gaussians by (x, y, z)
@@ -89,17 +89,19 @@ Actions can be repeated and applied in any order:
89
89
  Use n% to keep a percentage of Gaussians
90
90
  -G, --filter-floaters [size,op,min] Remove Gaussians not contributing to any solid voxel.
91
91
  Evaluates each Gaussian at occupied voxel centers.
92
- Default: size=0.05, opacity=0.1, min=0.004 (1/255)
92
+ Default: size=0.05, opacity=0.1, min=0.004 (1/255).
93
+ Bare flag (no value) uses all defaults.
93
94
  -D, --filter-cluster [res,op,min] Keep only the connected cluster at --seed-pos.
94
95
  GPU-voxelizes at coarse resolution (res world units/voxel).
95
- Default: res=1.0, opacity=0.999, min=0.1
96
+ Default: res=1.0, opacity=0.999, min=0.1.
97
+ Bare flag (no value) uses all defaults.
96
98
  -p, --params <key=val,...> Pass parameters to .mjs generator script
97
- -l, --lod <n> Specify the level of detail, n >= 0
99
+ -l, --lod <n> Tag the Gaussians with LOD level n (n >= 0)
98
100
  -m, --summary Print per-column statistics to stdout
99
101
  -M, --morton-order Reorder Gaussians by Morton code (Z-order curve)
100
102
  ```
101
103
 
102
- ## Global Options
104
+ ## General Options
103
105
 
104
106
  ```none
105
107
  -h, --help Show this help and exit
@@ -109,29 +111,80 @@ Actions can be repeated and applied in any order:
109
111
  --mem Show memory usage in progress output
110
112
  --tty Interactive bar rendering (default on a TTY; --no-tty to disable)
111
113
  -w, --overwrite Overwrite output file if it exists
112
- -i, --iterations <n> Iterations for SOG SH compression (more=better). Default: 10
114
+ ```
115
+
116
+ ## GPU Options
117
+
118
+ Used by SOG compression and GPU voxelization (`--filter-cluster`, `--filter-floaters`, `.voxel.json` output).
119
+
120
+ ```none
113
121
  -L, --list-gpus List available GPU adapters and exit
114
- -g, --gpu <n|cpu> Select device for SOG compression: GPU adapter index | 'cpu'
122
+ -g, --gpu <n|cpu> Device for GPU operations: GPU adapter index | 'cpu'
123
+ ('cpu' disables GPU and is incompatible with
124
+ GPU-only features like --filter-cluster)
125
+ ```
126
+
127
+ ## SOG Compression Options
128
+
129
+ Apply when writing `.sog`, `meta.json`, `lod-meta.json`, or `.html` outputs.
130
+
131
+ ```none
132
+ -i, --iterations <n> Iterations for SH compression (more=better). Default: 10
133
+ ```
134
+
135
+ ## HTML Viewer Output Options
136
+
137
+ Apply when writing `.html` outputs.
138
+
139
+ ```none
115
140
  -E, --viewer-settings <settings.json> HTML viewer settings JSON file
116
141
  -U, --unbundled Generate unbundled HTML viewer with separate files
142
+ ```
143
+
144
+ > [!NOTE]
145
+ > See the [SuperSplat Viewer Settings Schema](https://github.com/playcanvas/supersplat-viewer?tab=readme-ov-file#settings-schema) for details on how to pass data to the `-E` option.
146
+
147
+ ## LCC Input Options
148
+
149
+ Apply when reading `.lcc` files.
150
+
151
+ ```none
117
152
  -O, --lod-select <n,n,...> Comma-separated LOD levels to read from LCC input
153
+ ```
154
+
155
+ ## LOD Output Options
156
+
157
+ Apply when writing `lod-meta.json` (multi-LOD streaming SOG bundle).
158
+
159
+ ```none
118
160
  -C, --lod-chunk-count <n> Approximate number of Gaussians per LOD chunk in K. Default: 512
119
161
  -X, --lod-chunk-extent <n> Approximate size of an LOD chunk in world units (m). Default: 16
120
- --voxel-params [size,opacity] Voxel size and opacity threshold for .voxel.json. Default: 0.05,0.1
121
- --voxel-external-fill [size] Fill exterior voxels by dilation from seed. Default size: 1.6
122
- --voxel-floor-fill [radius] Fill each column upward from bottom until hitting solid (runs before carve).
162
+ ```
163
+
164
+ ## Voxel Output Options
165
+
166
+ Apply when writing `.voxel.json` (sparse voxel octree for collision detection). See the [Collision Mesh Guide](guides/COLLISION.md) for a deep dive on each step and tuning.
167
+
168
+ ```none
169
+ --voxel-params [size,opacity] Voxel size and opacity threshold. Default: 0.05,0.1
170
+ --voxel-external-fill [size] Seal exterior voxels via boundary flood fill (interior scenes).
171
+ [size] (world units) is the dilation distance applied
172
+ before the flood fill to bridge small wall gaps.
173
+ --seed-pos is used to verify the volume is enclosed at
174
+ the seed; the fill is skipped if the seed is reachable
175
+ from outside.
176
+ Default size: 1.6
177
+ --voxel-floor-fill [radius] Fill each column upward from bottom until hitting solid (exterior scenes).
123
178
  Optional radius (world units): only patch XZ areas surrounded by floor
124
179
  within 2*radius; large empty exterior areas are left alone.
125
180
  Default radius: 1.6
126
- --voxel-carve [h,r] Carve navigable space using capsule flood fill from seed.
181
+ --voxel-carve [h,r] Carve navigable space using capsule flood fill from seed.
127
182
  Default: height=1.6, radius=0.2
128
- --seed-pos <x,y,z> Seed position for voxel processing and --filter-cluster. Default: 0,0,0
129
- -K, --collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default shape: smooth.
183
+ --seed-pos <x,y,z> Seed position for voxel fill/carve and --filter-cluster.
184
+ Default: 0,0,0
185
+ -K, --collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default: smooth
130
186
  ```
131
187
 
132
- > [!NOTE]
133
- > See the [SuperSplat Viewer Settings Schema](https://github.com/playcanvas/supersplat-viewer?tab=readme-ov-file#settings-schema) for details on how to pass data to the `-E` option.
134
-
135
188
  ## Examples
136
189
 
137
190
  ### Basic Operations
@@ -244,22 +297,54 @@ splat-transform gen-grid.mjs -p width=10,height=10,scale=10,color=0.1 scenes/gri
244
297
 
245
298
  ### Voxel Format
246
299
 
247
- The voxel format stores sparse voxel octree data for collision detection. It consists of two files: `.voxel.json` (metadata) and `.voxel.bin` (binary octree data).
300
+ The voxel format stores sparse voxel octree data for collision detection. It consists of two files: `.voxel.json` (metadata) and `.voxel.bin` (binary octree data). Pass `-K` to also emit a `.collision.glb` mesh derived from the voxel grid.
301
+
302
+ For a step-by-step walkthrough of each option (with illustrations), see the [Collision Mesh Guide](guides/COLLISION.md).
303
+
304
+ #### Recommended pipeline
248
305
 
249
306
  ```bash
250
- # Generate voxel collision data from a splat file
251
- splat-transform input.ply output.voxel.json
307
+ splat-transform input.ply \
308
+ --filter-cluster --seed-pos x,y,z \
309
+ [--voxel-external-fill | --voxel-floor-fill] [--voxel-carve] \
310
+ [-K [smooth|faces]] \
311
+ output.voxel.json
312
+ ```
252
313
 
253
- # Generate voxel data with custom resolution and opacity threshold
254
- splat-transform --voxel-params 0.1,0.3 input.ply output.voxel.json
314
+ `--filter-cluster` isolates the central scene and discards stray floaters before voxelization. `--seed-pos` is shared by `--filter-cluster` and the voxel fill/carve passes — set it once to a known-walkable point inside the scene.
255
315
 
256
- # Generate voxel data with exterior fill and carve
257
- splat-transform --voxel-external-fill --voxel-carve input.ply output.voxel.json
316
+ #### Interior scenes (rooms, indoor scans)
317
+
318
+ Use `--voxel-external-fill` to seal the void around the room interior, then `--voxel-carve` to hollow out the navigable space:
319
+
320
+ ```bash
321
+ splat-transform room.ply \
322
+ --filter-cluster --seed-pos 0,1,0 \
323
+ --voxel-external-fill --voxel-carve \
324
+ -K room.voxel.json
325
+ ```
326
+
327
+ #### Exterior scenes (outdoor objects, terrain)
328
+
329
+ Use `--voxel-floor-fill` to fill the ground beneath surfaces, optionally followed by `--voxel-carve`:
330
+
331
+ ```bash
332
+ splat-transform terrain.ply \
333
+ --filter-cluster --seed-pos 0,0,0 \
334
+ --voxel-floor-fill \
335
+ -K terrain.voxel.json
336
+ ```
337
+
338
+ #### Other examples
339
+
340
+ ```bash
341
+ # Voxelize with custom resolution and opacity threshold
342
+ splat-transform --voxel-params 0.1,0.3 input.ply output.voxel.json
258
343
 
259
- # Generate voxel data with custom seed position and carve parameters
344
+ # Custom carve capsule (height, radius)
260
345
  splat-transform --seed-pos 1,0,0 --voxel-carve 2.0,0.3 input.ply output.voxel.json
261
346
 
262
- # Generate voxel data with a watertight voxel-face collision mesh
347
+ # Watertight voxel-face collision mesh
263
348
  splat-transform -K faces input.ply output.voxel.json
264
349
  ```
265
350
 
package/dist/cli.mjs CHANGED
@@ -17494,11 +17494,11 @@ class CompressedChunk {
17494
17494
  /**
17495
17495
  * The splat-transform version (semver MAJOR.MINOR.PATCH).
17496
17496
  */
17497
- const version = '2.0.4';
17497
+ const version = '2.0.6';
17498
17498
  /**
17499
17499
  * The splat-transform revision (short Git hash of HEAD at build time).
17500
17500
  */
17501
- const revision = 'fa46db3';
17501
+ const revision = 'aa3dbae';
17502
17502
 
17503
17503
  const generatedByString = `Generated by splat-transform ${version}`;
17504
17504
  const chunkProps = [
@@ -19486,13 +19486,17 @@ class GpuDilation {
19486
19486
  // Extract: clear bufferA, dispatch extract from sparse src into bufferA.
19487
19487
  this.dispatchClear(slot, slot.bufferA, numWords);
19488
19488
  this.dispatchExtract(slot, minBx, minBy, minBz, outerBx, outerBy, outerBz, numXWords);
19489
- // Tiny throwaway readback forces a queue submit between extract
19490
- // and dilate. WITHOUT this, atomicOr writes from the extract pass
19491
- // aren't reliably visible to the next dilate pass (apparent
19492
- // missing memory barrier in PlayCanvas/Dawn for cross-pass atomic
19493
- // writes). Promise is intentionally not awaitedonly the
19494
- // implicit submit+barrier matters. 16 bytes negligible PCIe.
19495
- slot.bufferA.read(0, 16, null, true).catch(() => { });
19489
+ // Force a queue submission boundary between the extract pass (which
19490
+ // writes bufferA via atomicOr bound as `storage, read_write` with
19491
+ // `array<atomic<u32>>`) and the dilate-X pass (which reads bufferA
19492
+ // bound as `storage, read` with `array<u32>`). Without this, the
19493
+ // atomic writes are not reliably visible to the next pass dilation
19494
+ // produces empty output. Other inter-pass transitions in this
19495
+ // pipeline (clear→extract, dilateX→Z→Y, Y→compact) rely on automatic
19496
+ // intra-encoder synchronization without issue. Verified raw Dawn
19497
+ // synchronizes this case correctly, so the bug is somewhere in
19498
+ // PlayCanvas's compute dispatch / bind-group path.
19499
+ this.device.submit();
19496
19500
  // X-pass: A → B
19497
19501
  this.dispatchX(slot, slot.bufferA, slot.bufferB, numXWords, outerNy, outerNz, halfExtentXZ);
19498
19502
  // Z-pass: B → A
@@ -20907,8 +20911,16 @@ const binIndices = (parent, lod) => {
20907
20911
  */
20908
20912
  const writeLod = async (options, fs) => {
20909
20913
  const { filename, iterations, createDevice, chunkCount, chunkExtent } = options;
20910
- const dataTable = convertToSpace(options.dataTable, Transform.IDENTITY, true);
20911
- const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.IDENTITY, true) : null;
20914
+ // Operate in PLY space so per-leaf bounds in tree.bound are in the same
20915
+ // coordinate frame as the SOG chunk data emitted by writeSog (which also
20916
+ // converts to Transform.PLY). Without this, view-dependent streaming
20917
+ // built on tree.bound picks the wrong chunks because the bounds are
20918
+ // 180°-Z-rotated relative to the splat positions inside them.
20919
+ // This intentionally mutates the input tables to avoid doubling peak
20920
+ // memory during LOD export. Callers should treat writeLod as consuming its
20921
+ // DataTable inputs.
20922
+ const dataTable = convertToSpace(options.dataTable, Transform.PLY, true);
20923
+ const envDataTable = options.envDataTable ? convertToSpace(options.envDataTable, Transform.PLY, true) : null;
20912
20924
  const outputDir = dirname(filename);
20913
20925
  // ensure top-level output folder exists
20914
20926
  await fs.mkdir(outputDir);
@@ -26420,8 +26432,14 @@ const voxelizeToBuffer = async (bvh, gpuVoxelization, gridBounds, voxelResolutio
26420
26432
  const buffer = new BlockMaskBuffer();
26421
26433
  const bStride = numBlocksX * numBlocksY;
26422
26434
  const batchSize = 16;
26423
- const MEGA_MAX_BATCHES = 512;
26424
- const MEGA_MAX_INDICES = 4 * 1024 * 1024;
26435
+ // Caps for one mega-flush. Sized to keep a single compute dispatch's
26436
+ // wall-clock comfortably under Windows D3D12 TDR (~2s watchdog) on slower
26437
+ // GPUs without materially affecting throughput on fast GPUs (the
26438
+ // double-buffered slots keep the GPU fed across the extra flushes).
26439
+ // Output is bit-identical regardless: each batch's full Gaussian list rides
26440
+ // with it, and slot buffers grow on demand to accommodate any oversize batch.
26441
+ const MEGA_MAX_BATCHES = 256;
26442
+ const MEGA_MAX_INDICES = 2 * 1024 * 1024;
26425
26443
  const maxBlocks = GpuVoxelization.MAX_BLOCKS_PER_BATCH;
26426
26444
  const numSlots = GpuVoxelization.NUM_SLOTS;
26427
26445
  const slotIndexArrays = [];
@@ -28227,21 +28245,32 @@ const cropToNavigable = (grid, gridBounds, voxelResolution) => {
28227
28245
  return { grid, gridBounds };
28228
28246
  }
28229
28247
  const { minBx, minBy, minBz, maxBx, maxBy, maxBz } = navBounds;
28230
- const cropMaxBx = maxBx + 1;
28231
- const cropMaxBy = maxBy + 1;
28232
- const cropMaxBz = maxBz + 1;
28233
- if (minBx === 0 && minBy === 0 && minBz === 0 &&
28248
+ // Pad by 1 block on each side so the cropped grid retains the solid wall
28249
+ // blocks immediately surrounding the navigable cavity. Matches the
28250
+ // MARGIN = 1 pattern used by carve() before this re-crop strips it. The
28251
+ // collision-mesh extractors treat out-of-grid as empty, so without this
28252
+ // padding the mesh has holes wherever the cavity reaches the cropped
28253
+ // boundary; with it, the mesh extractor sees a real SOLID→EMPTY
28254
+ // transition at the cavity edge and emits a sealed wall there.
28255
+ const MARGIN = 1;
28256
+ const cropMinBx = Math.max(0, minBx - MARGIN);
28257
+ const cropMinBy = Math.max(0, minBy - MARGIN);
28258
+ const cropMinBz = Math.max(0, minBz - MARGIN);
28259
+ const cropMaxBx = Math.min(nbx, maxBx + 1 + MARGIN);
28260
+ const cropMaxBy = Math.min(nby, maxBy + 1 + MARGIN);
28261
+ const cropMaxBz = Math.min(nbz, maxBz + 1 + MARGIN);
28262
+ if (cropMinBx === 0 && cropMinBy === 0 && cropMinBz === 0 &&
28234
28263
  cropMaxBx === nbx && cropMaxBy === nby && cropMaxBz === nbz) {
28235
28264
  return { grid, gridBounds };
28236
28265
  }
28237
28266
  const cropBar = logger.bar('Cropping grid', grid.types.length);
28238
- const croppedGrid = grid.cropTo(minBx, minBy, minBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
28267
+ const croppedGrid = grid.cropTo(cropMinBx, cropMinBy, cropMinBz, cropMaxBx, cropMaxBy, cropMaxBz, done => cropBar.update(done));
28239
28268
  cropBar.end();
28240
28269
  const blockSize = 4 * voxelResolution;
28241
- const croppedMin = new Vec3(gridBounds.min.x + minBx * blockSize, gridBounds.min.y + minBy * blockSize, gridBounds.min.z + minBz * blockSize);
28270
+ const croppedMin = new Vec3(gridBounds.min.x + cropMinBx * blockSize, gridBounds.min.y + cropMinBy * blockSize, gridBounds.min.z + cropMinBz * blockSize);
28242
28271
  const croppedBounds = {
28243
28272
  min: croppedMin,
28244
- max: new Vec3(croppedMin.x + (cropMaxBx - minBx) * blockSize, croppedMin.y + (cropMaxBy - minBy) * blockSize, croppedMin.z + (cropMaxBz - minBz) * blockSize)
28273
+ max: new Vec3(croppedMin.x + (cropMaxBx - cropMinBx) * blockSize, croppedMin.y + (cropMaxBy - cropMinBy) * blockSize, croppedMin.z + (cropMaxBz - cropMinBz) * blockSize)
28245
28274
  };
28246
28275
  return { grid: croppedGrid, gridBounds: croppedBounds };
28247
28276
  };
@@ -28368,13 +28397,27 @@ const writeVoxel = async (options, fs) => {
28368
28397
  gpuVoxelization = new GpuVoxelization(device);
28369
28398
  gpuVoxelization.uploadAllGaussians(pcDataTable, extentsResult.extents);
28370
28399
  // Align grid bounds to block boundaries BEFORE voxelization so the
28371
- // block coordinates used during voxelization match what the reader expects.
28372
- // When fillExterior runs, pad by halfExtent + 1 voxels per side so the
28373
- // boundary-face flood seeds survive the dilation (notably below the floor).
28400
+ // block coordinates used during voxelization match what the reader
28401
+ // expects. fillExterior and fillFloor both need a margin of empty
28402
+ // voxels outside the splat's tight 3-sigma extents to do their job:
28403
+ // fillExterior so the boundary-face flood seeds survive its dilation
28404
+ // (notably below the floor), fillFloor so its column walk has empty
28405
+ // XZ columns to convert into wall pillars and the dilation halo to
28406
+ // extend the floor footprint outward.
28407
+ //
28408
+ // Lateral pad combines both as `dilation_radius + 1` voxels per side.
28409
+ // Vertical pad is only contributed by exteriorPad — fillFloor's
28410
+ // dilation is XZ-only, and Y padding would extend the wall pillars
28411
+ // above the splat's natural ceiling and below its floor.
28374
28412
  const exteriorPad = hasFillExterior ?
28375
28413
  (Math.ceil(navExteriorRadius / voxelResolution) + 1) * voxelResolution :
28376
28414
  0;
28377
- let gridBounds = alignGridBounds(bounds.min.x - exteriorPad, bounds.min.y - exteriorPad, bounds.min.z - exteriorPad, bounds.max.x + exteriorPad, bounds.max.y + exteriorPad, bounds.max.z + exteriorPad, voxelResolution);
28415
+ const floorPad = hasFloorFill ?
28416
+ (Math.ceil(floorFillDilation / voxelResolution) + 1) * voxelResolution :
28417
+ 0;
28418
+ const padXZ = Math.max(exteriorPad, floorPad);
28419
+ const padY = exteriorPad;
28420
+ let gridBounds = alignGridBounds(bounds.min.x - padXZ, bounds.min.y - padY, bounds.min.z - padXZ, bounds.max.x + padXZ, bounds.max.y + padY, bounds.max.z + padXZ, voxelResolution);
28378
28421
  const buffer = await voxelizeToBuffer(bvh, gpuVoxelization, gridBounds, voxelResolution, opacityCutoff);
28379
28422
  bvh = null;
28380
28423
  pcDataTable = null;
@@ -29051,6 +29094,17 @@ const createDevice = async (adapterName) => {
29051
29094
  stencil: false
29052
29095
  });
29053
29096
  await graphicsDevice.createDevice();
29097
+ // Surface Dawn's device-lost reason directly. PlayCanvas logs this only via
29098
+ // Debug.warn (debug builds) and then attempts to recreate the device, which
29099
+ // can fail noisily (e.g. DXGI_ERROR_DEVICE_REMOVED on D3D12 TDR) and bury
29100
+ // the original cause. Skip the `destroyed` reason — that fires on intentional
29101
+ // device.destroy() during normal shutdown.
29102
+ // @ts-ignore - wgpu is private on WebgpuGraphicsDevice but exposed in practice
29103
+ graphicsDevice.wgpu?.lost?.then((info) => {
29104
+ if (info?.reason === 'destroyed')
29105
+ return;
29106
+ logger.error(`WebGPU device was lost: reason=${info.reason || 'unknown'}, message=${info.message || '(none)'}`);
29107
+ });
29054
29108
  return graphicsDevice;
29055
29109
  };
29056
29110
 
@@ -29706,98 +29760,71 @@ SUPPORTED INPUTS
29706
29760
  SUPPORTED OUTPUTS
29707
29761
  .ply .compressed.ply .sog meta.json lod-meta.json .glb .csv .html .voxel.json null
29708
29762
 
29709
- ACTIONS (can be repeated, in any order)
29763
+ ACTIONS (executed in order; can be repeated)
29710
29764
  -t, --translate <x,y,z> Translate Gaussians by (x, y, z)
29711
- -r, --rotate <x,y,z> Rotate Gaussians by Euler angles (x, y, z), in degrees
29765
+ -r, --rotate <x,y,z> Rotate Gaussians by Euler angles, in degrees
29712
29766
  -s, --scale <factor> Uniformly scale Gaussians by factor
29713
29767
  -H, --filter-harmonics <0|1|2|3> Remove spherical harmonic bands > n
29714
- -N, --filter-nan Remove Gaussians with NaN values and most Inf values;
29715
- retains +Infinity in opacity and -Infinity in scale_*
29768
+ -N, --filter-nan Remove Gaussians with NaN values and most Inf values
29716
29769
  -B, --filter-box <x,y,z,X,Y,Z> Remove Gaussians outside box (min, max corners)
29717
- -S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere (center, radius)
29718
- -V, --filter-value <name,cmp,value> Keep Gaussians where <name> <cmp> <value>
29770
+ -S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere
29771
+ -V, --filter-value <name,cmp,value> Keep Gaussians where <name> <cmp> <value>;
29719
29772
  cmp ∈ {lt,lte,gt,gte,eq,neq}
29720
- opacity, scale_*, f_dc_* use transformed values
29721
- (linear opacity 0-1, linear scale, linear color 0-1).
29722
- Append _raw for raw PLY values (e.g. opacity_raw).
29723
- -F, --decimate <n|n%> Simplify to n Gaussians via progressive pairwise merging
29724
- Use n% to keep a percentage of Gaussians
29725
- -G, --filter-floaters [size,op,min] Remove Gaussians not contributing to any solid voxel.
29726
- Evaluates each Gaussian at occupied voxel centers.
29727
- Default: size=0.05, opacity=0.1, min=0.004 (1/255)
29728
- -D, --filter-cluster [res,op,min] Keep only the connected cluster at --seed-pos.
29729
- GPU-voxelizes at coarse resolution (res world units/voxel).
29730
- Default: res=1.0, opacity=0.999, min=0.1
29773
+ -F, --decimate <n|n%> Simplify to n (or n%) Gaussians via pairwise merging
29774
+ -G, --filter-floaters [size,op,min] Remove Gaussians not contributing to any solid voxel. Default: 0.05,0.1,0.004
29775
+ -D, --filter-cluster [res,op,min] Keep only the connected cluster at --seed-pos. Default: 1.0,0.999,0.1
29731
29776
  -p, --params <key=val,...> Pass parameters to .mjs generator script
29732
- -l, --lod <n> Specify the level of detail, n >= 0
29777
+ -l, --lod <n> Tag the Gaussians with LOD level n (n >= 0)
29733
29778
  -m, --summary Print per-column statistics to stdout
29734
29779
  -M, --morton-order Reorder Gaussians by Morton code (Z-order curve)
29735
29780
 
29736
- GLOBAL OPTIONS
29781
+ GENERAL
29737
29782
  -h, --help Show this help and exit
29738
29783
  -v, --version Show version and exit
29739
29784
  -q, --quiet Suppress non-error output
29740
29785
  --verbose Show debug-level diagnostics
29741
29786
  --mem Show peak memory in progress output
29742
- --tty Interactive bar rendering (default on a TTY; --no-tty to disable)
29787
+ --tty Interactive bar rendering (--no-tty to disable)
29743
29788
  -w, --overwrite Overwrite output file if it exists
29744
- -i, --iterations <n> Iterations for SOG SH compression (more=better). Default: 10
29745
- -L, --list-gpus List available GPU adapters and exit
29746
- -g, --gpu <n|cpu> Select device for SOG compression: GPU adapter index | 'cpu'
29747
- -E, --viewer-settings <settings.json> HTML viewer settings JSON file
29748
- -U, --unbundled Generate unbundled HTML viewer with separate files
29749
- -O, --lod-select <n,n,...> Comma-separated LOD levels to read from LCC input
29750
- -C, --lod-chunk-count <n> Approximate number of Gaussians per LOD chunk in K. Default: 512
29751
- -X, --lod-chunk-extent <n> Approximate size of an LOD chunk in world units (m). Default: 16
29752
- --voxel-params [size,opacity] Voxel size and opacity threshold for .voxel.json. Default: 0.05,0.1
29753
- --voxel-external-fill [size] Fill exterior voxels by dilation from seed. Default size: 1.6
29754
- --voxel-floor-fill [size] Fill below-floor voxels by upward column walk from bottom. Default size: 1.6
29755
- --voxel-carve [h,r] Carve navigable space using capsule flood fill from seed. Default: 1.6,0.2
29756
- --seed-pos <x,y,z> Seed position for voxel processing and --filter-cluster. Default: 0,0,0
29757
- -K, --collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default shape: smooth
29758
-
29759
- EXAMPLES
29760
- # Scale then translate
29761
- splat-transform bunny.ply -s 0.5 -t 0,0,10 bunny-scaled.ply
29762
-
29763
- # Merge two files with transforms and compress to SOG format
29764
- splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.sog
29765
29789
 
29766
- # Generate unbundled HTML viewer with separate CSS, JS and SOG files
29767
- splat-transform -U bunny.ply bunny-viewer.html
29768
-
29769
- # Generate synthetic splats using a generator script
29770
- splat-transform gen-grid.mjs -p width=500,height=500,scale=0.1 grid.ply
29771
-
29772
- # Generate LOD with custom chunk size and node split size
29773
- splat-transform -O 0,1,2 -C 1024 -X 32 input.lcc output/lod-meta.json
29790
+ GPU (used by SOG compression and GPU voxelization: --filter-cluster, --filter-floaters, .voxel.json output)
29791
+ -L, --list-gpus List available GPU adapters and exit
29792
+ -g, --gpu <n|cpu> Device for GPU operations: GPU adapter index | 'cpu'
29793
+ ('cpu' disables GPU and is incompatible with GPU-only features)
29774
29794
 
29775
- # Generate voxel data
29776
- splat-transform input.ply output.voxel.json
29795
+ SOG COMPRESSION (.sog, meta.json, lod-meta.json, .html outputs)
29796
+ -i, --iterations <n> SH compression iterations (more=better). Default: 10
29777
29797
 
29778
- # Generate voxel data with collision mesh
29779
- splat-transform -K input.ply output.voxel.json
29798
+ HTML VIEWER OUTPUT (.html)
29799
+ -E, --viewer-settings <settings.json> HTML viewer settings JSON file
29800
+ -U, --unbundled Generate unbundled HTML viewer with separate files
29780
29801
 
29781
- # Generate voxel data with voxel-face collision mesh
29782
- splat-transform -K faces input.ply output.voxel.json
29802
+ LCC INPUT (.lcc)
29803
+ -O, --lod-select <n,n,...> Comma-separated LOD levels to read
29783
29804
 
29784
- # Generate voxel data with custom resolution and opacity threshold
29785
- splat-transform --voxel-params 0.1,0.3 input.ply output.voxel.json
29805
+ LOD OUTPUT (lod-meta.json)
29806
+ -C, --lod-chunk-count <n> Approximate Gaussians per LOD chunk in K. Default: 512
29807
+ -X, --lod-chunk-extent <n> Approximate LOD chunk size in world units. Default: 16
29786
29808
 
29787
- # Generate voxel data with exterior fill and carve
29788
- splat-transform --voxel-external-fill --voxel-carve input.ply output.voxel.json
29809
+ VOXEL OUTPUT (.voxel.json)
29810
+ --voxel-params [size,opacity] Voxel size + opacity threshold. Default: 0.05,0.1
29811
+ --voxel-external-fill [size] Seal exterior voxels via boundary flood fill (interior scenes). Default: 1.6
29812
+ --voxel-floor-fill [radius] Fill columns upward from bottom (exterior scenes). Default: 1.6
29813
+ --voxel-carve [h,r] Carve navigable space via capsule flood fill from seed. Default: 1.6,0.2
29814
+ --seed-pos <x,y,z> Seed position for voxel fill/carve and --filter-cluster. Default: 0,0,0
29815
+ -K, --collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default: smooth
29789
29816
 
29790
- # Generate voxel data with custom seed position and carve parameters
29791
- splat-transform --seed-pos 1,0,0 --voxel-carve 2.0,0.3 input.ply output.voxel.json
29817
+ EXAMPLES
29818
+ # Convert formats
29819
+ splat-transform input.ply output.sog
29792
29820
 
29793
- # Print statistical summary, then write output
29794
- splat-transform bunny.ply --summary output.ply
29821
+ # Merge files with transforms
29822
+ splat-transform -w a.ply -r 0,90,0 b.ply -s 2 merged.sog
29795
29823
 
29796
- # Print summary without writing a file (discard output)
29797
- splat-transform bunny.ply -m null
29824
+ # Generate voxel collision data
29825
+ splat-transform input.ply --filter-cluster output.voxel.json
29798
29826
 
29799
- # Read input from a URL and write to a local file
29800
- splat-transform https://example.com/scene.ply scene.sog
29827
+ More examples: https://github.com/playcanvas/splat-transform#examples
29801
29828
  `;
29802
29829
  const main = async () => {
29803
29830
  const startTime = performance.now();