The data

Radix stores 3D Gaussian splats in tables of typed arrays. A table has the same layout and bits on the CPU, in workers, in files, and on the GPU.

This page covers tables, formats, texture layout, splat layouts, typed access, writers, double buffering, workers, and files. The widgets run JavaScript ports of the codecs in core/.

sections below ↓
01 · The data model

Data is stored in tables

An ArrayData is a table. Each row is one element, such as a splat. Each column is a channel, one flat typed array. Channels that belong together form a named bundle, and all bundles have the same rows.

count
capacity
spare

Capacity is always a multiple of 4096. 4096 elements fill whole 1024-wide texture rows at every packing, so CPU rows and GPU texels line up.

// click a column header

Channel names: a channel's name says what its bits are (extA, covB, packed), and code finds splat data by those names and formats. A bundle definition is plain data, so it can be sent to a worker with postMessage or saved in a file header unchanged.

See also: ArrayData Bundles

02 · Column types

Formats and packing

A format is a scalar type with an optional component count, named like WebGPU vertex formats ("uint32x4"). packing puts 1, 2, or 4 elements in one texel. It only changes the GPU layout; CPU memory stays the same.

scalar
components
packing

See also: Formats

03 · On the GPU

One array texture per channel

On the GPU, GpuArrays holds the same table: each channel is a 2D array texture, a stack of equal layers that are 1024 texels wide. Width and height are powers of two, so a texel's address is found with shifts and masks only.

layer bitsy bits (log2 height)x bits (10)
packing
x     = t & 1023
y     = (t >> 10) & (height - 1)
layer = t >> (10 + log2(height))

Layout rule: the height is the largest power of two at or below half the number of rows (at most 4096), and there are 2 to 512 layers. Small data uses 2 to 4 layers. Past 4096 rows per layer, it adds one full layer at a time. The largest texture holds 231 texels.

See also: Texture layout TextureLayout

04 · Splats

Splat layouts: 16 or 32 bytes per splat

A Gaussian splat has a center, three scales, a rotation, a color, and an opacity. Ext splats add PBR fields: a normal, roughness, and metallic. Three layouts, extSplats, covSplats, and packedSplats, pack these into 32-bit words, and each layout's name is also the name of the bundle that holds it. Hover over a field for its description.

The layout is chosen at load time

Files decode directly into the requested layout. Packed splats take an optional encoding, the color and scale ranges of their 8-bit codes; without it, the defaults apply. The rest of the program is the same for every layout.

const data = await loadSplats(url, {
  layout: settings.lowMemory ? "packedSplats" : "extSplats",
  encoding: { rgbMin: -0.5, rgbMax: 1.5 }, // optional
});

// the same code for every layout, on the CPU:
const { bundle } = Splats.get(data);

// and on the GPU:
const arrays = GpuArrays.fromData(renderer, data);
const reader = GpuSplatReader.from(arrays);

Live encoder

The page runs the same color, opacity, and half-float encoders as core/encoding.ts.

Spherical harmonics

Bands 1 to 4 (up to 24 RGB coefficients) sit in an SH bundle beside the splats. Band 0 is the base color above. extSh stores each coefficient in one shared-exponent word, like the base color. packedSh stores a scale byte per band, then one signed byte per value. Neither needs a range per file. The page runs the same codecs as core/sh.ts.

HDR color: 8-bit magnitudes with three sign bits and a shared 5-bit exponent let colors go above 1 or below 0, which merged LoD nodes and lighting require. Opacity uses 128 for 1.0 so merged nodes can reach 255/128. The same word stores one spherical-harmonics coefficient in extSh. Palettes (RAD's clustered SH, SOG's) store each entry's texels side by side, and splats pick an entry by a 16-bit label (Spherical harmonics).

See also: Layouts How each field is stored

05 · Compact directions

Octahedral encoding maps the sphere to a square

Normals and rotation axes are stored octahedrally: the sphere is projected onto an octahedron and unfolded into a square. Drag on either view to change the direction. The encoder rounds each coordinate to its code, with no search, and gives the same code on the CPU and the GPU.

direction
the unfolded square, colored by direction: +Z in the middle, −Z folded out to the corners

As a rotation axis

