This page follows Niantic’s library: load-spz.cc, which reads and writes
the format, and splat-utils.h, which holds the conversions. A file of
versions 1 to 3 is a single gzip member whose contents are laid out as below. All
numbers are little-endian.
The header
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | uint32 | magic | 0x5053474E, the bytes NGSP |
| 4 | 4 | uint32 | version | 1, 2 or 3 |
| 8 | 4 | uint32 | numPoints | The number of splats, at least 1 |
| 12 | 1 | uint8 | shDegree | The spherical harmonics degree, 0 to 3 (4 in the latest library) |
| 13 | 1 | uint8 | fractionalBits | The bits after the binary point in the positions; the library writes 12 |
| 14 | 1 | uint8 | flags | Bit 0 (0x01): the scene was trained with antialiasing |
| 15 | 1 | uint8 | reserved | 0 |
The attributes follow at offset 16, each for every splat before the next attribute starts. The reader checks that the data is exactly as long as the header says.
Positions
Versions 2 and 3 store each coordinate as a signed 24-bit integer, three bytes,
least significant first: x, y and z of splat 0, then of splat 1, and so on. The
coordinate is the integer divided by 2fractionalBits; with 12
bits that is a step of 1/4096 and a reach of ±2048. Version 1 stores three half
floats instead, 6 bytes a splat.
Alphas
One byte a splat: the opacity after the sigmoid, times 255. A reader that wants the PLY file’s value takes the inverse sigmoid, ln(a / (1 − a)), of a = byte / 255.
Colours
Three bytes a splat: red, green and blue. Each holds the degree 0 spherical harmonics
coefficient f_dc, scaled by 0.15 and centred: the coefficient is
(byte / 255 − 0.5) / 0.15. The base colour is then
0.5 + SH_C0 × coefficient, with SH_C0 = 0.2820948, as in the
PLY layout.
Scales
Three bytes a splat, one an axis: the natural logarithm of the scale, as byte / 16 − 10. Bytes 0 to 255 cover logarithms from −10 to about 5.9, a scale from 0.000045 to about 370.
Rotations
Versions 1 and 2 store 3 bytes a splat: the x, y and z of the normalised quaternion, each as byte / 127.5 − 1. The writer first makes w positive, so the reader takes w = √(1 − x² − y² − z²).
Version 3 stores 4 bytes a splat, a 32-bit word in the “smallest three” form, which is more precise:
31–30Largest which of x, y, z, w (0 to 3) is left out2 bits29–20First kept component sign bit, then 9 bits of magnitude10 bits19–10Second kept component sign bit, then 9 bits of magnitude10 bits9–0Third kept component sign bit, then 9 bits of magnitude10 bits
The kept components are in the order x, y, z, w, leaving out the largest. Each has a sign in its top bit and a magnitude m in the other nine, meaning m / 511 × √½. The writer negates the quaternion first if the largest component is negative, so the reader takes the largest as the positive √(1 − the sum of the other three squared).
Spherical harmonics
When the degree is 1 or more, each splat has 3, 8, 15 or 24 coefficients a channel for degree 1, 2, 3 or 4, so 9, 24, 45 or 72 bytes. Unlike the PLY layout, the channel varies fastest: red, green and blue of coefficient 1, then red, green and blue of coefficient 2, and so on, in the coefficient order of the PLY layout. Each byte v stands for (v − 128) / 128. The writer keeps 5 bits of precision for degree 1 and 4 bits for the higher degrees by default, rounding the low bits away, which helps the compression.
Coordinates
The library’s own coordinate system is right, up, back (RUB): +Y up, the convention of OpenGL and three.js. Its functions convert from and to other systems when asked, including the right, down, forward system of the training code’s PLY files; when no system is given, no conversion is done.
Version 4
Version 4 drops gzip. The file starts with a 32-byte header in the clear: the same first 15 bytes as above with version 4, then the number of streams at offset 15 (usually 6), the offset of a table of contents at 16 (uint32) and 12 reserved bytes. When flag bit 1 (0x02) is set, extension records follow the header, each with a type and a length, so a reader can skip the ones it does not know. The table of contents has one entry a stream, two uint64 each: the compressed and the uncompressed size. The streams follow, one an attribute in the order above, each compressed with Zstandard on its own.