PAJAMA / BUILD
← Engineering series

05 / 12 · Build the splat

Turn a rectangle into a soft splat

Implement Gaussian falloff, premultiplied alpha and order-dependent composition.

The rectangle only decides where fragments may run. The Gaussian function decides how much each fragment contributes. Separating those two ideas explains both the soft shape and why large splats can be expensive.

1. Put a local coordinate on the rectangle

The vertex shader assigns local coordinates from −3 to +3 along both axes. The rasterizer interpolates them. At the center, local = (0, 0); one standard deviation to the right is (1, 0). These coordinates describe distance inside this Gaussian, not meters in the scene.

2. Calculate one fragment by hand

Square the two coordinates, add them, multiply by −1/2 and exponentiate. At (0,0) the weight is 1. At (1,0) it is about 0.6065. With base opacity 0.8, alpha is about 0.4852 there. Stage 3 now reveals a soft orange splat inside the same two triangles. We discard alpha below 1/255 and cap it at 0.99, matching the teaching baseline.

g(u,v)=e−12(u2+v2),α=o g,Cshader=α cg(u,v)=e^{-\frac12(u^2+v^2)},\qquad\alpha=o\,g,\qquad C_{\mathrm{shader}}=\alpha\,c

3. Pre-multiply exactly once

The shader returns (alpha × RGB, alpha), so the pipeline uses PREMULTIPLIED_ALPHA_BLENDING. New foreground color replaces a fraction of the old background: C = alpha × foreground + (1 − alpha) × background. Returning straight RGB into that blend rule makes transparent edges too bright; multiplying again in the blend makes them too dark.

4. Reverse two splats and predict the color

Stage 4 draws a far blue-tinted splat (0, 0.25, 1) then a near red splat (1, 0, 0), each with center opacity 0.5. Over black, their ideal center RGB is (0.5, 0.0625, 0.25). Reverse the order and it becomes (0.25, 0.125, 0.5). Pixel centers rarely coincide exactly with the mathematical center, so measured pixels differ slightly.

This path draws back to front and lets fixed-function blending compose colors. The common compute rasterizer instead walks sorted splats front to back while accumulating transmittance. Both can express the same over operation, but their loop direction and update equations must agree.

RUN → OBSERVE

Make this step concrete

Stage 3: one soft orange splat. Stage 4: overlapping red and blue; reversing order changes the overlap color.

Open GPU milestone 4 →
Terminal · repository root
cargo run --locked --example workshop -- 3
# Press 4, or open stage 4 online and toggle Reverse draw order.
cargo run --locked --example workshop_pixels

Commands use the reference repository. Rust/WGSL changes need a native rebuild or npm run build:wasm, followed by a browser reload.

IDEA → IMPLEMENTATION

Follow the running code

Excerpts are extracted from the repository at site build time. Expand only the part you are studying; full files include context and line numbers.

src/workshop.wgsl · L72–79

These five operations turn coverage into Gaussian opacity and color.

Actual repository code
@fragment fn fs_main(input: Out) -> @location(0) vec4<f32> {
    let stage = u32(globals.viewport_stage.z);
    if stage <= 2u { return vec4(input.rgba.rgb, 1.0); }
    let weight = exp(-0.5 * dot(input.local, input.local));
    let alpha = min(0.99, input.rgba.a * weight);
    if alpha < 1.0 / 255.0 { discard; }
    return vec4(input.rgba.rgb * alpha, alpha);
}
Open full source with line numbers ↗

Real-model render pass ↗ · Real-model shader ↗

Predict it, change it, explain it

Before toggling order, write down which channel will get stronger. Then set both opacities near zero in Rust and predict the background.

Check your reasoning

Putting blue last raises blue and lowers red. Near-zero opacity approaches the background. Turning on depth writes for these transparent quads would discard contributions rather than correctly solve transparency.

Keep the theory beside the code

Open the related derivation or lab ↗

3D Gaussian Splatting · original research ↗

WebGPU · pipeline, buffers and validation ↗