A local desktop GUI for @playcanvas/splat-transform:
Gaussian-splat format conversion, SOG bundling, streamed-LOD baking and
collision-mesh generation, with a PlayCanvas 3D viewer —
all in a dockable, Unity/Unreal-style tab editor you can rearrange and save per
workspace.
Built with PlayCanvas 2.21.3 · @playcanvas/splat-transform 3.2.0
The dockable editor: panels and the 3D viewport are tabs you can move, resize, float, close, and reopen from the Window menu — the layout is saved per workspace. Above: the Acropolis scan; below: HOTP.
📚 Documentation site · 📖 User Guide (illustrated, every feature) · 🏗️ Architecture (processes, modules, job pipeline) · ⚙️ Automation (how the app keeps itself current) · ⬇️ Download the latest release
- Features
- Download & install
- Getting started
- Usage
- Analyze & procedural generators
- Render to image (WebP) & output options
- Edit: measure-to-scale & set origin
- How it works
- Notes & caveats
- HTTP API
- Built with
- Contributing
- AI-assisted development
- License
- Support
- Export splats between formats —
.ply,.compressed.ply,.sog(bundled and unbundled),.spz, and streamed LOD SOG for large scenes — with spherical-harmonic compression controls. - Collision — voxelize a splat and emit a watertight triangle mesh
(
.collision.glb) with indoor / outdoor / object presets. - 3D viewer — PlayCanvas renderer with fly/orbit cameras, a scene hierarchy, movable gizmos, collision-overlay styles (X-ray / hidden-line / solid+edges), voxel-octree display, bounds, and an applyable skybox.
- Edit from the viewport — measure-to-scale and set-origin tools that drive splat-transform visually; apply a scale directly to a selected splat.
- Analyze — gaussian count, extent, NaN/Inf flags and per-column histograms
as a persistent card. Procedural
.mjsgenerators are first-class inputs. - Render to WebP — rasterize a splat to a lossless image (pinhole or 360° equirect) with depth-of-field and motion-blur controls.
- Dockable editor — every panel and the viewport is a tab you can move, float, close, and reopen; layouts are saved per workspace.
- Self-updating desktop app — ships as a standalone Windows app (Electron) that checks GitHub Releases on launch, then downloads and installs new versions in the background (you choose when to restart).
Grab the latest Windows build from the
Releases page —
no Node install, terminal, or npm run dev required. Each release lists the
PlayCanvas and splat-transform versions it was built against, plus a changelog of
what changed.
Splat-Studio-Setup-<version>.exe— NSIS installer (per-user, lets you choose the install dir, creates Start-menu/desktop shortcuts).SplatStudio-<version>-portable.exe— single self-contained exe; run it from anywhere, nothing is installed.
On first run the workspace defaults to Documents\Splat Studio; File → Change
Workspace Folder… points it at any folder of project subfolders (the choice is
remembered). File → Open Workspace in Explorer reveals it on disk.
Automatic updates: on launch (and every few hours), the app checks GitHub Releases. If a newer version exists it offers to download it — the download runs in the background (progress shows on the taskbar icon) and, when ready, it offers to restart and install (or installs automatically the next time you quit). Check any time via Help → Check for Updates. (The NSIS installer self-updates; the portable exe does not.)
Want to run from source — to develop, or to build the app yourself?
Prerequisites: Node.js 22+ and npm. Windows is required to build the desktop app; the dev server itself runs anywhere. A WebGPU-capable GPU unlocks voxelization, collision, and GPU renders — SOG compression falls back to CPU without one.
Clone, install, and launch with a synthetic scan so you see it working end to end:
git clone https://github.com/CodeByKeegan/splat-studio.git
cd splat-studio
npm install
npm run demo # writes workspace/demo-room.ply — a synthetic room scan
npm run dev # API on :5174, UI on http://localhost:5173Open http://localhost:5173. demo-room.ply is already listed in the Files panel —
click its 👁 eye to show it in the 3D viewer, then run your first job with
Convert → SOG (bundled). Output streams into the Job panel and the new file flashes
in the list.
The workspace defaults to ./workspace, where each top-level subfolder is a project; point
it elsewhere with the SPLAT_WORKSPACE environment variable.
npm run dist:win # build + package the NSIS installer and portable exe into release/
npm run pack:dir # faster: unpacked release/win-unpacked/Splat Studio.exe, no installer(npm run make-icon regenerates build/icon.ico — committed, only needed if the icon changes.)
npm run typecheck # tsc --noEmit
npm test # tests/e2e.mjs — black-box regression suite
SKIP_GPU=1 npm test # skip the GPU-only checks (collision, WebP render)The workspace defaults to ./workspace, but each top-level subfolder of the
workspace is a project (e.g. HOTP/, Acropolis/). Point the workspace at a
folder of asset folders with the SPLAT_WORKSPACE env var (set in dev.cmd).
Within a project, sources can be nested (e.g. RAW SOG/scene_10mil.sog); the
file list surfaces every source individually but collapses output bundles
(streamed LOD, unbundled SOG) to their lod-meta.json / meta.json entry
point. Generated outputs always land at the project root.
Then in the browser:
-
Project picker (header) — switch projects (filters everything below and clears the viewport) or + New to create one. Project scoping is per-request, so two windows can sit on different projects at once.
-
Files — drop a
.ply/.sog/.spz/.splat/.ksplatfile anywhere in the window (or use the generateddemo-room.ply). Click a file's 👁 eye to show or hide it (several splats can be shown at once); ✕ asks twice before deleting. Uploads show a progress bar; job outputs flash blue in the list. Panel headers collapse/expand on click (state persists), the Jobs panel stays pinned to the bottom of the sidebar, and all form values survive a reload. -
Export — pick an output format. SOG bundling is filename-driven in splat-transform: SOG (bundled) writes
name.sog, SOG (unbundled) writesname-sog/meta.json+ WebP textures. SH compression iterations apply to both. Streamed LOD SOG writesname-lod/lod-meta.jsonplus one unbundled-SOG chunk folder per LOD level, spatially chunked (-CK-gaussians per chunk,-Xmeters per chunk). The viewer streams chunks by camera distance — this is the format for big scenes in the PlayCanvas engine. Two source modes:- Decimate input automatically — the input is read once per level,
GPU-decimated to
keep%^levelof the original and tagged-l <n>. - Combine existing files as levels — for pre-authored detail chains (e.g. exports at 20M/10M/4M/2M gaussians): the Input is LOD 0 and each added row is the next, lighter level; no decimation is performed.
- Decimate input automatically — the input is read once per level,
GPU-decimated to
-
Collision — voxelizes the splat (
name.voxel.json/.bin) and emits a triangle meshname.collision.glb(-K smooth|faces). Presets:- Indoor —
--voxel-external-fillseals the room from outside,--voxel-carvere-opens the walkable interior from the seed. - Outdoor —
--voxel-floor-fillcloses holes in terrain. - Object — plain voxelization.
The seed position is in splat-transform's voxel space (Y-up, but rotated 180° about Y relative to the viewer — splat-transform maps raw splat space with
x,y → -x,-ywhile viewers rotate about X). The 📷 button takes the current camera position and converts it for you (for the demo room, 1 m above the floor is0, 1, 0). - Indoor —
-
Viewer — a top toolbar toggles the camera mode (fly default / orbit) and collision style; each item in the Scene hierarchy has a visibility button (splat / collision / voxels / bounds). Camera: orbit/pan with the mouse, fly with WASD. Generated collision results auto-load when the job finishes (toggle per panel). Each loaded layer shows a HUD chip; the chip's ✕ unloads that layer (frees its GPU memory — distinct from the visibility toggles), and Clear viewport unloads all three at once. Selecting the render camera or a capsule collider in the hierarchy raises a movable (and, for the camera, rotatable) gizmo; deselecting hides it. Collision style offers X-ray wireframe (small meshes), hidden-line wireframe (a depth-only prepass culls hidden edges — dense meshes auto-switch to this above 100K triangles), and Solid + edges (lit translucent surface — the mode for verifying placement against the splat and flying inside carved interiors). A separate Settings window (⚙ in the toolbar) holds visual settings (wire and voxel colours/opacity), the theme editor, workspace tools, advanced job options (
--scratch-dir), and update preferences. Clicking the eye on a.voxel.jsonrenders the sparse voxel octree as hardware-instanced translucent boxes (solid octree regions render as one merged box; display is capped at 1.5 M boxes — regenerate with a coarser voxel size if truncated). The.voxel.binformat is parsed client-side (client/src/voxel-parser.js, format documented in its header; validated against real output bynode scripts/test-voxel-parse.mjs <name>).
- Analyze panel — pick any splat and Summarize stats (
--stats,nulloutput, writes nothing). Results render as a persistent card: headline tiles (gaussian count, X×Y×Z extent, a NaN/Inf flag) over a per-column table with histograms, with a copy button for the raw Markdown. The card survives later jobs (unlike the transient Job log). .mjsgenerators — a JavaScript module that procedurally synthesizes a splat, run from the Generate tab (and usable as an Export/Analyze input). Drop one in or click + sample generator (writesexamples/gen-grid.mjs), then pick it in the Generate tab. A generator mustexportaGeneratorclass with a staticcreate(params)returning{ count, columnNames, getRow(index, row) }; column values are raw (log-space scale, logit opacity, SH-DC colour). Local-only.- Generator params (
-p/--params): passwidth=16,height=16,scale=4. If the generator advertises a staticparamsschema ([{name,label,min,max,step, default}]), the GUI renders live sliders instead of the freeform field. - ✨ Generate & view runs the generator and loads the result straight into the 3D viewer in one click; releasing a slider regenerates and re-previews.
- Generator params (
- Bounds (Viewer panel) — overlay the loaded splat's axis-aligned bounding box; its extent and any floaters/outliers (which stretch the box) show at a glance.
- WebP render — use the Render tab to rasterize
the splat to a lossless
.webpvia the GPU (--camera/--look-at/--fov/--resolution/--background). 📷 from viewer seeds the camera from the 3D view. Projection switches pinhole ↔ equirectangular 360° panorama. Depth of field (--f-stop/--focus-distance, pinhole only) and motion blur (--camera-end/--shutter/--motion-samples) are exposed too. - Device dropdown — choose the GPU adapter (listed via
-L/--list-gpus) or CPU (-g). Verbose adds--verbose --memorydiagnostics to the Job log. - HTML viewer output gains Unbundled (
-U, separate files) and a Viewer settings JSON (-E). An .lcc input gains LOD levels (-O).
The Edit panel drives splat-transform from the viewport — splats have no inherent scale or origin, so these fix both visually:
- Measure → scale — turn on Measure mode and click on the splat surface
to drop two markers (A green, B orange) at the ends of a feature whose real
size you know (a doorway, a 1 m scale bar); each marker can then be dragged to
fine-tune. The live readout shows the A–B distance; type the real length and
Apply scale writes a correctly-scaled splat (
-s/--scale) that auto-loads. - Apply scale directly — with a splat selected, type a scale factor and apply it without measuring.
- Set origin — turn on Pick origin, place the marker at the point that
should be
(0,0,0), and Set as origin recenters the splat (-t/--translate) — handy before placing it in a scene.
Both write a new splat and load it straight into the viewer.
- Backend — Express server (
server/) that spawns thesplat-transformCLI (--no-tty) as job subprocesses and serves theworkspace/directory. The CLI brings its own native WebGPU (Dawn) device for GPU stages. - Frontend — Vite + TypeScript (
client/), PlayCanvas engine 2.x for rendering. The splat loads via thegsplatasset type (.ply,.compressed.ply,.sog, unbundledmeta.json); the collision.collision.glbloads via thecontainerasset type and is drawn withRENDERSTYLE_WIREFRAMEon the Immediate layer. - Desktop app — the Electron main process
(electron/main.mjs) picks a free port, launches the Express
server (the Electron binary runs as Node via
ELECTRON_RUN_AS_NODE, so it can still spawn the native-WebGPUsplat-transformCLI), waits for it to come up, then opens the UI in a Chromium window. - Automation — GitHub Actions builds and publishes a Windows release on every
push:
devcuts a beta, promotingdevtomaincuts a stable release. A weekly routine tracks new splat-transform / PlayCanvas releases and wires new CLI flags into the GUI. See docs/AUTOMATION.md.
- GPU required for voxelization/collision and
--filter-cluster(the CLI uses native WebGPU). SOG compression can fall back to CPU ("CPU only" checkbox, 5–10× slower). - The API binds to
127.0.0.1only (it can write/delete files and spawn processes). A job is killed only after 10 minutes with no output (an idle watchdog, not a wall-clock cap — a streamed-LOD bake on a large scene runs well over an hour while emitting a chunk every few seconds, and must not be reaped). Override withSPLAT_JOB_IDLE_TIMEOUT_MS. A running job can be cancelled from the job panel. Uploads are capped at 8 GB and written via temp file + rename so aborted uploads leave nothing behind. - Export jobs never overwrite a pre-existing file the app didn't generate —
outputs divert to a
-convertedname instead (e.g. convertingroom.compressed.plyback to PLY won't clobber your originalroom.ply). - The splat renders with the conventional 180° X flip (PLY data is Y-down);
splat-transform's collision GLB is Y-up via a 180° Z rotation instead, so the
viewer applies a 180° Y rotation to it — verified with an asymmetric test
blob (
scripts/axis-test.mjs). Flip collision removes that rotation for meshes from tools that already match viewer space. - Workspace files live in
workspace/(gitignored). The job log panel shows the exact CLI invocation for reproducing outside the GUI.
Everything the GUI does goes through this loopback API, so the whole app is
scriptable (see the splat-studio-control skill for a full walkthrough):
| Route | Purpose |
|---|---|
GET /api/health · GET /api/versions |
liveness, app/CLI/engine versions |
GET/POST /api/workspace |
read / repoint the workspace root |
GET/POST /api/projects |
list / create projects |
GET /api/files |
list workspace files |
POST /api/upload?name= |
upload (raw body stream) |
DELETE /api/files/:name |
delete file (folder for unbundled SOG) |
POST /api/convert |
{ input, format, options } → { jobId } |
POST /api/collision |
{ input, options } → { jobId } |
POST /api/summary · GET /api/stats |
stats as a job / parsed on demand |
POST /api/trim |
box/sphere carve → new splat |
GET /api/gpus |
list GPU adapters |
GET /api/generator-params |
a generator's slider schema |
GET /api/jobs · GET /api/jobs/:id |
job list / status, log, outputs |
POST /api/jobs/:id/cancel |
kill a running job |
GET/POST /api/layout |
saved dock layout |
GET/POST /api/groups |
linked-location groups |
POST /api/editor/command · GET /api/editor/status · POST /api/editor/control |
MCP live-editor relay (consent-gated) |
GET /files/* |
static workspace files |
Splat Studio ships an MCP server (mcp-server/) that lets an AI
agent drive the headless splat-transform pipeline (convert, LOD, render, collision, trim, analyze —
always available) and, with your consent, the live editor (camera, panels, gizmos, tools). It connects
to the running app over loopback and never launches it.
Point an MCP client (Claude Desktop, Claude Code, etc.) at the server:
{
"mcpServers": {
"splat-studio": {
"command": "node",
"args": ["<path-to>/splat-studio/mcp-server/index.mjs"]
}
}
}Start Splat Studio first. Headless tools work immediately. To let an agent control the live editor, turn on Settings → Agent control (MCP) — it's off by default, loopback-only, and revocable instantly. 28 tools total.
📖 Step-by-step install + client setup (Claude Desktop / Claude Code), the full tool list, and
troubleshooting: docs/MCP_SETUP.md. Workflow tutorials — from "convert this
for the web" to 360° panoramas and real-world scaling: docs/MCP_WORKFLOWS.md.
For the agent-facing playbooks, see the splat-studio-mcp and splat-studio-workflows skills.
- PlayCanvas engine (playcanvas.com) — WebGL/WebGPU rendering and Gaussian-splat support.
- @playcanvas/splat-transform — the splat conversion / SOG / collision CLI this GUI drives.
- Electron + electron-builder — the standalone desktop app and Windows installers.
- Vite + TypeScript — the frontend build.
- Express — the local job server.
- dockview — the dockable tab/window editor.
Contributions are welcome — issues and pull requests both.
- Fork and create a branch off
dev, named<type>/<kebab-summary>(feat,fix,chore,docs,ci, orrefactor— e.g.fix/region-gizmo-detach). PRs targetdev(the beta channel);mainonly moves by promotingdevto a stable release. See CONTRIBUTING.md for the full flow. npm install, then make your change. Keep it focused (one feature/fix per PR).- Before opening a PR, run the checks:
Both must pass. UI/viewer changes should be verified against a running
npm run typecheck npm test # tests/e2e.mjs regression suite
npm run dev. - Match the existing style: brief code comments (file headers + one-line function
signatures; short whys only for non-obvious constraints); new CLI flags get
a control + tooltip in the GUI and a
check(...)in the e2e suite (see the.claude/skills/routines for how features and dependency bumps are wired).
Open an issue first if you want to discuss a larger change.
Splat Studio is developed with Claude Code.
Commits and pull requests carry standard Co-Authored-By attribution, and PRs
opened autonomously by the weekly dependency-update routine say so in their
description. All changes — human- or agent-authored — are reviewed before merge
and held to the same checks described in Contributing.
Released under the Apache License 2.0 © 2026 CodeByKeegan — see also NOTICE.
If Splat Studio is useful to you, you can support its development:

