# How Serialization and Deserialization of .splat Files Works in Supersplat

> Learn how Supersplat serializes Gaussian splat data to a 32-byte binary format and deserializes using the @playcanvas/splat-transform library for efficient rendering.

- Repository: [PlayCanvas/supersplat](https://github.com/playcanvas/supersplat)
- Tags: how-to-guide
- Published: 2026-05-10

---

**Supersplat serializes Gaussian splat data into a compact 32-byte binary format using the `serializeSplat` function in [`src/splat-serialize.ts`](https://github.com/playcanvas/supersplat/blob/main/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`](https://github.com/playcanvas/supersplat/blob/main/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) * 255` and 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 + 128` to 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.

```typescript
// 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`](https://github.com/playcanvas/supersplat/blob/main/src/io/write/writer.ts) and [`src/io/write/index.ts`](https://github.com/playcanvas/supersplat/blob/main/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`:

```typescript
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:

```typescript
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 `serializeSplat` function in [`src/splat-serialize.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat-serialize.ts) writes data using `DataView`, applying `Math.exp()` to scales, sigmoid to opacity, and 8-bit packing to quaternions.
- **Deserialization**: The `@playcanvas/splat-transform` package 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.ts`](https://github.com/playcanvas/supersplat/blob/main/src/splat-serialize.ts) and `src/io/write/`, while parsing is handled by the external dependency imported in viewer code such as [`src/pc-app.ts`](https://github.com/playcanvas/supersplat/blob/main/src/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.