Skip to main content

SplatTransform

SplatTransform is an open source library and CLI tool for converting and editing Gaussian splats. Whether you need to convert between formats, apply transformations, filter data, generate collision volumes, or analyze splat statistics, SplatTransform gives developers precise control over their Gaussian splat workflows. The library is platform-agnostic and runs in both Node.js and browser environments.

Open Source
Prefer a web UI?

The SuperSplat Convert page at superspl.at/convert is the web frontend to splat-transform. It runs the same conversions and transforms in your browser via WebAssembly — no installation required. Use the web UI for one-off conversions and the CLI below for scripted or batch workflows.

Why Use SplatTransform?

SplatTransform solves the problems developers face when working with Gaussian splats:

🔄 Broad Format Support — read PLY, Compressed PLY, SOG, SPZ, SPLAT, KSPLAT and LCC; write PLY, Compressed PLY, SOG, SPZ, GLB, CSV, HTML Viewer, LOD, Voxel and WebP image
🛠️ Powerful Transformations — translate, rotate, and scale your splats with precision
🧹 Smart Filtering — strip NaN/Inf, filter by value, box, sphere, harmonic band, or floater contribution, and keep only the connected cluster around a seed point
📐 Decimation & Reordering — simplify via progressive pairwise merging, and reorder by Morton code for spatial locality
🧱 Collision Generation — voxelize a scene into a sparse octree and emit a .collision.glb mesh ready for runtime physics
🖼️ Image Rendering — render a scene to a lossless WebP from a configurable camera view
📊 Statistical Analysis — generate per-column statistics for data analysis or test validation
📦 Scene Merging — combine multiple splat files into a single merged scene
⚙️ Generators — procedurally synthesize splat data with JavaScript generator scripts
🆓 Open Source — MIT licensed and freely available on GitHub

Installation

Install or update to the latest version:

npm install -g @playcanvas/splat-transform

For library usage, install as a dependency:

npm install @playcanvas/splat-transform

Verify your CLI installation:

splat-transform --version

For running on a backend with Docker (including GPU/Vulkan setup), see the Docker Backend guide.

CLI Usage

The general syntax for SplatTransform is:

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 null as the output to discard file output (useful with --summary for analysis-only runs)

Supported Formats

SplatTransform detects file format from the file extension:

FormatInputOutputDescription
.plyStandard PLY format
.sogBundled super-compressed format (recommended)
meta.jsonUnbundled super-compressed format (accompanied by .webp textures). Output filename must be meta.json
.compressed.plyCompressed PLY format (auto-detected and decompressed on read)
.spzCompressed splat format (Niantic format, v2–4)
.lccLCC file format (XGRIDS)
.ksplatCompressed splat format (mkkellogg format)
.splatCompressed splat format (antimatter15 format)
.mjsGenerate a scene using an mjs script (Beta)
.glbBinary glTF with KHR_gaussian_splatting extension
.csvComma-separated values spreadsheet
.htmlHTML viewer app (single-page or unbundled) based on SOG
.voxel.jsonSparse voxel octree for collision detection. See the Collision Mesh guide. Output filename must end with .voxel.json (the prefix is up to you, e.g. room.voxel.json)
lod-meta.jsonMulti-level-of-detail SOG bundle (accompanied by per-LOD .sog chunks). Output filename must be lod-meta.json
.webpLossless WebP image rendered from a camera view via GPU rasterizer
nullDiscard output (useful with --summary 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).
-F, --decimate <n|n%> Simplify to n Gaussians via progressive pairwise merging.
Use n% to keep a percentage of Gaussians.
-G, --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.
-D, --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, --lod <n> Tag the Gaussians with LOD level n (n >= 0)
-m, --summary Print per-column statistics to stdout
-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
--mem Show memory usage in progress output
--tty Interactive bar rendering (default on a TTY; --no-tty to disable)
-w, --overwrite Overwrite output file if it exists

GPU Options

Used by SOG compression and GPU voxelization (--filter-cluster, --filter-floaters, .voxel.json output).

-L, --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, --iterations <n> Iterations for SH compression (more=better). Default: 10

SPZ Output Options

Apply when writing .spz outputs.

--spz-version <3|4> The SPZ format version to write. Default: 4

HTML Viewer Output Options

Apply when writing .html outputs.

-E, --viewer-settings <settings.json> HTML viewer settings JSON file
-U, --unbundled Generate unbundled HTML viewer with separate files
note

See the SuperSplat Viewer Settings Schema for details on how to pass data to the -E option.

LCC Input Options

Apply when reading .lcc files.

-O, --lod-select <n,n,...> Comma-separated LOD levels to read from LCC input

LOD Output Options

Apply when writing lod-meta.json (multi-LOD streaming SOG bundle).

