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
scales.webpPer-axis sizes via codebookR,G,B
quats.webpOrientation – compressed quaternionR,G,B,A
sh0.webpBase color (DC) + opacityR,G,B,A
shN_centroids.webpSH palette coefficients (optional)R,G,B
shN_labels.webpIndices into SH palette (optional)R,G
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)
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)
];
};
}
note
  • 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 files arrays 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 - 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.
  • 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.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:

BandsCoefficientsTexture 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 - 1] + c;
const v = Math.floor(n / 64);

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

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.