Splats · interface

SplatData

A capture packed for the GPU, plus the two things the CPU still needs.

Explained in Gaussian splats.

interface SplatData
import type { SplatData } from '@driftengine/splats';

In depth

positions survives packing because the sorter reads it every time the view turns, and it is the only per-splat attribute that does. Everything else is in packed and is never looked at again by JavaScript.

This takes ownership of the arrays handed to packSplats. They are twelve and four bytes a splat, so copying a million-splat capture would be sixteen megabytes of duplicate to no purpose; the cost is that a caller must not go on mutating a source after packing it, which is stated here because nothing enforces it.

Properties

NameTypeDescription
countreadonlynumber
positionsreadonlyFloat32ArrayThree per splat. See above: the sorter's input, and the reason this is not packed away.
packedreadonlyUint32ArraywordsPerSplat per splat — texel 0 is position and colour, texel 1 is the covariance, and texel 2 is the l=1 band where the capture carries one.
wordsPerSplatreadonlynumberSPLAT_WORDS or SPLAT_WORDS_SH1. Everything that indexes packed reads this rather than a constant, and FORMAT.md §4.7 anticipated exactly this: a SPLT block already carries its own wordsPerSplat and says "a capture that carries spherical-harmonic coefficients later fits by raising it and nothing else".
boundsMinreadonlyFloat32ArrayThe capture's axis-aligned extent, for the frustum test that precedes a sort.
boundsMaxreadonlyFloat32Array
extentsreadonlyFloat32ArrayOne per splat: the largest of its three standard deviations, in the capture's own units.
More

What the budget ranks by, and the second attribute that survives packing. A splat's contribution to the picture is roughly how many pixels it covers, which is its world extent over its distance to the camera; the distance is per-frame and this is the half that is not. The largest sigma bounds the projected radius from above for every orientation, so it ranks correctly without a per-splat projection.

What it costs is four bytes a splat — four megabytes at a million, on top of the twelve positions already keeps — and it is paid whether or not a capture is ever budgeted, because packing cannot know. What would make it wrong is a capture of extremely flat splats seen edge-on, where the largest sigma is a poor estimate of the pixels covered and this over-ranks them; the honest fix there is the projected radius, which is per-frame work this deliberately avoids.

sphericalHarmonicsreadonlynumberHow many f_rest_* coefficients the source carried, of which this kept the first band.
More

Zero for a format that has none. Nine are read as of 2026-08-27 and the rest are not, and the costing that decided which is below, kept because it is the argument against raising it.

What view-dependent colour costs, costed 2026-08-27 and built the same day

A splat with no harmonics is SPLAT_WORDS uint32s — two RGBA32UI texels, 32 bytes — and the vertex stage runs six times a splat, because the quad is two unindexed triangles and SPLAT_VERT says why. Every invocation fetches every texel, so the data texture is read at 192 bytes a splat a frame before any texture cache.

A degree-n expansion carries ((n+1)^2 − 1) * 3 coefficients:

Degree Coefficients Extra texels Bytes a splat Read a splat a frame
0 0 0 32 192
1 (built) 9 1 48 288
2 24 3 80 480
3 45 6 128 768

Against the mobile budget of SPLAT_BUDGET_DEFAULT, 400,000 splats: 76.8 MB a frame at degree 0, 115.2 at degree 1, 192 at degree 2 and 307.2 at degree 3. docs/CAPABILITIES.md records a mid-range phone already moving 388.7 MB a frame of attachment traffic, so degree 3 would very nearly double the memory traffic of a frame on the device that is already the constraint — and the texture itself would grow from 12.8 MB to 51 MB, which is the mistake lightBudget.ts's shadow pool records making once already.

Degree 1 is the band that earns its texel. The l=1 lobe is broad and directional — it is what makes glass, a wet surface and a polished floor read as themselves — where degrees 2 and 3 are the sharp specular detail that costs four and six times as much. Nine coefficients fit one texel as bytes with a per-splat scale beside them, rather than halves: nine halves is 18 bytes and would not fit, and a per-splat scale spends the sixteenth byte on the accuracy that quantising to a capture-wide range would have thrown away.

What is still unmeasured, and it is the number that would refine this: how much of the 6x amplification a real texture cache absorbs, since six consecutive invocations read the same texels. Answering it needs a GPU timing of the splat pass against a capture, and this checkout has none — demo/dev/public/ being uncommitted.

shDegreereadonly0 | 1How many bands of that expansion this capture actually carries: 0 or 1.
More

Separate from sphericalHarmonics, which counts what the source had — a .ply written at degree 3 reports 45 there and 1 here, because forty-five coefficients arrived and nine were kept. The two disagreeing is the honest state rather than a discrepancy.