The SOG Format
SOG (Spatially Ordered Gaussians) is a compact container for 3D Gaussian Splat data. It achieves high compression via quantization (lossy by design), typically yielding files ~15–20× smaller than an equivalent PLY.
You can create SOG files with SplatTransform and preview them in the PlayCanvas Viewer.
This document is the format specification.
1. File set
A SOG dataset is a set of images plus a metadata file:
| File | Purpose | Channels (8-bit) |
|---|---|---|
meta.json | Scene metadata and filenames | — |
means_l.webp | Positions – lower 8 bits (RGB) | R,G,B |
means_u.webp | Positions – upper 8 bits (RGB) | R,G,B |
scales.webp | Per-axis sizes via codebook | R,G,B |
quats.webp | Orientation – compressed quaternion | R,G,B,A |
sh0.webp | Base color (DC) + opacity | R,G,B,A |
shN_centroids.webp | SH palette coefficients (optional) | R,G,B |
shN_labels.webp | Indices into SH palette (optional) | R,G |
- By default, images should be lossless WebP to preserve quantized values exactly.
- Each property in
meta.jsonnames its file, so other 8-bit RGBA-capable formats may be used. - Do not use lossy encodings for these assets as lossy compression will corrupt values and can produce visible/structural artifacts.
1.1 Image dimensions & indexing
All per-Gaussian properties are co-located: the same pixel (x, y) across all property images (except shN_centroids) belongs to the same Gaussian.
- Pixels are laid out row-major, origin at the top-left.
- For image width
Wand heightH, the number of addressable Gaussians isW*H. meta.countmust be<= W*H. Any trailing pixels are ignored.
Indexing math (zero-based):
- From index to pixel:
x = i % W,y = floor(i / W) - From pixel to index:
i = x + y * W
1.2 Coordinate system
Right-handed:
- x: right
- y: up
- z: back (i.e., −z is “forward” in camera-looking-down −z conventions)
1.3 Bundled variant
A bundled SOG is a ZIP of the files above. Readers should accept either layout:
- Multi-file directory (recommended during authoring)
- Single archive (e.g.,
scene.sog) containing the same files at the archive root
Readers must unzip and then resolve files using meta.json exactly as for the multi-file version.
2. meta.json
interface Meta {
version: 2; // File format version (integer)
asset?: { // Optional tool/version metadata
generator?: string; // e.g. "splat-transform v1.2.3"
};
count: number; // Number of gaussians (<= W*H of the images)
antialias: boolean; // True iff scene was trained with anti-aliasing
means: {
// Ranges for decoding *log-transformed* positions (see §3.1).
mins: [number, number, number]; // min of nx,ny,nz (log-domain)
maxs: [number, number, number]; // max of nx,ny,nz (log-domain)
files: ["means_l.webp", "means_u.webp"];
};
scales: {
codebook: number[]; // 256 floats; see §3.3
files: ["scales.webp"];
};
quats: {
files: ["quats.webp"]; // §3.2
};
sh0: {
codebook: number[]; // 256 floats; DC coefficients for gamma-space color (§3.4)
files: ["sh0.webp"];
};
// Present only if higher-order SH exist:
shN?: {
count: number; // Palette size (up to 65536)
bands: number; // Number of SH bands (1..3). DC (=band 0) lives in sh0.
codebook: number[]; // 256 floats; shared for all AC coefficients (§3.5)
files: [
"shN_centroids.webp",// Palette of AC coefficients as pixels (§3.5)
"shN_labels.webp" // Per-gaussian palette index (0..count-1)
];
};
}
- The scales codebook contains linear-space values. The sh0 codebook contains gamma-space DC coefficients. The shN codebook contains gamma-space AC coefficients.
- Image data must be treated as raw 8-bit integers (no gamma conversion).
- Unless otherwise stated, channels not mentioned are ignored.
- Filenames in
filesarrays are arbitrary, but the order is significant.
3. Property encodings
3.1 Positions
means_l.webp,means_u.webp(RGB, 16-bit per axis)
Each axis is quantized to 16 bits across two images:
// 16-bit normalized value per axis (0..65535)
const qx = (means_u.r << 8) | means_l.r;
const qy = (means_u.g << 8) | means_l.g;
const qz = (means_u.b << 8) | means_l.b;
// Dequantize into *log-domain* nx,ny,nz using per-axis ranges from meta:
const nx = lerp(meta.means.mins[0], meta.means.maxs[0], qx / 65535);
const ny = lerp(meta.means.mins[1], meta.means.maxs[1], qy / 65535);
const nz = lerp(meta.means.mins[2], meta.means.maxs[2], qz / 65535);
// Undo the symmetric log transform used at encode time:
const unlog = (n: number) => Math.sign(n) * (Math.exp(Math.abs(n)) - 1);
const p = {
x: unlog(nx),
y: unlog(ny),
z: unlog(nz),
};
3.2 Orientation
quats.webp(RGBA, 26-bit “smallest-three”)
Quaternions are encoded with 3×8-bit components + 2-bit mode (total 26 bits) using the standard smallest-three scheme.
- R,G,B store the three kept (signed) components, uniformly quantized to
[-√2/2, +√2/2]. - A stores the mode in the range 252..255. The mode is
A - 252∈ 3 and identifies which of the four components was the largest by magnitude (and therefore omitted from the stream and reconstructed). - Let
norm = Math.SQRT2(i.e., √2).
// Dequantize the stored three components:
const toComp = (c: number) => (c / 255 - 0.5) * 2.0 / Math.SQRT2;
const a = toComp(quats.r);
const b = toComp(quats.g);
const c = toComp(quats.b);
const mode = quats.a - 252; // 0..3 (R,G,B,A is one of the four components)
// Reconstruct the omitted component so that ||q|| = 1 and w.l.o.g. the omitted one is non-negative
const t = a*a + b*b + c*c;
const d = Math.sqrt(Math.max(0, 1 - t));
// Place components according to mode
let q: [number, number, number, number];
switch (mode) {
case 0: q = [d, a, b, c]; break; // omitted = x
case 1: q = [a, d, b, c]; break; // omitted = y
case 2: q = [a, b, d, c]; break; // omitted = z
case 3: q = [a, b, c, d]; break; // omitted = w
default: throw new Error("Invalid quaternion mode");
}
Validity constraints
quats.amust be in 252, 253, 254, 255. Other values are reserved.
3.3 Scales
scales.webp(RGB via codebook)
Per-axis sizes are codebook indices:
const sx = meta.scales.codebook[scales.r]; // 0..255
const sy = meta.scales.codebook[scales.g];
const sz = meta.scales.codebook[scales.b];
Interpretation (e.g., principal axis standard deviations vs. full extents) follows the source training setup; values are in scene units.
3.4 Base color + opacity (DC)
sh0.webp(RGBA)
sh0 holds the DC (l=0) SH coefficient per color channel and alpha:
- R,G,B are 0..255 indices into
sh0.codebook. - A is the opacity in
[0,1](i.e.,sh0.a / 255).
To convert the DC coefficient to gamma-space RGB color:
// SH_C0 = Y_0^0 = 1 / (2 * sqrt(pi))
const SH_C0 = 0.28209479177387814;
const r = 0.5 + meta.sh0.codebook[sh0.r] * SH_C0;
const g = 0.5 + meta.sh0.codebook[sh0.g] * SH_C0;
const b = 0.5 + meta.sh0.codebook[sh0.b] * SH_C0;
const a = sh0.a / 255;
3.5 Higher-order SH (optional)
shN_centroids.webp,shN_labels.webp
If present, higher-order (AC) SH coefficients are stored via a palette:
shN.count∈ [1,64k] number of entries.shN.bands∈ [1,3] number of bands per entry.
Centroids (palette)
shN_centroids.webpis an RGB image storing the SH coefficient palette.- There are always 64 entries per row; entries are packed row-major with origin top-left.
The texture width is dependent on the number of bands:
| Bands | Coefficients | Texture width (pixels) |
|---|---|---|
| 1 | 3 | 64 * 3 = 96 |
| 2 | 8 | 64 * 8 = 512 |
| 3 | 15 | 64 * 15 = 960 |
Calculating the pixel location for spherical harmonic entry n and coefficient c:
const coeffs = [3, 8, 15];
const u = (n % 64) * coeffs[bands - 1] + c;
const v = Math.floor(n / 64);
Labels
shN_labels.webpstores a 16-bit index per gaussian with range (0..count-1).
const index = shN_labels.r + (shN_labels.g << 8);
Decoding
Each centroid pixel's R,G,B channels are codebook indices (0..255) into the shared shN.codebook. The three channels correspond to the three color channels of the SH coefficient.
To decode the full set of AC SH coefficients for a single gaussian:
const coeffs = [3, 8, 15];
const shCoeffs = coeffs[meta.shN.bands - 1]; // coefficients per color channel
const codebook = meta.shN.codebook; // 256 shared float entries
// 1. Look up this gaussian's palette entry
const label = shN_labels.r + (shN_labels.g << 8);
// 2. For each coefficient, read RGB from the centroids pixel and map through the codebook
for (let c = 0; c < shCoeffs; c++) {
const u = (label % 64) * shCoeffs + c;
const v = Math.floor(label / 64);
// pixel (u, v) in shN_centroids.webp
const r = centroidsPixel(u, v).r;
const g = centroidsPixel(u, v).g;
const b = centroidsPixel(u, v).b;
sh_channel0[c] = codebook[r]; // SH AC coefficient for color channel 0
sh_channel1[c] = codebook[g]; // SH AC coefficient for color channel 1
sh_channel2[c] = codebook[b]; // SH AC coefficient for color channel 2
}
4. Example meta.json
{
"version": 2,
"asset": { "generator": "splat-transform v1.2.3" },
"count": 187543,
"antialias": true,
"means": {
"mins": [-2.10, -1.75, -2.40],
"maxs": [ 2.05, 2.25, 1.90],
"files": ["means_l.webp", "means_u.webp"]
},
"scales": {
"codebook": [/* 256 floats */],
"files": ["scales.webp"]
},
"quats": { "files": ["quats.webp"] },
"sh0": {
"codebook": [/* 256 floats */],
"files": ["sh0.webp"]
},
"shN": {
"count": 65536,
"bands": 3,
"codebook": [/* 256 floats */],
"files": ["shN_centroids.webp", "shN_labels.webp"]
}
}
5. Versioning & compatibility
- Readers must check
version. This document describes version 2. - Additional optional properties may appear in future versions; readers should ignore unrecognized fields.