How Serialization and Deserialization of .splat Files Works in Supersplat
Supersplat serializes Gaussian splat data into a compact 32-byte binary format using the serializeSplat function in src/splat-serialize.ts, while deserialization is delegated to the external @playcanvas/splat-transform library which parses the same little-endian layout back into renderable geometry.
Supersplat is a PlayCanvas-based editor and viewer for 3D Gaussian Splatting models. Understanding the serialization and deserialization of .splat files is essential for developers integrating this compact binary format into custom pipelines or extending the editor's import/export functionality. The format stores transformed Gaussian data in a fixed-structure binary layout optimized for web delivery and real-time rendering.
The 32-Byte Binary Layout of .splat Files
Each Gaussian (splat) occupies exactly 32 bytes stored in little-endian format. The serializeSplat function in src/splat-serialize.ts writes these records sequentially to a Uint8Array buffer, with the SingleSplat helper preparing per-Gaussian data transformations before writing.
Field Structure and Byte Offsets
The binary record follows this strict memory layout:
| Byte Offset | Field | Type | Description |
|---|---|---|---|
| 0-11 | x, y, z |
float32 | World-space position (pre-transformed by palette and 180° Z-flip) |
| 12-23 | scale_0, scale_1, scale_2 |
float32 | Log-scale values converted to linear space |
| 24-26 | f_dc_0, f_dc_1, f_dc_2 |
uint8 | Base color after SH-C₀ conversion |
| 27 | opacity |
uint8 | Alpha after sigmoid transformation |
| 28-31 | rot_0, rot_1, rot_2, rot_3 |
uint8 | Quaternion components packed to [0, 255] range |
Data Transformations During Serialization
The writer applies specific mathematical transforms to optimize storage and rendering performance:
- Position: Values are already transformed by the palette matrix and the 180° Z-flip applied by the PLY exporter.
- Scale: Log-scale values are converted to linear space using
Math.exp()before writing to maximize precision for small values. - Color: Spherical Harmonics C₀ coefficients are converted via
(0.5 + SH_C0 * value) * 255and clamped to the[0, 255]range. - Opacity: The sigmoid function
1 / (1 + Math.exp(-opacity))maps the value to linear alpha, then scales to 8-bit. - Rotation: Quaternion components are packed into unsigned bytes using
value * 128 + 128to map[-1, 1]to[0, 255].
Implementing Serialization in src/splat-serialize.ts
The serializeSplat function constructs a Uint8Array of length totalGaussians * 32 and uses a DataView to write little-endian values. The function iterates over splat data, applying the transformations above to each Gaussian before writing to the buffer at the correct byte offset.
// From src/splat-serialize.ts (lines 110-147)
const result = new Uint8Array(totalGaussians * 32);
const dataView = new DataView(result.buffer);
// Position (little-endian float32)
dataView.setFloat32(off + 0, data.x, true);
dataView.setFloat32(off + 4, data.y, true);
dataView.setFloat32(off + 8, data.z, true);
// Scale (converted from log to linear)
dataView.setFloat32(off + 12, Math.exp(data.scale_0), true);
dataView.setFloat32(off + 16, Math.exp(data.scale_1), true);
dataView.setFloat32(off + 20, Math.exp(data.scale_2), true);
// Color (SH-C₀ conversion)
dataView.setUint8(off + 24, clamp((0.5 + SH_C0 * data.f_dc_0) * 255));
dataView.setUint8(off + 25, clamp((0.5 + SH_C0 * data.f_dc_1) * 255));
dataView.setUint8(off + 26, clamp((0.5 + SH_C0 * data.f_dc_2) * 255));
// Opacity (sigmoid)
dataView.setUint8(off + 27, clamp((1 / (1 + Math.exp(-data.opacity))) * 255));
// Rotation (quaternion packing)
dataView.setUint8(off + 28, clamp(data.rot_0 * 128 + 128));
dataView.setUint8(off + 29, clamp(data.rot_1 * 128 + 128));
dataView.setUint8(off + 30, clamp(data.rot_2 * 128 + 128));
dataView.setUint8(off + 31, clamp(data.rot_3 * 128 + 128));
The clamp helper ensures all uint8 values remain within the 0-255 range. The filesystem abstraction in src/io/write/writer.ts and src/io/write/index.ts handles the actual byte stream output.
Deserialization via @playcanvas/splat-transform
Supersplat does not implement its own binary parser. Instead, it delegates deserialization of .splat files to the external @playcanvas/splat-transform package. This library provides the Splat class that reconstructs GSplatData structures from the binary buffer.
When loading a file, the viewer instantiates Splat directly from the Uint8Array:
import { Splat } from '@playcanvas/splat-transform';
// Load binary buffer (e.g., from FileSystem abstraction)
const data = await fs.readFile('model.splat');
const splat = new Splat(data);
The splat-transform library reads the buffer in 32-byte strides, extracting fields in the same order defined by the Supersplat writer. It automatically applies the inverse of the 180° Z-flip to restore correct world-space orientation, and reverses the color, opacity, and quaternion transformations to recover the original floating-point values.
Complete Export-Import Cycle Example
The following demonstrates a round-trip using both the Supersplat serializer and the external deserializer:
import { serializeSplat } from './splat-serialize';
import { Splat } from '@playcanvas/splat-transform';
// Export: Write binary .splat file
await serializeSplat(splats, { /* SerializeSettings */ }, fileSystem);
// Import: Read back into GSplatData structure
const binary = await fileSystem.readFile('output.splat');
const loaded = new Splat(binary);
This round-trip is lossless because both sides use identical field ordering, little-endian byte alignment, and symmetric mathematical transformations for color, opacity, and rotation encoding.
Summary
- Binary Format: Each Gaussian occupies 32 bytes containing position (12 bytes), scale (12 bytes), color (3 bytes), opacity (1 byte), and rotation (4 bytes) in little-endian format.
- Serialization: The
serializeSplatfunction insrc/splat-serialize.tswrites data usingDataView, applyingMath.exp()to scales, sigmoid to opacity, and 8-bit packing to quaternions. - Deserialization: The
@playcanvas/splat-transformpackage parses the binary layout, automatically reversing transformations and the 180° Z-flip to restore original geometry. - Key Files: Writer logic resides in
src/splat-serialize.tsandsrc/io/write/, while parsing is handled by the external dependency imported in viewer code such assrc/pc-app.ts.
Frequently Asked Questions
What is the exact byte size per Gaussian in a .splat file?
Each Gaussian requires exactly 32 bytes. This includes 12 bytes for position (three float32), 12 bytes for scale (three float32), 3 bytes for base color (three uint8), 1 byte for opacity (uint8), and 4 bytes for rotation quaternion (four uint8).
Why does Supersplat use an external library for deserialization?
Supersplat delegates parsing to @playcanvas/splat-transform to maintain compatibility with the broader PlayCanvas ecosystem. This separation allows any PlayCanvas-based viewer using the same library to load files exported from Supersplat without implementing custom binary parsers.
How are rotations stored in the .splat binary format?
Rotations are stored as four unsigned 8-bit integers representing quaternion components. The serializer applies the transformation value * 128 + 128 to map the [-1, 1] range to [0, 255], while the deserializer reverses this calculation to recover the original floating-point quaternion values.
Is the .splat format lossless compared to PLY?
The format is designed for efficient rendering rather than archival precision. While position and scale use full float32 precision, color, opacity, and rotation are quantized to 8-bit values. This introduces minor quantization error compared to PLY's floating-point storage, but enables significantly smaller file sizes and faster GPU upload.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →