SplatTransform - 3D Gaussian Splat Converter
| User Guide | API Reference | Blog | Forum |
SplatTransform is an open source library and CLI tool for converting and editing Gaussian splats. It can:
📥 Read PLY, Compressed PLY, SOG, SPZ, SPLAT, KSPLAT, LCC and LCC2 formats
📤 Write PLY, Compressed PLY, SOG, SPZ, GLB, CSV, HTML Viewer, LOD, Voxel and WebP image formats
📊 Generate statistical summaries for data analysis
🔗 Merge multiple splats
🔄 Apply transformations to input splats
🎛️ Filter out Gaussians or spherical harmonic bands
🔀 Reorder splats for improved spatial locality
⚙️ Procedurally generate splats using JavaScript generators
The library is platform-agnostic and can be used in both Node.js and browser environments.
Installation
Install or update to the latest version:
npm install -g @playcanvas/splat-transformFor library usage, install as a dependency:
npm install @playcanvas/splat-transformFor running on a backend with Docker (including GPU/Vulkan setup), see the Docker Backend Guide.
[!TIP] For one-off conversions without installing anything, try SuperSplat Convert — a browser-based frontend to splat-transform. See the Convert page docs for details.
Guides
- Streamed SOG Guide — build a multi-LOD streamed SOG from a single PLY.
- LOD Streaming Guide — load and render streamed SOG output in a PlayCanvas app.
- Collision Mesh Guide — generate voxel/collision data from a splat scene.
- Docker Backend Guide — run splat-transform on a backend (incl. GPU/Vulkan setup).
Format Specifications
| Format | Description |
|---|---|
| PLY | Industry-standard uncompressed format for source, editing and interchange |
| SOG | Super-compressed format for web delivery (meta.json + WebP textures, bundled or unbundled) |
| Streamed SOG | Multi-LOD chunked SOG for streaming very large scenes (lod-meta.json) |
| Voxel | Sparse voxel octree for collision detection (.voxel.json / .voxel.bin) |
CLI Usage
splat-transform [GLOBAL] input [ACTIONS] ... output [ACTIONS]Key points:
- Input files become the working set; ACTIONS are applied in order
- The last file is the output; actions after it modify the final result
- Use
nullas output to discard file output
Supported Formats
| Format | Input | Output | Description |
|---|---|---|---|
.ply | ✅ | ✅ | Standard PLY format |
.sog | ✅ | ✅ | Bundled super-compressed format (recommended) |
meta.json | ✅ | ✅ | Unbundled super-compressed format (accompanied by .webp textures) |
.compressed.ply | ✅ | ✅ | Compressed PLY format (auto-detected and decompressed on read) |
.spz | ✅ | ✅ | Compressed splat format (Niantic format, v2–4) |
.lcc | ✅ | ❌ | LCC file format (XGRIDS) |
.lcc2 | ✅ | ❌ | LCC2 file format (XGRIDS, octree) |
.ksplat | ✅ | ❌ | Compressed splat format (mkkellogg format) |
.splat | ✅ | ❌ | Compressed splat format (antimatter15 format) |
.mjs | ✅ | ❌ | Generate a scene using an mjs script (Beta) |
.glb | ❌ | ✅ | Binary glTF with KHR_gaussian_splatting extension |
.csv | ❌ | ✅ | Comma-separated values spreadsheet |
.html | ❌ | ✅ | HTML viewer app (single-page or unbundled) based on SOG |
.voxel.json | ❌ | ✅ | Sparse voxel octree for collision detection |
lod-meta.json | ❌ | ✅ | Streamed LOD data stored in SOG chunks |
.webp | ❌ | ✅ | Lossless WebP image rendered from a camera view via GPU rasterizer |
null | ❌ | ✅ | Discard output (useful with --stats for analysis-only runs) |
Actions
Actions execute in the order specified and can be repeated. Any action may appear after any input or output file:
-t, --translate <x,y,z> Translate Gaussians by (x, y, z)
-r, --rotate <x,y,z> Rotate Gaussians by Euler angles (x, y, z), in degrees
-s, --scale <factor> Uniformly scale Gaussians by factor
-H, --filter-harmonics <0|1|2|3> Remove spherical harmonic bands > n
-N, --filter-nan Remove Gaussians with NaN values and most Inf values;
retains +Infinity in opacity and -Infinity in scale_*
-B, --filter-box <x,y,z,X,Y,Z> Remove Gaussians outside box (min, max corners)
-S, --filter-sphere <x,y,z,radius> Remove Gaussians outside sphere (center, radius)
-V, --filter-value <name,cmp,value> Keep Gaussians where <name> <cmp> <value>
cmp ∈ {lt,lte,gt,gte,eq,neq}
opacity, scale_*, f_dc_* use transformed values
(linear opacity 0-1, linear scale, linear color 0-1).
Append _raw for raw PLY values (e.g. opacity_raw).
-d, --decimate <n|n%> Simplify to n Gaussians via merge-based decimation
Use n% to keep a percentage of Gaussians.
Memory-bounded and streaming: scales to scenes of 100M+
Gaussians. Must be the final action, and the output must
be .ply (write a decimated PLY first, then convert in a
second invocation). Deep targets on huge scenes spill
temporary files to --scratch-dir (default: the output
file's directory).
--scratch-dir <path> Directory for decimation spill files
-F, --filter-floaters [size,op,min] Remove Gaussians not contributing to any solid voxel.
Evaluates each Gaussian at occupied voxel centers.
Default: size=0.05, opacity=0.1, min=0.004 (1/255).
Bare flag (no value) uses all defaults.
-C, --filter-cluster [res,op,min] Keep only the connected cluster at --seed-pos.
GPU-voxelizes at coarse resolution (res world units/voxel).
Default: res=1.0, opacity=0.999, min=0.1.
Bare flag (no value) uses all defaults.
-p, --params <key=val,...> Pass parameters to .mjs generator script
-l, --tag-lod <n> Tag the Gaussians with LOD level n (n >= 0, or -1 for environment)
--stats [text|json] Print file info and per-column statistics to stdout. Default: text
--info [text|json] Print structural metadata (per-LOD counts, columns) to stdout. Default: text
-m, --morton-order Reorder Gaussians by Morton code (Z-order curve)General Options
-h, --help Show this help and exit
-v, --version Show version and exit
-q, --quiet Suppress non-error output
--verbose Show debug-level diagnostics
--memory Show peak memory in progress output
--tty Interactive bar rendering (default on a TTY; --no-tty to disable)
-w, --overwrite Overwrite output file if it existsGPU Options
Used by SOG compression and GPU voxelization (--filter-cluster, --filter-floaters, .voxel.json output).
--list-gpus List available GPU adapters and exit
-g, --gpu <n|cpu> Device for GPU operations: GPU adapter index | 'cpu'
('cpu' disables GPU and is incompatible with
GPU-only features like --filter-cluster)SOG Compression Options
Apply when writing .sog, meta.json, lod-meta.json, or .html outputs.
-i, --sh-iterations <n> Iterations for SH compression (more=better). Default: 10
--max-workers <n> Worker threads for SOG encoding (0 = inline/serial). Default: 4SPZ Output Options
Apply when writing .spz outputs.
--spz-version <3|4> The SPZ format version to write. Default: 4HTML Viewer Output Options
Apply when writing .html outputs.
--viewer-settings <settings.json> HTML viewer settings JSON file
--unbundled Generate unbundled HTML viewer with separate files[!NOTE] See the SuperSplat Viewer Settings Schema for details on how to pass data to the
--viewer-settingsoption.
LCC / LCC2 Input Options
Apply when reading .lcc and .lcc2 files.
-L, --select-lod <n,n,...> Comma-separated LOD levels to read from LCC / LCC2 inputLOD Output Options
Apply when writing lod-meta.json (multi-LOD streaming SOG bundle).
--lod-chunk-count <n> Approximate number of Gaussians per LOD chunk in K. Default: 512
--lod-chunk-extent <n> Approximate size of an LOD chunk in world units (m). Default: 16See Generating Streamed SOG for an end-to-end walkthrough.
Voxel Output Options
Apply when writing .voxel.json (sparse voxel octree for collision detection). See the Collision Mesh Guide for a deep dive on each step and tuning.
--voxel-size <n> Voxel size for .voxel.json. Default: 0.05
--voxel-opacity <n> Voxel opacity threshold for .voxel.json. Default: 0.1
--voxel-external-fill [size] Seal exterior voxels via boundary flood fill (interior scenes).
[size] (world units) is the dilation distance applied
before the flood fill to bridge small wall gaps.
--seed-pos is used to verify the volume is enclosed at
the seed; the fill is skipped if the seed is reachable
from outside.
Default size: 1.6
--voxel-floor-fill [size] Fill each column upward from bottom until hitting solid (exterior scenes).
Optional size (world units): only patch XZ areas surrounded by floor
within 2*size; large empty exterior areas are left alone.
Default size: 1.6
--voxel-carve [h,r] Carve navigable space using capsule flood fill from seed.
Default: height=1.6, radius=0.2
--seed-pos <x,y,z> Seed position for voxel fill/carve and --filter-cluster.
Default: 0,0,0
--collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default: smoothImage Output Options
Apply when writing .webp (lossless WebP rendered via GPU rasterizer).
--projection <pinhole|equirect> Camera projection. Default: pinhole.
equirect = 360°×180° panorama from --camera-pos; --camera-fov must be
omitted; --resolution must be 2:1 (default 2048x1024).
--camera-pos <x,y,z> Camera position in world space. Default: 2,1,-2
--camera-target <x,y,z> Camera target point. Default: 0,0,0
--camera-up <x,y,z> World up vector. Default: 0,1,0
--camera-fov <degrees> Vertical field of view in degrees. Default: 60. Rejected with --projection equirect.
--resolution <WxH> Output resolution, e.g. 1920x1080. Default: 1280x720 (pinhole) or 2048x1024 (equirect)
--camera-near <n> Near clip distance. Default: 0.2 (matches reference 3DGS)
--background <r,g,b[,a]> Background color in [0,1]. Default: 0,0,0,1
--f-stop <N> Aperture as a photographic f-stop (e.g. 2.8, 5.6, 11). Enables defocus blur;
smaller = more blur. Pinhole only. Default: disabled (no defocus).
--focus-distance <n> Camera-space Z of the focus plane (world units). Default: distance to --camera-target.
Pinhole only; only meaningful with --f-stop.
--sensor-size <n> Vertical sensor height in world units. Gives --f-stop a physical meaning.
Default: 0.024 (35mm full-frame, world units = meters). Scale to your world:
world unit = decimeter → 0.24, world unit = millimeter → 24.
--camera-pos-end <x,y,z> End camera position. When set, enables camera motion blur: the renderer
averages sub-frames with the camera interpolated from --camera-pos (shutter open)
to --camera-pos-end (shutter close). Default: disabled (no motion blur).
--camera-target-end <x,y,z> End camera target. Default: same as --camera-target. Only with --camera-pos-end.
--camera-up-end <x,y,z> End up vector. Default: same as --camera-up. Only with --camera-pos-end.
--shutter <0..1> Fraction of the start→end segment integrated, centered on the midpoint
(1.0 = full motion; 0.5 = 180° shutter). Default: 1. Only with --camera-pos-end.
--motion-samples <n> Sub-frames to accumulate for motion blur. Cost is N× a single render.
Default: 16. Only with --camera-pos-end.Examples
Basic Operations
# Simple format conversion
splat-transform input.ply output.csv
# Convert from .splat format
splat-transform input.splat output.ply
# Convert from .ksplat format
splat-transform input.ksplat output.ply
# Convert to compressed PLY
splat-transform input.ply output.compressed.ply
# Uncompress a compressed PLY back to standard PLY
# (compressed .ply is detected automatically on read)
splat-transform input.compressed.ply output.ply
# Convert to SOG bundled format
splat-transform input.ply output.sog
# Convert to SOG unbundled format
splat-transform input.ply output/meta.json
# Convert from SOG (bundled) back to PLY
splat-transform scene.sog restored.ply
# Convert from SOG (unbundled folder) back to PLY
splat-transform output/meta.json restored.ply
# Convert to standalone HTML viewer (bundled, single file)
splat-transform input.ply output.html
# Convert to unbundled HTML viewer (separate CSS, JS, and SOG files)
splat-transform --unbundled input.ply output.html
# Convert to HTML viewer with custom settings
splat-transform --viewer-settings settings.json input.ply output.htmlTransformations
# Scale and translate
splat-transform bunny.ply -s 0.5 -t 0,0,10 bunny_scaled.ply
# Rotate by 90 degrees around Y axis
splat-transform input.ply -r 0,90,0 output.ply
# Chain multiple transformations
splat-transform input.ply -s 2 -t 1,0,0 -r 0,0,45 output.plyFiltering
# Remove entries containing NaN and Inf
splat-transform input.ply --filter-nan output.ply
# Filter by opacity values (keep only splats with opacity > 0.5)
splat-transform input.ply -V opacity,gt,0.5 output.ply
# Strip spherical harmonic bands higher than 2
splat-transform input.ply --filter-harmonics 2 output.ply
# Simplify to 50000 splats via progressive pairwise merging
splat-transform input.ply --decimate 50000 output.ply
# Simplify to 25% of original splat count
splat-transform input.ply -d 25% output.plyAdvanced Usage
# Combine multiple files with different transforms
splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.compressed.ply
# Apply final transformations to combined result
splat-transform input1.ply input2.ply output.ply -t 0,0,10 -s 0.5Statistics
Generate per-column statistics for data analysis or test validation:
# Print stats, then write output
splat-transform input.ply --stats output.ply
# Print stats without writing a file (discard output)
splat-transform input.ply --stats null
# Print stats as JSON for scripting
splat-transform input.ply --stats json null
# Print stats before and after a transform
splat-transform input.ply --stats -s 0.5 --stats output.plyThe output starts with the file info block (including the gaussian verdict — false for a readable container that isn't splat data, such as a plain point-cloud PLY), followed by min, max, median, mean, stdDev, nanCount, infCount and a histogram for each column, one table per LOD. The JSON form is the same info fields plus a columnar per-LOD stats array. The stats are computed in a single streaming pass; the median is approximated from a 1024-bin histogram (error within ~1/1000 of the column's range), all other fields are exact.
Generators (Beta)
Generator scripts can be used to synthesize gaussian splat data. See gen-grid.mjs for an example.
splat-transform gen-grid.mjs -p width=10,height=10,scale=10,color=0.1 scenes/grid.ply -wVoxel Format
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 --collision-mesh to also emit a .collision.glb mesh derived from the voxel grid.
For a step-by-step walkthrough of each option (with illustrations), see the Collision Mesh Guide.
Recommended pipeline
splat-transform input.ply \
--filter-cluster --seed-pos x,y,z \
[--voxel-external-fill | --voxel-floor-fill] [--voxel-carve] \
[--collision-mesh [smooth|faces]] \
output.voxel.json--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.
Interior scenes (rooms, indoor scans)
Use --voxel-external-fill to seal the void around the room interior, then --voxel-carve to hollow out the navigable space:
splat-transform room.ply \
--filter-cluster --seed-pos 0,1,0 \
--voxel-external-fill --voxel-carve \
--collision-mesh room.voxel.jsonExterior scenes (outdoor objects, terrain)
Use --voxel-floor-fill to fill the ground beneath surfaces, optionally followed by --voxel-carve:
splat-transform terrain.ply \
--filter-cluster --seed-pos 0,0,0 \
--voxel-floor-fill \
--collision-mesh terrain.voxel.jsonOther examples
# Voxelize with custom resolution and opacity threshold
splat-transform --voxel-size 0.1 --voxel-opacity 0.3 input.ply output.voxel.json
# Custom carve capsule (height, radius)
splat-transform --seed-pos 1,0,0 --voxel-carve 2.0,0.3 input.ply output.voxel.json
# Watertight voxel-face collision mesh
splat-transform --collision-mesh faces input.ply output.voxel.jsonImage Rendering
Render a splat scene to a lossless WebP image from a given camera view. Rendering runs on the GPU.
# Default 1280x720 render
splat-transform input.ply view.webp
# Custom camera and resolution
splat-transform input.ply view.webp \
--camera-pos 2,1,-2 --camera-target 0,0,0 \
--camera-fov 50 --resolution 1920x1080
# Transparent background
splat-transform input.ply view.webp --background 0,0,0,0
# Defocus blur (focus on camera-target, f/2.8 aperture)
splat-transform input.ply view.webp --f-stop 2.8
# Defocus with explicit focus distance and a smaller world scale
splat-transform input.ply view.webp \
--f-stop 2.8 --focus-distance 3 --sensor-size 0.1
# 360° equirectangular panorama from camera position
splat-transform input.ply pano.webp \
--projection equirect --camera-pos 0,1,0 --camera-target 0,1,1
# Camera motion blur (dolly from start to end pose over the shutter)
splat-transform input.ply view.webp \
--camera-pos 2,1,-2 --camera-pos-end 3,1,-2 \
--motion-samples 16 --shutter 1Device Selection for SOG Compression
When compressing to SOG format, you can control which device (GPU or CPU) performs the compression:
# List available GPU adapters
splat-transform --list-gpus
# Let WebGPU automatically choose the best GPU (default behavior)
splat-transform input.ply output.sog
# Explicitly select a GPU adapter by index
splat-transform -g 0 input.ply output.sog # Use first listed adapter
splat-transform -g 1 input.ply output.sog # Use second listed adapter
# Use CPU for compression instead (much slower but always available)
splat-transform -g cpu input.ply output.sog[!NOTE] When
-gis not specified, WebGPU automatically selects the best available GPU. Use--list-gpusto list available adapters with their indices and names. The order and availability of adapters depends on your system and GPU drivers. Use-g <index>to select a specific adapter, or-g cputo force CPU computation.
[!WARNING] CPU compression can be significantly slower than GPU compression (often 5-10x slower). Use CPU mode only if GPU drivers are unavailable or problematic.
Getting Help
# Show version
splat-transform --version
# Show help
splat-transform --helpLibrary Usage
SplatTransform exposes a programmatic API for reading, processing, and writing Gaussian splat data. Scenes flow through lazy, chunked ChunkSources, so resident memory is bounded by chunk size rather than scene size — the same pipeline the CLI uses to process scenes of hundreds of millions of gaussians.
Basic Import
import {
readFile,
writeSource,
getInputFormat,
getOutputFormat,
createChunkDataPool,
processSourceBridged
} from '@playcanvas/splat-transform';Key Exports
Chunk-source pipeline (the primary API):
| Export | Description |
|---|---|
readFile | Read a splat file as lazy ChunkSources |
readFileInfo | Header-only structural metadata (validate/inspect without decoding) |
getInputFormat | Detect input format from filename |
getOutputFormat | Detect output format from filename |
ChunkSource | The streaming contract: chunked/gathered reads over one scene |
createChunkDataPool | Pooled read buffers shared across a pipeline |
processSource, processSourceBridged | Apply a sequence of processing actions to a source |
selectLod, stackLods, concatSource, bakeTransform | Structural combinators (lazy views) |
decimateSource | Chunk-native, memory-bounded decimation to an exact target count |
writeSource | Stream a source to any single-scene output format |
writeLodSource | Write streamed SOG (lod-meta.json + chunked units) from a multi-LOD source |
computeStats | Streaming per-LOD, per-column statistics for a source or table |
DataTable compat (secondary; every entry materializes the whole scene in memory):
| Export | Description |
|---|---|
DataTable, Column | Legacy whole-scene table |
combine | Merge multiple DataTables into one |
processDataTable | Apply processing actions to a DataTable |
dataTableToChunkSource, materializeToDataTable | Bridges between the DataTable and chunk-source worlds |
writeFile | Write a DataTable to any output format |
writeVoxel | Write sparse voxel octree files |
writeImage | Render a camera view to a lossless WebP image (requires GPU) |
File System Abstractions
The library uses abstract file system interfaces for maximum flexibility:
Reading:
UrlReadFileSystem- Read from URLs (browser/Node.js)MemoryReadFileSystem- Read from in-memory buffersZipReadFileSystem- Read from ZIP archives
Writing:
MemoryFileSystem- Write to in-memory buffersZipFileSystem- Write to ZIP archives
Example: Reading and Processing
import { Vec3 } from 'playcanvas';
import {
readFile,
writeSource,
getInputFormat,
getOutputFormat,
createChunkDataPool,
processSourceBridged,
UrlReadFileSystem,
MemoryFileSystem
} from '@playcanvas/splat-transform';
// Read a PLY file from a URL as a lazy, chunked source
const fileSystem = new UrlReadFileSystem('https://example.com/');
const [source] = await readFile({
filename: 'scene.ply',
inputFormat: getInputFormat('scene.ply'),
fileSystem
});
// Apply actions: transforms compose lazily, filters stream chunk-by-chunk
const pool = createChunkDataPool();
const processed = await processSourceBridged(source, [
{ kind: 'scale', value: 0.5 },
{ kind: 'translate', value: new Vec3(0, 1, 0) },
{ kind: 'filterNaN' }
], pool);
// Stream the result to an in-memory PLY
const memFs = new MemoryFileSystem();
await writeSource({
filename: 'output.ply',
outputFormat: getOutputFormat('output.ply', {}),
source: processed,
pool,
options: {}
}, memFs);
await processed.close();
// Get the output data
const outputBuffer = memFs.results.get('output.ply');Consumers still on the DataTable API can bridge in either direction with materializeToDataTable(source, pool) and dataTableToChunkSource(dataTable) — both materialize the full scene, so prefer staying on sources for large inputs.
Processing Actions
processSource / processSourceBridged (and the compat processDataTable) accept an array of actions:
type ProcessAction =
| { kind: 'translate'; value: Vec3 }
| { kind: 'rotate'; value: Vec3 } // Euler angles in degrees
| { kind: 'scale'; value: number }
| { kind: 'filterNaN' }
| { kind: 'filterByValue'; columnName: string; comparator: 'lt'|'lte'|'gt'|'gte'|'eq'|'neq'; value: number }
| { kind: 'filterBands'; value: 0|1|2|3 }
| { kind: 'filterBox'; min: Vec3; max: Vec3 }
| { kind: 'filterSphere'; center: Vec3; radius: number }
| { kind: 'filterFloaters'; voxelResolution?: number; opacityCutoff?: number; minContribution?: number } // GPU
| { kind: 'filterCluster'; voxelResolution?: number; seed?: Vec3; opacityCutoff?: number; minContribution?: number } // GPU
| { kind: 'decimate'; count: number | null; percent: number | null }
| { kind: 'param'; name: string; value: string }
| { kind: 'stats'; format?: 'text' | 'json' }
| { kind: 'info'; format?: 'text' | 'json' }
| { kind: 'mortonOrder' };[!NOTE]
filterFloatersandfilterClusterrequire a GPU device — passcreateDevicevia theProcessOptionsargument.processSourcestreams and throws on actions that need the DataTable bridge (decimate,mortonOrder, the GPU voxel filters);processSourceBridgedhandles every action, materializing only those runs.
Custom Logging
Configure the logger for your environment:
import { logger, TextRenderer } from '@playcanvas/splat-transform';
// Route status output (scopes, progress bars, messages) to stderr and
// pipeable output (e.g. JSON stats) to stdout
logger.setRenderer(new TextRenderer({
write: process.stderr.write.bind(process.stderr),
output: process.stdout.write.bind(process.stdout)
}));
logger.setVerbosity('quiet'); // 'quiet' | 'normal' | 'verbose'