QAT-3DGS (vanilla Inria 3DGS PLY)

The splatforge-qat-3dgs preset is a lossless single-PLY codec for vanilla Inria 3DGS Gaussian-Splat PLYs — the de-facto consumer format produced by Luma, Polycam, Scaniverse, and the official graphdeco-inria/gaussian-splatting trainer (62 fp32 columns per vertex: x/y/z, nx/ny/nz, f_dc_0..2, f_rest_0..44, opacity, scale_0..2, rot_0..3).

The codec scans every per-vertex column for a constant value (within fp32 epsilon), drops constant columns from the PLY body, and stores them as comment constant_field <name> <hex> header lines instead. The transform is fully reversible — the encoder decodes its own output in-process and asserts a bit-exact match against the source before emitting the file. Non-lossless outputs can never escape: if the round-trip check fails the encoder errors and deletes the output rather than ship it.

What gets stripped (and why ~5%)

On every Inria 3DGS PLY we have measured, the canonical nx, ny, nz normals columns are initialised to zero and never updated by the Inria trainer — the Inria renderer doesn't read them. Those three fp32 columns are exactly constant (span = 0.0) and collapse to a single hex header line each. On the 287 MB bonsai PLY this saves ~14 MB / 4.84% of file size bit-exact.

Some scenes also yield a few f_rest_* SH coefficients that happen to be exactly zero (low-frequency channels in SH-degree-0/1 captures); the codec picks those up automatically without a schema change.

What's NOT in this tier

The f_rest_0..44 SH coefficients dominate Inria 3DGS file size — 45 fp32 channels = 180 bytes/vertex = ~73% of the 248-byte per-vertex footprint. The full ~55% projected save requires int8 QAT of those SH coefficients, which depends on a retrain leg with GT cameras + images to absorb feature-quant noise via L1+SSIM loss (same recipe as the QAT-Bundle tier for Scaffold-GS). Naive post-hoc int8 of f_rest_* with no retrain destroys render quality the same way naive post-hoc int8 of f_anchor_feat lost 14.5 dB on the Scaffold side.

That tier ships separately as splatforge-qat-3dgs-bundle — pack a tar with point_cloud.ply + cameras.json + images/ (the same shape as QAT-Bundle) and the endpoint runs a 5000-iter int8 QAT finetune on A100. See the table on /bench for the SH-int8 QAT-3DGS-Bundle numbers once we land the multi-scene validation.

End-to-end smoke (one scene)

The deployed Modal /qat-3dgs route was validated against the canonical Inria 3DGS bonsai PLY:

FieldValue
scenebonsai (Mip-NeRF 360, Inria 3DGS iter 7k)
n_vertices1,157,141
n_props_in / out62 → 59
size_bytes_in286.97 MB
size_bytes_out273.09 MB
ply_save_pct4.84%
delta_psnr_db0.00 (lossless by construction)
constants_detectednx, ny, nz (all exactly 0.0)
bitexact_roundtriptrue

What happens server-side

  1. Browser uploads the PLY to Vercel Blob via a presigned PUT.
  2. The worker validates the preset and forwards { preset: "splatforge-qat-3dgs", blob_url, callback_url } to the private Modal /qat-3dgs endpoint.
  3. The endpoint validates the PLY layout (must have x/y/z + f_dc_0..2 + opacity + scale_0..2 + rot_0..3; Scaffold-GS f_anchor_feat_*/f_offset_* PLYs are rejected with a redirect to splatforge-qat-scaffold).
  4. Every per-vertex column is scanned for a constant value (|max - min| ≤ 1e-6 in fp32). Constant columns are removed from the body and emitted as header comments.
  5. The encoder decodes its own output in-process and asserts np.array_equal per column against the source. If any column differs the output is deleted and the encoder raises — non-lossless emits are impossible.
  6. The compressed PLY is uploaded to Vercel Blob and returned via the callback.

API callback shape

{
  "status": "done",
  "output_url": "https://...vercel-storage.com/jobs/<id>/scene_qat3dgs.ply",
  "size_bytes_in": 286968700,
  "size_bytes_out": 273086276,
  "ply_save_pct": 4.84,
  "delta_psnr_db": 0.0,
  "lossless": true,
  "preset": "splatforge-qat-3dgs",
  "n_vertices": 1157141,
  "constants_detected": ["nx", "ny", "nz"],
  "constant_values": {"nx": 0.0, "ny": 0.0, "nz": 0.0},
  "bitexact_roundtrip": true
}

Reader compatibility

The encoded PLY remains a valid PLY file; any decoder that understands the comment constant_field <name> <hex> convention round-trips bit-identically. Legacy decoders that don't honor the header comments will see a 59-column PLY instead of 62 — harmless for the Inria renderer (which ignores nx/ny/nz) but cosmetic-only renderers may produce undefined behavior. We recommend the SplatForge plugin for proper round-trip; the canonical loader patch in splatforge-private/external/scaffold-gs/scene/gaussian_model.py is the reference implementation.

← back to Try it  ·  QAT-Bundle (Scaffold-GS full retrain)  ·  QAT-Scaffold (Scaffold-GS single-PLY)  ·  SplatBench