Quickstart#
The code below is a complete page that loads a splat file and draws it with orbit controls. After it come the first things most apps change. It assumes three.js and a bundler; the imports come from @sparkjsdev/radix.
A page that draws a file#
import * as THREE from "three";
import {
CameraControls,
GpuArrays,
SplatCollection,
SplatRenderer,
createRenderer,
loadSplats,
} from "@sparkjsdev/radix";
// 1. The renderer: WebGPU, or the WebGL2 backend where WebGPU is missing
const renderer = await createRenderer();
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.append(renderer.domElement);
// 2. A scene, a camera, and controls
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 1000);
camera.position.set(0, 0.5, 3);
const controls = new CameraControls({
canvas: renderer.domElement,
object: camera,
storageKey: "my-page",
settings: { mode: "orbit" },
});
controls.lookAt(new THREE.Vector3(0, 0, 0));
// 3. Load the file on a worker, then upload it
const data = await loadSplats("scene.spz", {
onProgress: (fraction, stage) => console.log(stage, fraction),
});
const splats = GpuArrays.fromData(renderer, data);
// 4. Place it, and draw it
const model = new THREE.Object3D();
// Most captures are stored upside down (y down)
model.rotation.x = Math.PI;
scene.add(model);
const collection = new SplatCollection([
SplatCollection.arrays(splats, { matrixWorld: model.matrixWorld }),
]);
const splatRenderer = new SplatRenderer({ renderer, collection });
scene.add(splatRenderer);
// 5. Render every frame
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
What each step does#
createRenderer()returns an initializedWebGPURenderer. It picks WebGPU when the browser has it and the WebGL2 backend otherwise, and asks the device for the limits that about 95% of devices support. URL parameters can override its options:?gpu=webgpu-glforces WebGL2,?profile=1turns on GPU timing (The renderer).CameraControlsmoves the camera from the pointer, touch, keyboard, and gamepads.update()applies the input once per frame. Press F to switch between orbit and fly (Camera controls).loadSplats(url)downloads and decodes PLY, SPZ, RAD, or RDX on a worker and returns anArrayData: a table with one row per splat, here in theextSplatslayout (32 bytes per splat).GpuArrays.fromDatauploads the table into textures, so rowion the CPU is rowion the GPU (Splat files, GPU arrays).SplatCollectionlists what to draw: here one object, the uploaded splats placed in the world bymodel.matrixWorld.SplatRendereris aTHREE.Meshthat draws a collection. The matrix is read on every render, so movingmodelmoves the splats (Rendering).- Each render projects every splat for the camera, sorts them back to front, and draws them in one draw call. The app needs no other per-frame calls and no views.
Test the WebGL2 fallback#
Open the page with ?gpu=webgpu-gl. The page works unchanged, with two visible differences:
- The sort runs on a worker. The first frames draw nothing until the first order arrives, and while the camera moves the order lags a few frames behind it.
- The splats appear a moment after loading, and a new collection replaces the old one a few frames after it is set.
Sorting explains why, and Views and CPU orders shows how to control it.
Change the look#
// "ewa" (default), "3dgut", "htgs", or "raygs"
splatRenderer.method = "raygs";
// Less anti-aliasing blur
splatRenderer.objectParams.blurAmount = 0.1;
// Draw each Gaussian a little farther out
splatRenderer.params.maxStdDev = 3;
The four methods are four ways to rasterize a splat. ewa is classic 3D Gaussian splatting and the cheapest. The ray methods evaluate each splat along the pixel's ray, which is exact for large and close splats (Projection). Changing a param recompiles nothing, so params can change every frame (Params).
Load another layout#
// 16 bytes per splat: half the memory
const data = await loadSplats(url, { layout: "packedSplats" });
packedSplats keeps colors, scales, and rotations in 8 bits each. Use it for very large files or low-memory devices (Splats).
Spherical harmonics#
Spherical harmonics load (into extSh, 64 bytes per splat at degree 3) and draw by default; no extra code is needed. To save memory or skip them:
// 48 bytes per splat, near-lossless
const smaller = await loadSplats(url, { sh: "packed" });
// Base colors only
const plain = await loadSplats(url, { sh: false });
// Or keep them and draw fewer bands (0: none). Nothing recompiles
splatRenderer.objectParams.maxShDegree = 1;
Spherical harmonics covers the codecs, palettes, and drawing SH in your own generator.
Clean up#
splatRenderer.collection = new SplatCollection([
SplatCollection.arrays(splats, {
matrixWorld: model.matrixWorld,
release: () => splats.dispose(),
}),
]);
// Later: dispose runs the release, which frees the textures
scene.remove(splatRenderer);
splatRenderer.dispose();
release runs once nothing draws the object any more. Radix doesn't free an app's data; it calls release when freeing it is safe (Lifetimes).
Next#
- Recipes: several files, generated splats, subsets, effects, split screens, and stills.
- Concepts: definitions of the terms the other pages use.
- The example pages in this repo:
/build/examples/splats/index.html(a full viewer:examples/splats/),/build/examples/scenes/index.html(short usage examples:examples/scenes/scenes.ts), and/build/examples/xr/index.html(WebXR:examples/xr/main.ts). The same pages run as static pages on the library bundle (npm run build,npm run serve, thenhttp://localhost:8000/build/examples/index.html), with the ports of Spark's examples and larger examples, each self-contained in its folder ofexamples/: SOG and LCC viewers with level of detail (sogs/,lcc/), 3D fractals (fractals/), MLS-MPM water (mpm/, WebGPU), an effect-graph editor (fxedit/), and the Doom and Quake shareware episodes (doom/,quake/).npm run build:singlealso builds each of these pages as one HTML file (build/examples/<name>/<name>.html).
Source: docs/quickstart.md