-C, --lod-chunk-count <n> Approximate number of Gaussians per LOD chunk in K. Default: 512
-X, --lod-chunk-extent <n> Approximate size of an LOD chunk in world units (m). Default: 16

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-params [size,opacity] Voxel size and opacity threshold. Default: 0.05,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
-K, --collision-mesh [smooth|faces] Generate collision mesh (.collision.glb). Default: smooth

Image Output Options

Apply when writing .webp (lossless WebP rendered via GPU rasterizer).

--camera <x,y,z> Camera position in world space. Default: 2,1,-2
--look-at <x,y,z> Camera target point. Default: 0,0,0
--up <x,y,z> World up vector. Default: 0,1,0
--fov <degrees> Vertical field of view in degrees. Default: 60
--resolution <WxH> Output resolution, e.g. 1920x1080. Default: 1280x720
--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

Examples

Format Conversion

# 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 -U input.ply output.html

# Convert to HTML viewer with custom settings
splat-transform -E settings.json input.ply output.html

# Export to CSV for spreadsheet analysis
splat-transform scene.ply data.csv

Transformations

# 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.ply

Filtering

# 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 -F 25% output.ply

Scene Merging

# 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.5

Statistical Summary

Generate per-column statistics for data analysis or test validation:

# Print summary, then write output
splat-transform input.ply --summary output.ply

# Print summary without writing a file (discard output)
splat-transform input.ply -m null

# Print summary before and after a transform
splat-transform input.ply --summary -s 0.5 --summary output.ply

The summary includes min, max, median, mean, stdDev, nanCount and infCount for each column in the data.

Generators (Beta)

Generator scripts synthesize Gaussian splat data procedurally. See the example generator scripts in the GitHub repository for more.

splat-transform gen-grid.mjs -p width=10,height=10,scale=10,color=0.1 scenes/grid.ply -w

Voxel Pipeline (Collision)

The voxel format stores a sparse voxel octree for collision detection (.voxel.json + .voxel.bin). Pass -K to also emit a .collision.glb mesh derived from the voxel grid. The recommended pipeline:

splat-transform input.ply \
--filter-cluster --seed-pos x,y,z \
[--voxel-external-fill | --voxel-floor-fill] [--voxel-carve] \
[-K [smooth|faces]] \
output.voxel.json

For a step-by-step walkthrough of each option (with illustrations) and full interior/exterior recipes, see the Collision Mesh guide.

Image 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 2,1,-2 --look-at 0,0,0 \
--fov 50 --resolution 1920x1080

# Transparent background
splat-transform input.ply view.webp --background 0,0,0,0

Device 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 -g is not specified, WebGPU automatically selects the best available GPU. Use -L to list available adapters with their indices and names. The order and availability of adapters depend on your system and GPU drivers. Use -g <index> to select a specific adapter, or -g cpu to 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.

Common Workflows

Production Optimization Pipeline

# Clean, limit spherical harmonic bands, and apply a scale for production
splat-transform raw_capture.ply \
--filter-nan \
--filter-harmonics 2 \
-s 0.8 \
production/capture.sog

Format Migration

# Convert existing KSPLAT assets to PlayCanvas SOG
for file in *.ksplat; do
splat-transform "$file" "${file%.ksplat}.sog"
done

Quality Analysis

# Export for quality analysis in a spreadsheet
splat-transform scene.ply \
--filter-nan \
-V opacity,gt,0.05 \
quality_analysis.csv

Multi-Scene Composition

# Combine multiple scenes with precise positioning
splat-transform \
environment.ply -t 0,0,0 \
character.ply -t 2,0,1 -r 0,180,0 \
props.ply -t -3,0,2 -s 1.2 \
complete_scene.ply

Generating LOD Format

The LOD (Level of Detail) format enables efficient streaming and rendering of large Gaussian splat scenes. SplatTransform builds an optimized streaming format with an octree structure for progressive download from a set of LOD levels, where each level has progressively fewer Gaussians (LOD 0 = highest detail, higher numbers = lower detail).

You can obtain those LOD levels in two ways:

  • Supply your own LOD files — provide a separate splat file for each level, for example produced during training or exported from another tool.
  • Generate them by decimating a single source — use --decimate to create the lower-detail levels from one high-quality input, so you don't have to author each level separately.
Output Filename Requirements

The output filename determines the format. These are not arbitrary names:

  • lod-meta.json — generates LOD streaming format (multiple SOG chunks with an octree structure for progressive loading)
  • meta.json — generates unbundled SOG format (a single SOG file, no streaming)

The output filename must be exactly lod-meta.json or meta.json — only the directory path before it can vary. For example: output/lod-meta.json, my-scene/lod-meta.json.

# Generate LOD streaming format from multiple input files
# Each input file represents a different detail level (LOD 0 is highest quality)
splat-transform \
lod0.ply -l 0 \
lod1.ply -l 1 \
lod2.ply -l 2 \
lod3.ply -l 3 \
output/lod-meta.json \
--filter-nan \
--filter-harmonics 0

