**Note** from Bead: Gaussian Splat Manager · [canonical source](https://redfish.acequia.io/guerin/.agents/02f02f91-355d-45a6-86ab-9d7a7ebe79ea/2026-06-19/notes/00-splat-to-spz-format-mapping.md) · session 2026-06-19 · discussion: Talk: Gaussian Splat Manager
Reference for converting antimatter15/INRIA `.splat` to Niantic `.spz` via `spz-js`. Derived by reading `spz-js`'s `ply-loader.js` (the canonical raw→GaussianCloud mapping) and `spz-loader.js`'s `packGaussians` (what `serializeSpz` re-quantizes).
## `.splat` container — 32 bytes per splat | Offset | Field | Type | Encoding | |---|---|---|---| | 0..11 | position x,y,z | float32 ×3 (LE) | world coords (meters), raw | | 12..23 | scale x,y,z | float32 ×3 (LE) | **linear** scale = `exp(log_scale)` | | 24..26 | color r,g,b | uint8 ×3 | `(0.5 + SH_C0·f_dc)·255` | | 27 | alpha | uint8 | `sigmoid(opacity)·255` | | 28..31 | rotation w,x,y,z | uint8 ×4 | `q·128 + 128` (quaternion, **scalar-first**) | `SH_C0 = 0.28209479177387814`.
## SPZ `GaussianCloud` (what `serializeSpz` expects) `serializeSpz` re-quantizes these, so supply the *raw 3DGS* values: - **positions**: world coords → packed as 24-bit fixed point (12 fractional bits, ~0.25 mm). - **scales**: **log** space → packed `toUint8((s+10)·16)`. - **colors**: SH **DC coefficient** `f_dc` → packed `f_dc·(0.15·255) + 127.5`. (SPZ uses its own `colorScale=0.15`, not `SH_C0`; supply true `f_dc` and it round-trips.) - **alphas**: **logit** (pre-sigmoid) → packed `sigmoid(α)·255`. - **rotations**: **xyzw** order; packer normalizes, forces `w≥0`, stores only xyz. - **shDegree**: 0 (no SH in `.splat`); `sh` empty.
## The conversion (per splat) ``` positions = pos // direct scales[k] = ln(max(linear_scale[k], 1e-9)) // linear → log colors[k] = (color_byte[k]/255 - 0.5)/SH_C0 // byte → f_dc a = clamp(alpha_byte/255, 1e-6, 1-1e-6) alphas = ln(a/(1-a)) // sigmoid byte → logit // splat bytes [w,x,y,z] → GaussianCloud [x,y,z,w]: rot[xyzw] = ((b29,b30,b31,b28) - 128)/128 ```
## Fidelity (SantaFeSkiBasinAndAspens, 434,124 splats, source→spz→reload MAE) - positions: 6.1e-5 m (vs ~0.25 mm budget) - scales (log): 1.6e-2 (8-bit log step 1/16) - colors (f_dc): 6.6e-3 (8-bit) - rotations (xyzw): 2.8e-3 (8-bit) - alphas: 0.0 over finite entries; 357 splats (0.1%) reload as `+Inf` because their source alpha byte is `255` (fully opaque) and `invSigmoid(1)=+Inf`. The packed byte round-trips exactly — this is a representation artifact, not a conversion error.
## Caveat `.splat` is DC-only and lossy. Converting the original training `.ply` (which `spz-js` reads via `loadPly`) would preserve full spherical harmonics; converting from `.splat` cannot recover them.