Rotation bits (10 + 10 + 12): many splats are close to the identity rotation. For a small rotation the axis error is scaled by sin(angle / 2), so the angle's step determines the precision. With 12 bits, rotations under 2° decode within about 0.03°. Compare a small angle with a large one above.

See also: Octahedral directions Quaternions

06 · Reading and writing splats

The bundle's type determines the available functions

Splat access functions take the bundle as their first argument, with no wrapper object around it. TypeScript uses the bundle's type to decide which functions apply. GPU readers use the same names, and the TSL codecs produce the same bits as the CPU codecs.

the bundle is typed as

CPU (core/splats.ts)

GPU readers (gpu/splatAccess.ts)

Writer helpers


      

The CPU and GPU use the same layouts. Shaders use the GPU's half-float pack and unpack and its sine and cosine, and round with floor(x + 0.5). Where 32-bit GPU math falls on the other side of a rounding boundary than the CPU's 64-bit math, a code differs by one step.

See also: Splats on the CPU In shaders

07 · Writing from shaders

Writers: fragment, compute, and scatter

Shaders write arrays through a writer. A writer takes a TSL function, write(index, out), that fills one texel. The fragment writer draws quads and works on both backends. The compute writer in wgpu/ takes the same arguments and writes the same texels in one dispatch. For writes to arbitrary indices, WgpuArrayStore stores at any index from any kernel.


  
GpuArrayWriterWgpuArrayWriterWgpuArrayStore
backendWebGPU and WebGL2WebGPUWebGPU
runs asa render pass per layer, quads over rowsone dispatch, a thread per texelinside any WgpuCompute kernel
writestexels [at, at + count)the same texels, same indicesany index (scatter)
limits4 channels, 64 bytes per texel, renderable formats4 channels, storable formats, storage: truestorable formats, storage: true
countcount = max(count, at + count)set by the kernel

Either writer: arrays.write(writer, range) takes either writer, and writeBatch groups items by kind, so one batch can contain fragment and compute writers.

See also: GpuArrayWriter WgpuArrayWriter WgpuArrayStore

08 · Separate read and write buffers

Double buffering with swap

A shader cannot read and write the same texture in one pass. Instead, read from the front buffer, write the next frame into the back buffer, then call swap. Readers hold texture references, so bind them again after a swap.

arrays Aframe 1
arrays Bempty
reader → front (A) writer → back (B)

CPU: ArrayData.swap works the same way. When a worker hands arrays back, setFromTransferable adopts them in place, so references held elsewhere stay valid.

See also: Double buffering GpuArrays

09 · Off the main thread

Workers are created on demand

WorkerPool starts with zero workers and creates one only when a call needs it, up to its size. Arrays are passed in and out as transferables, so nothing is copied. In Node or Cloudflare, where there are no workers, the same call runs on the current thread.

the worker option

See also: Workers

10 · Loading and saving

Splat files

loadSplats reads PLY, SPZ, RAD, and RDX into the requested layout, extSplats by default (plus a lodTree bundle when the file has a tree), and writeSplats writes any layout to any of these formats. Files are recognised by their first bytes, and by the file name's extension when the bytes are not conclusive. Drop a file to detect its format. The file is read only in this browser page.

drop any file here

See also: Splat files

11 · The native format

RDX: the table, chunked and compressed

An RDX file is a header followed by chunks of rows, in one file or split across files for streaming. A whole file can be read by seeking: chunk i starts at the header's length plus the sizes of the chunks before it, so one HTTP Range request fetches any chunk.

header · count, chunkSize, bundles, chunk sizes
chunk 0 · rows 0–65535
chunk 1
chunk 2
…

Each channel's rows are byte-transposed before gzip: byte 0 of every element, then byte 1, and so on. Similar bytes end up next to each other and compress better: the high bytes of nearby half floats (their sign and exponent), the nearly constant alpha, and colors that change slowly.

The sizes on the left are measured. The page gzips 4096 elements of rgba (unorm8x4) and center (float16x3) both ways, one payload per channel as in an RDX chunk, with your browser's CompressionStream.

See also: RDX files

More

Related pages

NextThe pipelineHow splats in these tables are projected, given sort keys, sorted on the GPU or a worker, and drawn. OverviewHow it fits togetherThe classes and the data passed between them in one frame. ReferenceArrayData and splatsMembers and defaults, starting with ArrayData.