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:
| Field | Value |
|---|---|
scene | bonsai (Mip-NeRF 360, Inria 3DGS iter 7k) |
n_vertices | 1,157,141 |
n_props_in / out | 62 → 59 |
size_bytes_in | 286.97 MB |
size_bytes_out | 273.09 MB |
ply_save_pct | 4.84% |
delta_psnr_db | 0.00 (lossless by construction) |
constants_detected | nx, ny, nz (all exactly 0.0) |
bitexact_roundtrip | true |
What happens server-side
- Browser uploads the PLY to Vercel Blob via a presigned PUT.
-
The worker validates the preset and forwards
{ preset: "splatforge-qat-3dgs", blob_url, callback_url }to the private Modal/qat-3dgsendpoint. -
The endpoint validates the PLY layout (must have
x/y/z+f_dc_0..2+opacity+scale_0..2+rot_0..3; Scaffold-GSf_anchor_feat_*/f_offset_*PLYs are rejected with a redirect tosplatforge-qat-scaffold). -
Every per-vertex column is scanned for a constant value
(
|max - min| ≤ 1e-6in fp32). Constant columns are removed from the body and emitted as header comments. -
The encoder decodes its own output in-process and asserts
np.array_equalper column against the source. If any column differs the output is deleted and the encoder raises — non-lossless emits are impossible. - 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