# Generate the lower-detail levels by decimating a single high-quality source
# Step 1: create progressively smaller versions of the source splat
splat-transform source.ply -F 50% lod1.ply
splat-transform source.ply -F 25% lod2.ply
splat-transform source.ply -F 10% lod3.ply
# Step 2: bundle the full-detail source and the decimated levels into a streaming LOD format
splat-transform \
source.ply -l 0 \
lod1.ply -l 1 \
lod2.ply -l 2 \
lod3.ply -l 3 \
output/lod-meta.json \
--filter-nan

# Generate LOD with custom chunk settings for better performance
splat-transform \
-C 1024 \
-X 32 \
lod0.ply -l 0 \
lod1.ply -l 1 \
lod2.ply -l 2 \
output/lod-meta.json \
--filter-nan

# For very large scenes, increase Node.js memory allocation
node --max-old-space-size=32000 node_modules/.bin/splat-transform \
lod0.ply -l 0 \
lod1.ply -l 1 \
lod2.ply -l 2 \
lod3.ply -l 3 \
output/lod-meta.json \
--filter-nan \
--filter-harmonics 0

# Generate LOD streaming format directly from an LCC file
# (LCC files already contain multiple LOD levels)
splat-transform scene.lcc output/lod-meta.json

Tips:

  • Use --decimate (-F) to generate lower LOD levels from a single high-quality source, instead of authoring each level separately
  • Use --filter-nan to remove invalid Gaussians before processing
  • Use --filter-harmonics 0 to reduce file size if colour detail is less critical
  • Use -C to control the number of generated SOG files containing splats
  • Use -X to control the size of each node. Increase for very large scenes to avoid generating a huge number of nodes to manage
  • For very large scenes, use Node's --max-old-space-size flag to give it more memory

Getting Help

# General help
splat-transform --help

# Get version information
splat-transform --version

For issues, feature requests, or contributions, visit the GitHub repository. The project welcomes bug reports and pull requests from the community.

Library Usage

SplatTransform exposes a programmatic API for reading, processing, and writing Gaussian splat data.

Basic Import

import {
readFile,
writeFile,
getInputFormat,
getOutputFormat,
DataTable,
processDataTable
} from '@playcanvas/splat-transform';

Key Exports

ExportDescription
readFileRead splat data from various formats
writeFileWrite splat data to various formats
getInputFormatDetect input format from filename
getOutputFormatDetect output format from filename
DataTable, ColumnCore data structures for splat data
combineMerge multiple DataTables into one
convertToSpaceConvert a DataTable between coordinate spaces
processDataTableApply a sequence of processing actions
computeSummaryGenerate statistical summary of data
sortMortonOrderSort indices by Morton code for spatial locality
sortByVisibilitySort indices by visibility score for filtering
writeVoxelWrite sparse voxel octree files
writeImageRender a camera view to a lossless WebP image (requires GPU)
renderSplatsLower-level renderer returning the raw RGBA byte buffer

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 buffers
  • ZipReadFileSystem - Read from ZIP archives

Writing:

  • MemoryFileSystem - Write to in-memory buffers
  • ZipFileSystem - Write to ZIP archives

Example: Reading and Processing

import { Vec3 } from 'playcanvas';
import {
readFile,
writeFile,
getInputFormat,
getOutputFormat,
processDataTable,
UrlReadFileSystem,
MemoryFileSystem
} from '@playcanvas/splat-transform';

// Read a PLY file from URL
const fileSystem = new UrlReadFileSystem();
const inputFormat = getInputFormat('scene.ply');

const dataTables = await readFile({
filename: 'https://example.com/scene.ply',
inputFormat,
options: { iterations: 10 },
params: [],
fileSystem
});

// Apply transformations
const processed = processDataTable(dataTables[0], [
{ kind: 'scale', value: 0.5 },
{ kind: 'translate', value: new Vec3(0, 1, 0) },
{ kind: 'filterNaN' }
]);

// Write to in-memory buffer
const memFs = new MemoryFileSystem();
const outputFormat = getOutputFormat('output.ply', {});

await writeFile({
filename: 'output.ply',
outputFormat,
dataTable: processed,
options: {}
}, memFs);

// Get the output data
const outputBuffer = memFs.files.get('output.ply');

Processing Actions

The processDataTable function accepts 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: Comparator; 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: 'lod'; value: number }
| { kind: 'summary' }
| { kind: 'mortonOrder' };

type Comparator = 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'neq';
note

filterFloaters and filterCluster require a GPU device — pass createDevice via the ProcessOptions argument to processDataTable.

Custom Logging

Configure the logger for your environment:

import { logger } from '@playcanvas/splat-transform';

logger.setLogger({
log: console.log,
warn: console.warn,
error: console.error,
debug: console.debug,
progress: (text) => process.stdout.write(text),
output: console.log
});

logger.setQuiet(true); // Suppress non-error output