SPZ is a compressed file format for 3D Gaussian Splats, designed by Niantic. It provides efficient storage of Gaussian Splat data with configurable spherical harmonics degrees and coordinate system support.
See docs/SPZ_SPEC_v3.md for more information.
$ # install:
$ cargo install spz
$ # or
$ flatpak install io.github.jackneill.spz
$ # or
$ snap install spz
$
$ # run:
$ spz info assets/racoonfamily.spz
$ # or in container:
$ podman/docker run --rm -it -v "${PWD}:/app" -w /app spz \
$ info assets/racoonfamily.spz
GaussianSplat:
Number of points: 932560
Spherical harmonics degree: 3
Antialiased: true
Median ellipsoid volume: 0.0000000046213082
Bounding box:
x: -281.779541 to 258.382568 (size 540.162109, center -11.698486)
y: -240.000000 to 240.000000 (size 480.000000, center 0.000000)
z: -240.000000 to 240.000000 (size 480.000000, center 0.000000)[dependencies]
spz = { version = "0.0.7", default-features = false, features = [] }use spz::prelude::*;cargo run --example load_spz// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::path::PathBuf;
use anyhow::Result;
use spz::{
coord::CoordinateSystem,
gaussian_splat::GaussianSplat,
gaussian_splat::{LoadOptions, SaveOptions},
packed::PackedGaussians,
};
fn main() -> Result<()> {
let mut sample_spz = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
sample_spz.push("assets/racoonfamily.spz");
let gs = GaussianSplat::builder().load(sample_spz)?;
let pg = gs.to_packed_gaussians(
&SaveOptions::builder()
.coord_sys(CoordinateSystem::RightUpBack) // packed will be in RUB (OpenGL)
.build(),
)?;
let bytes = pg.as_bytes_vec()?;
let pg2 = PackedGaussians::from_bytes(bytes.as_slice())?;
let _gs2 = GaussianSplat::new_from_packed_gaussians(
&pg2,
&LoadOptions::builder()
.coord_sys(CoordinateSystem::LeftUpFront) // _gs2 will be in LUF (glTF)
.build(),
)?;
Ok(())
}- This outline is non-exhaustive.
// mod gaussian_splat ──────────────────────────────────────────────────────────
pub struct GaussianSplatBuilder { /* ... */ }
impl GaussianSplatBuilder {
pub fn packed(self, packed: bool) -> Result<Self>;
pub fn load_options(self, opts: LoadOptions) -> Self;
pub fn load<P: AsRef<Path>>(self, filepath: P) -> Result<GaussianSplat>;
pub async fn load_async<P: AsRef<Path>>(self, filepath: P) -> Result<GaussianSplat>;
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Arbitrary)]
pub struct GaussianSplat {
pub num_points: i32,
pub spherical_harmonics_degree: i32, // 0-3
pub antialiased: bool,
pub positions: Vec<f32>, // flattened: [x0, y0, z0, x1, y1, z1, ...]
pub scales: Vec<f32>, // flattened: [x0, y0, z0, x1, y1, z1, ...], log-scale
pub rotations: Vec<f32>, // flattened: [x0, y0, z0, w0, x1, y1, z1, w1, ...]
pub alphas: Vec<f32>, // opacity (sigmoid-encoded)
pub colors: Vec<f32>, // flattened: [r0, g0, b0, r1, g1, b1, ...], DC color
pub spherical_harmonics: Vec<f32>, // SH coefficients (degrees 1-3)
}
impl GaussianSplat {
// Construction & Loading
pub fn builder() -> GaussianSplatBuilder;
pub fn load_packed_from_file<F: AsRef<Path>>(filepath: F, opts: &UnpackOptions) -> Result<Self>;
pub fn load_packed<D: AsRef<[u8]>>(data: D) -> Result<PackedGaussians>;
pub fn new_from_packed_gaussians(packed: &PackedGaussians, opts: &UnpackOptions) -> Result<Self>;
// Serialization
pub fn save_as_packed<F: AsRef<Path>>(&self, filepath: F, opts: &PackOptions) -> Result<()>;
pub fn serialize_as_packed_bytes(&self, opts: &PackOptions) -> Result<Vec<u8>>;
pub fn to_packed_gaussians(&self, opts: &PackOptions) -> Result<PackedGaussians>;
// Transforms
pub fn convert_coordinates(&mut self, from: CoordinateSystem, to: CoordinateSystem);
// Introspection
pub fn bbox(&self) -> BoundingBox;
/// Compute median ellipsoid volume.
pub fn median_volume(&self) -> f32;
/// Validates that all internal arrays have consistent sizes.
pub fn check_sizes(&self) -> bool;
}
pub struct LoadOptions {
pub coord_sys: CoordinateSystem,
}
impl LoadOptions {
pub fn builder() -> LoadOptionsBuilder;
}
pub struct LoadOptionsBuilder {
coord_sys: CoordinateSystem,
}
impl LoadOptionsBuilder {
pub fn coord_sys(mut self, coord_sys: CoordinateSystem) -> Self;
pub fn build(self) -> LoadOptions;
}
pub struct SaveOptions {
pub coord_sys: CoordinateSystem,
}
impl SaveOptions {
pub fn builder() -> SaveOptionsBuilder;
}
pub struct SaveOptionsBuilder {
coord_sys: CoordinateSystem,
}
impl SaveOptionsBuilder {
pub fn coord_sys(mut self, coord_sys: CoordinateSystem) -> Self;
pub fn build(self) -> SaveOptions;
}
pub struct BoundingBox {
pub x_min: f32, pub x_max: f32,
pub y_min: f32, pub y_max: f32,
pub z_min: f32, pub z_max: f32,
}
impl BoundingBox {
pub fn size(&self) -> (f32, f32, f32); // (width, height, depth)
pub fn center(&self) -> (f32, f32, f32); // (x, y, z)
}
// mod coord ───────────────────────────────────────────────────────────────────
pub enum CoordinateSystem {
Unspecified = 0,
/* LDB */ LeftDownBack = 1,
/* RDB */ RightDownBack = 2,
/* LUB */ LeftUpBack = 3,
/* RUB */ RightUpBack = 4, // SPZ Internal, Three.js coordinate system
/* LDF */ LeftDownFront = 5,
/* RDF */ RightDownFront = 6, // PLY coordinate system
/* LUF */ LeftUpFront = 7, // GLB coordinate system
/* RUF */ RightUpFront = 8, // Unity coordinate system
}
impl CoordinateSystem {
/// Computes the axis flip multipliers needed to convert from `self` to `target`.
pub fn axis_flips_to(self, target: CoordinateSystem) -> AxisFlips;
/// Compares axis orientations between two coordinate systems.
pub fn axes_align(self, other: CoordinateSystem) -> (bool, bool, bool);
/// Returns a short 3-letter abbreviation for the coordinate system.
pub fn as_short_str(&self) -> &'static str;
/// Returns an iterator over all coordinate system variants.
pub fn iter() -> impl Iterator<Item = CoordinateSystem>;
}
/// Sign multipliers (+1.0 or -1.0) for transforming Gaussian splat data between
/// coordinate systems.
pub struct AxisFlips {
/// Sign multipliers for XYZ position coordinates.
pub position: [f32; 3],
/// Sign multipliers for quaternion rotation components (X, Y, Z; W is unchanged).
pub rotation: [f32; 3],
/// Sign multipliers for spherical harmonic coefficients (15 values for degrees 1-3).
pub spherical_harmonics: [f32; 15],
}
// mod packed ──────────────────────────────────────────────────────────────────
/// Represents a full splat with lower precision.
pub struct PackedGaussians {
// Total number of points (gaussians).
pub num_points: i32,
// Degree of spherical harmonics.
pub sh_degree: i32,
// Number of bits used for fractional part of fixed-point coords.
pub fractional_bits: i32,
// Whether gaussians should be rendered with mip-splat antialiasing.
pub antialiased: bool,
// Whether gaussians use the smallest three method to store quaternions.
pub uses_quaternion_smallest_three: bool,
pub positions: Vec<u8>,
pub scales: Vec<u8>,
pub rotations: Vec<u8>,
pub alphas: Vec<u8>,
pub colors: Vec<u8>,
pub spherical_harmonics: Vec<u8>,
}
impl PackedGaussians {
/// Constructs an SPZ header from this packed data's metadata.
pub fn construct_header(&self) -> PackedGaussiansHeader;
/// Serializes to a complete SPZ file as a byte vector.
pub fn as_bytes_vec(&self) -> Result<Vec<u8>>;
/// Writes this packed data to a writer in SPZ format.
pub fn write_self_to<W>(&self, stream: &mut W) -> Result<()>;
/// Returns `true` if positions are stored as float16.
pub fn uses_float16(&self) -> bool;
/// Returns the packed data for a single splat at index `i`.
pub fn at(&self, i: usize) -> Result<PackedGaussian>;
/// Unpacks a single splat at index `i` with coordinate transformation.
pub fn unpack(&self, i: usize, coord_flip: &AxisFlips) -> Result<UnpackedGaussian>;
/// Validates that all internal arrays have the expected sizes.
pub fn check_sizes(&self, num_points: usize, sh_dim: u8, uses_float16: bool) -> bool;
}
impl TryFrom<Vec<u8>> for PackedGaussians;
impl TryFrom<&[u8]> for PackedGaussians;
/// Represents a single low precision gaussian.
pub struct PackedGaussian {
pub position: [u8; 9],
pub rotation: [u8; 4],
pub scale: [u8; 3],
pub color: [u8; 3],
pub alpha: u8,
pub sh_r: [u8; 15],
pub sh_g: [u8; 15],
pub sh_b: [u8; 15],
}
impl PackedGaussian {
/// Decompresses this packed Gaussian into full-precision values,
/// [`UnpackedGaussian`].
pub fn unpack(
&self,
uses_float16: bool,
uses_quaternion_smallest_three: bool,
fractional_bits: i32,
coord_flip: &AxisFlips,
) -> Result<UnpackedGaussian>;
}
// mod unpacked ────────────────────────────────────────────────────────────────
/// Represents a single inflated gaussian.
pub struct UnpackedGaussian {
pub position: [f32; 3], // x, y, z
pub rotation: [f32; 4], // x, y, z, w
pub scale: [f32; 3], // std::log(scale)
pub color: [f32; 3], // rgb sh0 encoding
pub alpha: f32, // inverse logistic
pub sh_r: [f32; 15],
pub sh_g: [f32; 15],
pub sh_b: [f32; 15],
}
// mod header ──────────────────────────────────────────────────────────────────
/// Fixed-size 16-byte header for SPZ (packed Gaussian splat) files.
#[repr(C)]
pub struct PackedGaussiansHeader {
pub magic: i32, // 0x5053474e "NGSP"
pub version: i32, // 3 (this lib only supports spz v3)
pub num_points: i32,
pub spherical_harmonics_degree: u8, // 0-3
pub fractional_bits: u8,
pub flags: u8, // 0x1 = antialiased
pub reserved: u8,
}
impl PackedGaussiansHeader {
/// Reads a header from the given reader.
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self>;
/// Writes this header to the given writer.
pub fn serialize_to<W: Write>(&self, writer: &mut W) -> Result<()>;
}- Install
nextestrunner. - For fuzz testing:
cargo install cargo-fuzz- Further documentation is available in fuzz/README.md.
- Install cargo-mutants for test insights.
cargo install cargo-mutants
just test
just fuzz
just mutantscargo install cargo-criterion- Install
gnuplotfor html reports.
just bench- The html report of the benchmark can be found under
./target/criterion/report/index.html. - View Benchmark and Profiling data on CodSpeed, (from CI runs).
- Install the
moldlinker: https://github.com/rui314/mold
uvx pip install spz# pyproject.toml
[project]
dependencies = [
"spz",
]import numpy as np
import spz
# Load from file
splat = spz.load("scene.spz") # -> GaussianSplat
# or
splat = spz.GaussianSplat.load(
"scene.spz", coordinate_system=spz.CoordinateSystem.RUB
) # -> GaussianSplat
# or
with spz.SplatReader("scene.spz") as ctx:
splat2 = ctx.splat # -> GaussianSplat
with spz.temp_save(splat) as tmp_path:
import subprocess
subprocess.run(["viewer", str(tmp_path)])
# Access properties
print(f"{splat.num_points:,} points")
print(f"center: {splat.bbox.center}")
print(f"size: {splat.bbox.size}")
# Access data as numpy arrays
positions = splat.positions # shape: (num_points, 3)
scales = splat.scales # shape: (num_points, 3)
rotations = splat.rotations # shape: (num_points, 4)
alphas = splat.alphas # shape: (num_points,)
colors = splat.colors # shape: (num_points, 3)
sh = splat.spherical_harmonics # shape: (num_points, sh_dim * 3)
# Serialize
data = splat.to_bytes() # -> bytes
splat2 = spz.GaussianSplat.from_bytes(data) # -> GaussianSplat
# Create from numpy arrays
new_splat = spz.GaussianSplat(
positions=np.zeros((2, 3), dtype=np.float32),
scales=np.full((2, 3), -5.0, dtype=np.float32),
rotations=np.tile([1.0, 0.0, 0.0, 0.0], (2, 1)).astype(np.float32),
alphas=np.array([0.5, 0.8], dtype=np.float32),
colors=np.array([[255.0, 0.0, 0.0], [0.0, 255.0, 0.0]], dtype=np.float32),
) # -> GaussianSplat
# Save to file
new_splat.save("output.spz")
with spz.SplatWriter("output2.spz") as writer:
writer.splat = splat2
# Coordinate conversion
with spz.modified_splat("scene.spz", "scene_converted.spz") as splat:
splat.convert_coordinates(spz.CoordinateSystem.RUB, spz.CoordinateSystem.RDF)- Documentation for the C bindings can be found in
./crates/spz-capi/README.md.
Further documentation is available under ./docs.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
SPDX-License-Identifier: Apache-2.0
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
SPDX-License-Identifier: MIT
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.