Skip to main content

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:

FilePurposeChannels (8-bit)
meta.jsonScene metadata and filenames
means_l.webpPositions – lower 8 bits (RGB)R,G,B
means_u.webpPositions – upper 8 bits (RGB)R,G,B
quats.webpOrientation – compressed quaternionR,G,B,A
scales.webpPer-axis sizes via codebookR,G,B
sh0.webpBase color (DC) + opacityR,G,B,A
shN_labels.webpIndices into SH palette (optional)R,G
shN_centroids.webpSH palette coefficients (optional)RGBA
Image formats
  • By default, images should be lossless WebP to preserve quantized values exactly.
  • Each property in meta.json names 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 W and height H, the number of addressable Gaussians is W*H.
  • meta.count must 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)
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; maps quantized DC to linear 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 1) lives in sh0.
codebook: number[]; // 256 floats; shared for all AC coefficients (§3.5)
files: [
"shN_labels.webp", // Per-gaussian palette index (0..count-1)
"shN_centroids.webp" // Palette of AC coefficients as pixels (§3.5)
];
};
}
note
  • All codebooks contain linear-space values, not sRGB.
  • Image data must be treated as raw 8-bit integers (no gamma conversion).
  • Unless otherwise stated, channels not mentioned are ignored.

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 - 2523 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.a must 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 (linear domain).
  • A is the opacity in [0,1] (i.e., sh0.a / 255).

To convert the DC coefficient to linear RGB contribution:

// 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;

Color space. Values are linear. If you output to sRGB, apply the usual transfer after shading/compositing.

3.5 Higher-order SH (optional)

shN_labels.webp, shN_centroids.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.

Labels

  • shN_labels.webp stores a 16-bit index per gaussian with range (0..count-1).
const index = shN_labels.r + (shN_labels.g << 8);

Centroids (palette)

  • shN_centroids.webp is 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:

BandsCoefficientsTexure width (pixels)
1364 * 3 = 96
2864 * 8 = 512
31564 * 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] + c;
const v = Math.floor(n / 64);

4. Example meta.json

{
"version": 2,
"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": 128,
"bands": 3,
"codebook": [/* 256 floats */],
"files": ["shN_labels.webp", "shN_centroids.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.