The pipeline

How splats get from a file to the screen. Each splat is projected into a few words with a sort key from its distance, sorted on the GPU in the same frame or on a worker, and drawn as a quad whose corners come from the vertex index.

This page covers the four projection methods, stereo, sort keys, radix sort, compaction, the WebGL2 path, and the draw. Radix uses three.js WebGPURenderer with TSL only, on WebGPU or the WebGL2 backend.

sections below ↓
01 · The pipeline

From file to screen

These are the steps from a file on disk to the screen, and back to the CPU or a file. Click a step or press play.


    

See also: What a render does Quickstart

02 · From 3D to the screen

Four projection methods

Before a splat is drawn, its 3D Gaussian is converted to 2D: a quad that covers it and a way to shade each pixel. Radix has four methods. Move the camera close and to the side: ewa linearizes the projection at the center and moves away from the true outline, while the ray methods stay on it (the dots are points of the splat's 3σ surface, projected).

ewa
the Jacobian at the center: a 2D Gaussian
3dgut
7 sigma points, projected
htgs
the exact outline (a dual quadric)
raygs
a 3D quad facing the eye

Ray methods: 3dgut, htgs, and raygs use their outline only to place the quad. Each pixel then finds where its ray passes closest to the splat's center in 3D, so the shading stays exact even when the outline is approximate.

See also: The four methods Generators

03 · What the draw reads

Packing into words

The projection writes each splat as a few uint32 words: first what both eyes share, measured from the head (the camera in mono), then what each eye needs. The words fill uint32x4 channels, with a uint32x2 last one when only one or two are left, and a separate RGBA8 texel holds the sort key. Hover over a field for its description.

bytes per splat, of the writers' 64
channels, of the writers' 4

See also: Words

04 · XR

Stereo: shared head data, per-eye bounds

In XR the camera is two eyes a few centimetres apart. A splat is read once and projected for both eyes. Its distance, color, opacity, and 3D shape come from the head and are stored once; only the 2D bound (or raygs' quad frame) is stored per eye. Increase the eye separation to see the bound shift in each eye's view.

See also: Head and eyes WebXR

05 · Back to front

Distance to sort key

Splats blend over each other, so they are drawn from far to near. The sort key is the distance's float32 bits, cut to 16, 24, or 32 bits (the renderer's sortBits, 24 by default) and flipped so ascending keys run back to front. Distances are never negative, so the sign bit is always 0 and is dropped at every width. The GPU computes the key in the projection, with the bit count as a uniform.

float32
32 bits
24 bits
16 bits

16-bit sort keys: cutting the float to its top 16 bits would leave 7 mantissa bits. Instead the 16-bit sort key keeps distances from 2-32 to 232 (6 exponent bits) with 10 mantissa bits, about 0.1% apart, and clamps the rest.

See also: Sort keys

06 · GPU sorting

Radix sort: one digit per pass

WgpuRadixSort sorts by one digit per pass, least significant first: count each digit (histogram), turn the counts into starting offsets (prefix scan), then move every key to its digit's next free slot (scatter). Each pass keeps the order of equal digits, so after the last pass the keys are fully sorted. This example uses 6-bit keys with 2-bit digits, so it takes 3 passes. The splat sort uses 8-bit digits and 2 to 4 passes, one per 8 sort key bits.

Stability: the colors show each key's starting position. Within a digit, keys keep their relative order in every pass. This makes LSD radix sort correct, and it makes the GPU order identical to the CPU's SortKeys.sort.

See also: WgpuRadixSort The GPU sort

07 · Visible splats only

Compaction

Splats culled in every eye get the invalid sort key. Before sorting, WgpuCompact keeps only the valid sort keys: an exclusive prefix sum of the valid flags gives each remaining splat its slot, in input order. The total becomes the instance count of an indirect draw, so culled splats are not drawn and nothing is read back.

See also: WgpuCompact

08 · The GPU sort and the WebGL2 fallback

On WebGL2, sort orders arrive late

With WebGPU, WgpuSplatSorter sorts in the same frame as the draw, for that frame's camera (sortMode: "gpu", the default there). On the WebGL2 fallback (sortMode: "cpu"), the camera's view computes the sort keys, reads them back, sorts them on a worker, and applies the order. The order arrives a few frames later. Sort keys depend only on the head's position, so the view re-sorts when the head has moved far enough (or when an object or the collection of splats has changed). Turning the head never requires a new order, unless a sortGuard dropped the splats outside the view.

// what autoSort does for the camera's view: one sort in flight,
// applied when it finishes
if (!sorting && camera.position.distanceTo(sortedAt) > threshold) {
  sorting = true;
  sortedAt.copy(camera.position);
  // sort keys on the GPU, a 4-byte readback per splat, a worker sort
  const order = await view.sort(camera);
  // draws the collection it was sorted for
  view.apply(order);
  sorting = false;
}

See also: The CPU sort Orders The multi-splat model

09 · No vertex attributes

A quad from four vertex indices

Each splat is a quad: two triangles over four corners. There are no vertex attributes, only one index buffer of 1024 quads, shared by every renderer: quad q is 4q + [0, 1, 2, 2, 1, 3]. One instance draws 1024 splats, because GPUs run an instance's vertices together and instances of 4 vertices would leave most of that work idle. The vertex shader's splat is instanceIndex × 1024 + vertexIndex / 4, and its corner comes from the two low bits of vertexIndex (bit 0 is x, bit 1 is y). Because the draw is indexed, the GPU's vertex cache shades the two shared corners once, so the vertex shader, the costly stage, runs 4 times per splat instead of 6. Below is one quad (q = 0); click an index buffer entry to select it.

quad 0 of the index buffer: [0, 1, 2, 2, 1, 3]
vertexIndex
const corner = vec2(v.bitAnd(1), v.shiftRight(1).bitAnd(1))
  .mul(2).sub(1);
// 

See also: The draw

10 · Usage in an app

A render loop

The app calls each part explicitly, so the code below reads top to bottom. Hover over a line to see which block it uses. The viewer at /build/examples/splats/index.html is this code with a panel and camera controls added.


      
loadSplats · GpuArraysa file into a table, then into textures
SplatCollection · SplatRendererwhat to draw, placed; the method and params
SplatProjectorwords and a sort key per splat, one dispatch per object
WgpuSplatSorterWebGPU: compact, sort, and indirect draw
view.sort · applythe WebGL2 fallback: sort keys on the GPU, a worker sort
SplatRenderera Mesh: one quad per splat, in order

See also: Rendering Recipes

More

Related pages

NextDrawing many splat objectsHow one sort draws many objects, and why that takes collections, views, versioned orders, and releases. OverviewHow it fits togetherThe classes and the data passed between them in one frame. ReferenceRenderingSplatCollection, SplatRenderer, and every option and default.