encoding.md

docs/encoding.md · raw · all examples · site home

Headings: EncodingsThe fieldsOctahedral directionsQuaternions as an axis and an angleShared-exponent colorEncoding: the CPU codecsSplatMath: quaternions and covariancesThe shader twinsCPU and GPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# Encodings

The splat layouts store their fields with the codecs below. `Encoding` and `SplatMath` (CPU) and their twins `GpuEncoding` and `GpuSplatMath` (TSL) are public, so other formats and shaders can reuse them and get the same bits.

Code: `src/core/encoding.ts` and `math.ts`; `src/gpu/encoding.tsl.ts` and `math.tsl.ts`. The word maps of each layout are in [splats.md](splats.md#layouts).

```ts
// A direction in 24 bits
const code = Encoding.encodeOct24({ x: 0, y: 0, z: 1 });

// Decodes within 0.06 degrees
const back = Encoding.decodeOct24(code, { x: 0, y: 0, z: 0 });

// The same bits in a shader
const dir = GpuEncoding.decodeOct24(word.bitAnd(0xffffff));
```

## The fields

| Field | Layouts | Stored as | Precision |
|---|---|---|---|
| center | ext, cov | float32 x 3 | exact |
| center | packed | half float x 3 | 11 significant bits (about 0.05% of the distance from the origin) |
| scales | ext | half(ln s) x 3 | ln s within about 2^-11 of itself, so s within about 2^-11 x \|ln s\| of itself (0.25% at s = e^-5) |
| scales | packed | 8-bit ln s over `[lnScaleMin, lnScaleMax]` (0 = a zero scale) | a step of 21 / 255 ≈ 0.08 in ln s by default: about 8% of the scale per step |
| rotation | ext | `QuatOct32`: a 10 + 10 bit octahedral axis and a 12-bit angle | rotations under 2 degrees within 0.03 degrees |
| rotation | packed | `QuatOct24`: an 8 + 8 bit axis and an 8-bit angle | an angle step of 0.7 degrees |
| covariance | cov | half(ln) of the diagonal; half(sign x ln of the abs correlation) for the rest | relative, about 2^-11 |
| color | ext, cov | three 8-bit magnitudes, three sign bits, and a shared 5-bit exponent | 1 / 255 of the largest channel's power of two; values above 1 and below 0 are kept |
| color | packed | unorm8 over `[rgbMin, rgbMax]` | 1 / 255 of the range |
| opacity | ext, cov | one byte, 128 = 1.0 | 1 / 128; up to 255 / 128 for merged LoD nodes |
| opacity | packed | unorm8 | 1 / 255; at most 1 |
| normal | ext, cov | `Oct24`: 12 + 12 bits; code 0 = none | within 0.06 degrees |
| roughness, metallic | ext | unorm8 each | 1 / 255 |

A fresh row (all zero bits) decodes as roughness 0, metallic 0, and no normal until `Splats.storePbr` writes it.

## Octahedral directions

A direction is divided by its L1 norm, which maps it onto an octahedron. The lower half of the octahedron is folded over the upper half, giving a point in a square. Each of the point's two coordinates is rounded to `bits` bits, and the code is `u | v << bits`. Code 0 means no vector. A direction that rounds to 0 gets the all-ones code instead, which decodes to the same direction.

The error is nearly uniform over the sphere. `Oct24` (12 bits per axis) is within about 0.06 degrees. The encoder rounds each coordinate and does not search neighboring codes.

## Quaternions as an axis and an angle

`QuatOct` codes store the rotation's axis as an octahedral direction in the low `2 x axisBits` bits and its angle (0 to pi, for the quaternion with w >= 0) in the `angleBits` bits above. A zero quaternion encodes as the identity (code 0).

The angle gets the most bits because many splats are rotated only slightly from the identity. For a small rotation, the axis error is scaled by sin(angle / 2), so the angle's step sets the precision: with 12 bits (a step of pi / 4095), rotations under 2 degrees decode within about 0.03 degrees. A splat aligned with a coordinate axis can also be stored as a small rotation from the identity, with its scales permuted.

## Shared-exponent color

`encodeExtRgb` finds the largest absolute channel, rounds its log2 up to an exponent `e` (stored as `e + 15`, 0 to 31), and stores each channel as an 8-bit magnitude in units of `2^e / 255`, with its sign in one of three bits. This keeps colors from about 2^-15 to 2^16, including the negative and above-1 values that merged LoD nodes and lighting produce. The `ext` spherical-harmonics codec stores each SH coefficient (an RGB triple) in one such word; [sh.md](sh.md#codecs) has its layout and the other SH codecs.

## Encoding: the CPU codecs

| Member | What it does |
|---|---|
| `encodeHalf(f)`, `decodeHalf(h)` | half-float bits, rounded to nearest even like the GPU's `packHalf2x16` |
| `encodeUnorm8(v)` | `round(v * 255)`, clamped to 0..255 |
| `encodeLodOpacity(o)`, `decodeLodOpacity(byte)` | 128 = 1.0, up to 255 |
| `encodeExtRgb(rgb)`, `decodeExtRgb(code, out)` | the shared-exponent color word |
| `encodeOct(v, bits)`, `decodeOct(code, bits, out)` | an octahedral direction, `bits` per axis (1 to 16) |
| `encodeQuatOct(q, axisBits, angleBits)`, `decodeQuatOct(code, axisBits, angleBits, out)` | a quaternion as an axis and an angle (at most 32 bits in all) |
| `encodeOct24` / `decodeOct24`, `encodeQuatOct32` / `decodeQuatOct32`, `encodeQuatOct24` / `decodeQuatOct24` | the fixed-size codecs, each built once with its constants |
| `floatView(u32)` | a `Float32Array` over the same memory (cached per array); ext centers are read through it |

The generic and fixed-size codecs give the same codes (the checks compare them).

## SplatMath: quaternions and covariances

| `SplatMath` member | What it does |
|---|---|
| `normalizeQuat(q, out)` | a unit quaternion (the identity for a zero one) |
| `covFromScaleQuat(scales, q, out)` | Σ = M Mᵀ with M = R(q) diag(scales) |
| `symEigens(cov)` | eigenvalues (descending) and eigenvectors (`basis`, columns, right-handed) of a symmetric 3x3 matrix, by Jacobi rotations with a relative tolerance |
| `basisToQuat(m, out)` | the quaternion (w >= 0) of an orthonormal basis given as columns |

Types: `Vec3`, `Quat`, `Rgb`, and `Cov` (`{ xx, yy, zz, xy, xz, yz }`).

## The shader twins

`GpuEncoding` has the same codecs on TSL nodes: `encodeHalf` / `decodeHalf`, `encodeHalf2` / `decodeHalf2` (two halves in one word), `encodeUnorm8`, `encodeLodOpacity` / `decodeLodOpacity`, `encodeExtRgb` / `decodeExtRgb`, `encodeOct` / `decodeOct`, `encodeQuatOct` / `decodeQuatOct`, and the fixed-size `encodeOct24`, `encodeQuatOct32`, and `encodeQuatOct24` with their decoders. Half floats use the GPU's `packHalf2x16` / `unpackHalf2x16`.

`GpuSplatMath` has `quatRotate(q, v)`, `quatMul(a, b)`, `quatConj(q)`, `mat3FromMat4(m)`, `scaleQuatMatrix(scales, q)`, `covFromScaleQuat(scales, q)`, `covMatrix(xxyyzz, xyxzyz)`, `symEigens(xxyyzz, xyxzyz)`, and `basisToQuat(basis)`.

## CPU and GPU

The CPU and TSL codecs produce the same bits for the same inputs, except where float32 math on the GPU lands on the other side of a rounding step from the CPU's float64 math. In that case the code is one step off: under 2% of ext and packed splats, and about 7% of cov splats (those with correlations near 1). The checks require every such code to still decode within the field's budget ([checks.md](checks.md)).