Skip to main content

Gaussian splat formats and loaders

Khronos Gaussian splats, 3D Tiles, and SPZ

Decoded glTF primitives declaring KHR_gaussian_splatting can be prepared directly without adding a loaders.gl or glTF dependency to the rendering package:

import {loadGPUSplatDataFromGLTF, makeGPUSplatDataFromGLTF} from '@luma.gl/splats';

const prepared = makeGPUSplatDataFromGLTF(device, decodedPrimitive, {
sourceBatchIndex: tile.sourceBatchIndex,
rowIndexBase: tile.firstSourceRow,
maxSphericalHarmonicsDegree: 2
});

const compressed = await loadGPUSplatDataFromGLTF(device, compressedPrimitive, {
signal,
decodeCompressedPrimitive: (primitive, options) =>
applicationOwnedSPZDecoder.decode(primitive, options)
});

The structural adapter validates POINTS primitives, the supported ellipse kernel, color space, projection, and sorting method. It preserves source POSITION, decodes normalized scale/opacity accessors, converts glTF XYZW quaternions to renderer WXYZ order, reconstructs spherical-harmonic DC radiance, and packs complete degree-one through degree-three RGB coefficient bands. Authored EXT_mesh_features feature identifiers become stable source semantic IDs. Compressed KHR_gaussian_splatting_compression_spz_2 payloads are handed to an explicitly caller-owned decoder; this renderer does not claim to parse SPZ or fetch 3D Tiles itself.

loaders.gl already provides Tileset3D and Tiles3DSource for 3D Tiles traversal, content fetching, tile transforms, cache management, and request scheduling. Its glTF loader already decodes EXT_mesh_features and EXT_structural_metadata. The existing SPZLoader currently supports SPZ version 4, not SPZ version 2. End-to-end Gaussian 3D Tiles require loader-owned KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 runtime handlers, SPZ v2 decoding, and an application bridge from selected tiles into prepared splat batches or the existing SplatLayer. loaders.gl already computes Tile3D.computedTransform, but the paged splat renderer currently exposes one global model-view-projection transform and no per-page model transform. The remaining renderer-side integration must therefore apply loader-computed transforms per tile/page. The tileset traversal and layer already exist; follow tracking issue #1245 for the missing integration.

Local loaders.gl 5 alpha showcase

The Gaussian Splats showcase normally generates a deterministic scene without depending on loaders.gl. To exercise a neighboring loaders.gl 5 alpha checkout instead, expose its location when starting the standalone example:

VITE_LOADERS_GL_ROOT=/path/to/loaders.gl \
yarn workspace luma.gl-examples-showcase-gaussian-splats start

Open the example with ?loaders=local to stream the complete 741,883-splat Train scene from the same Hugging Face catalog used by the loaders.gl Gaussian splat example. Use ?loaders=local&scene=drjohnson, scene=playroom, or scene=truck to select the other catalog scenes. If the Hugging Face CDN is unavailable, Train automatically falls back to its two GitHub-hosted PLY segments; scene=train-github selects those segments directly.

Provide an authorized custom RAD source with ?source=https://example.com/scene.rad. On WebGPU, the showcase first range-fetches the root page, then uses the live camera and authored per-row child links to request, cancel, and evict only the source pages needed by its current hierarchy frontier. GPUPagedSplatRenderer projects the selected original rows and sorts them globally across bounded GPU segments. The default residency window retains at most one million original source rows; set &residentSplats=250000 to choose another whole-page source budget. The overlay reports the complete authored source count, current selected rows, and resident pages.

The website showcase ships a bounded module-worker pool for actual RAD chunk decoding. Abortable source byte ranges are transferred to a background worker, parsed with the isolated loaders.gl 5 bundle, serialized as transferable Arrow IPC, and reconstructed without losing original child-row links or global source identities. Unsupported worker environments retain an explicit main-thread fallback. Compatible RAD sources use Spark-calibrated best-first angular refinement, nonlinear coarse-level opacity, analytic covariance projection, and area-preserving Gaussian antialiasing. Explicit CPU and non-hierarchical RAD sources retain their bounded whole-page path.

Use ?loaders=local&scene=fixture for the lightweight 1,000-splat parser fixture, or provide source to select a custom .ply, .splat, .ksplat, .spz, or .rad file. Full PLY scenes are streamed through their original Arrow record batches, and the showcase reports download, batch, and splat progress while retaining independently owned GPU buffers. The loader remains an application-level dependency; @luma.gl/splats continues to own only GPU data and rendering. Both the default WebGPU graph and the explicit &renderer=cpu comparison retain and evaluate camera-dependent higher-order spherical harmonics.

GraphDECO captures do not embed a universal world-up direction. The showcase applies known scene-specific up vectors and, for Truck, its published initial camera; unfamiliar custom sources retain the existing Z-up default. Foreground-centered framing, preserved manual camera movement during streaming, and idle redraw suppression keep large scenes easier to inspect.

Package boundaries

  • @loaders.gl/splats parses Gaussian splat file formats and produces application-level data.
  • @loaders.gl/tiles and @loaders.gl/3d-tiles own tileset traversal, content transport, transforms, request scheduling, and caching.
  • @loaders.gl/gltf owns glTF parsing, extension decoding, and feature metadata.
  • @luma.gl/splats owns rendering, Gaussian projection, sorting, and GPU resource lifetimes.
  • Applications or deck.gl layers own viewport integration, file selection, and interactive UI.

This separation keeps the renderer reusable from standalone luma.gl applications and deck.gl adapters while preserving streaming batch boundaries.