3dtiled-to-3dtiles
3DTiles + glTF

3D Gaussian Splatting extension lineage

The glTF extensions for 3D Gaussian splatting went through several incarnations in 2025–2026.

The glTF extensions for 3D Gaussian splatting went through several incarnations in 2025–2026. Understanding the lineage is what pinned down the SPZ-version bug above, so it is documented here in full.

Timeline

  • ~June 2025 — a single experimental extension KHR_spz_gaussian_splats_compression shipped in CesiumJS. It bundled the SPZ blob and the splat semantics together.
  • Mid 2025 — Khronos split it into two:
    • KHR_gaussian_splatting — renderer-agnostic base: defines the splat attributes (POSITION, COLOR_0, and namespaced KHR_gaussian_splatting:SCALE / :ROTATION / :SH_DEGREE_*), the optional shape (ellipsoid/…) and hints (sorting method, projection), and allows uncompressed or meshopt-compressed data.
    • KHR_gaussian_splatting_compression_spz_2 — a child extension nested inside the base on the primitive, carrying only the SPZ-compressed blob (a bufferView).
  • Sep 2025 (CesiumJS) — the original single extension was deprecated; the tiler in Cesium ion switched to outputting the base + _spz_2 pair.
  • Nov 2025 (CesiumJS v1.135) — the original extension was removed; only the base + _spz_2 pair is supported. Existing tilesets must be re-tiled.
  • Jan 2026 — KHR_gaussian_splatting (base) merged into the glTF repo as a release candidate.
  • Jan 2026 (16th) — a second compression codec is proposed: KHR_gaussian_splatting_compression_lgsc, wrapping Qualcomm's L-GSC (Lite 3D Gaussian Splat Codec) v1.0. Opened as KHR_gaussian_splatting_lgsc and renamed during review — "The pattern we've been using for compression extensions is KHR_gaussian_splatting_compression_lgsc" — which confirms the shape the split above was aiming at: one base extension, an open set of interchangeable compression children, SPZ being the first rather than the only one. Still an open PR (unmerged, awaiting an approving review) and carrying only a README, so the payload nesting isn't specified yet and nothing in this repo's renderer stack decodes it. Details + codec characteristics: KHR_gaussian_splatting → L-GSC.

Why the _2 matters (the bug). The _2 in KHR_gaussian_splatting_compression_spz_2 is the SPZ format version (v2). As lexaknyazev notes in the PR, a KHR extension cannot float across SPZ versions — for IP and portability reasons it is locked to a specific one. CesiumJS decodes via the Niantic spz library against the v2 layout. SPZ versions differ in the rotation block:

SPZ versionPositionsRotationsBytes/splatNiantic packer
v116-bit———
v2 ← what _spz_2 requires24-bit (frac bits)3 bytes (x,y,z; w≥0 recovered)19packQuaternionFirstThree
v324-bit4 bytes (smallest-three uint32)20packQuaternionSmallestThree

rad-to-3dtiles had been emitting v3 (because SparkJS writes v3 by default). Under the _spz_2 extension, CesiumJS read that 4-byte rotation block at a 3-byte stride → corrupted rotations that looked like global scale/falloff weirdness. Fix: emit SPZ v2 (see blocker #1 above). SparkJS's reader version-dispatches, so v2 also renders in the 3DTRJS 3DGS plugin.

A separate, related issue: glTF #2509 reports splats rendering upside-down in Cesium — that is a coordinate-system matter (SPZ's Left-Up-Front convention / SH handedness baked into the Niantic CoordinateConverter), distinct from the rotation-stride bug. If, after re-tiling to v2, orientation (not scale) still looks off, that conversion is the next thing to check.

Primary sources