This page describes the compressed PLY as the PlayCanvas engine decodes it
(gsplat-compressed-data.js) and splat-transform writes it
(write-compressed-ply.ts and compressed-chunk.ts). Splat
i belongs to chunk ⌊i / 256⌋; the last chunk can hold fewer
than 256 splats.
The header
ply
format binary_little_endian 1.0
comment Generated by splat-transform …
element chunk 4153
property float min_x
property float min_y
property float min_z
property float max_x
property float max_y
property float max_z
property float min_scale_x
property float min_scale_y
property float min_scale_z
property float max_scale_x
property float max_scale_y
property float max_scale_z
property float min_r
property float min_g
property float min_b
property float max_r
property float max_g
property float max_b
element vertex 1063091
property uint packed_position
property uint packed_rotation
property uint packed_scale
property uint packed_color
element sh 1063091
property uchar f_rest_0
…
property uchar f_rest_44
end_header
f_rest_1 to f_rest_43 left out here.splat-transform writes at most three bands of harmonics: 3, 8 or 15 coefficients a
channel, so the sh element has 9, 24 or 45 properties, or is left out.
chunk: the ranges
| Offset | Properties | Meaning |
|---|---|---|
| +0 | min_x, min_y, min_z | The smallest position on each axis |
| +12 | max_x, max_y, max_z | The largest position on each axis |
| +24 | min_scale_x, min_scale_y, min_scale_z | The smallest log scale on each axis, clamped to −20 to 20 |
| +36 | max_scale_x, max_scale_y, max_scale_z | The largest log scale on each axis, clamped to −20 to 20 |
| +48 | min_r, min_g, min_b | The smallest base colour, 0.5 + SH_C0 × f_dc, of each channel |
| +60 | max_r, max_g, max_b | The largest base colour of each channel |
The colour ranges are absent in files with 12 floats a chunk; their colour bytes are then the colour itself, from 0 to 1.
vertex: four packed words
Each word is a uint, little-endian. A field of b bits holds
an unsigned value v, which stands for the fraction
t = v / (2b − 1) of its range, and the
value is min × (1 − t) + max × t. The
writer rounds: v = ⌊t × (2b − 1) + 0.5⌋.
+0packed_position x in bits 31–21, y in 20–11, z in 10–04 bytes+4packed_rotation largest component in bits 31–30, then three of 10 bits4 bytes+8packed_scale x in bits 31–21, y in 20–11, z in 10–04 bytes+12packed_color red, green, blue, opacity, 8 bits each from the top4 bytes
Position
packed_position holds x in its top 11 bits, y in the next 10 and z in
the low 11. Each is a fraction of the chunk’s min_ to max_
range on that axis.
Rotation
The rotation is the quaternion rot_0 to rot_3
(w, x, y, z), normalised and stored by
the “smallest three” method. The writer finds the component with the largest absolute
value, negates the whole quaternion if that component is negative (a quaternion and
its negative are the same rotation), and leaves the largest out. Bits 31–30 hold its
index: 0 for w, 1 for x, 2 for y, 3 for
z. The other three follow in that order in bits 29–20, 19–10 and 9–0, each
as v = ⌊(q × √2 / 2 + 0.5) × 1023 + 0.5⌋.
To decode, each 10-bit value gives (v / 1023 − 0.5) × √2, which lies between −0.7071 and 0.7071, and the largest component is √(1 − a² − b² − c²).
Scale
packed_scale has the same 11, 10 and 11 bits as the position. Each is a
fraction of the chunk’s log scale range on that axis; the scale is e to the power of
the value, as in the original
layout.
Colour and opacity
packed_color holds red in bits 31–24, green in 23–16, blue in 15–8 and
opacity in 7–0. Each byte v stands for v / 255. The three
colours are fractions of the chunk’s colour ranges, which already include the
0.5 + SH_C0 × f_dc step, so the result is the base colour.
The opacity byte is not scaled by any range: it is the opacity after the sigmoid,
sigmoid(opacity) × 255.
sh: spherical harmonics in bytes
The sh element has one item a splat, of uchar properties
f_rest_0 on, in the order of the
original layout: all of red’s
coefficients, then green’s, then blue’s. The writer stores a coefficient
c as ⌊(c / 8 + 0.5) × 256⌋, clamped to 0 to 255, so the range
−4 to 4 fits a byte. The PlayCanvas decoder reads a byte v back as
v × 8 / 255 − 4.
Order of the splats
splat-transform sorts the splats along a Morton (Z-order) curve before it makes the chunks, so the 256 splats of a chunk lie close together and their ranges are small. The last chunk is filled out for its ranges by repeating its last splat, but only the real splats are written to the vertex and sh elements.