← Back
The Slang Code

This entire page unpacks the following Slang shader code, which implements the alpha-blending forward pass and its analytic inverse for backpropagation.

[Differentiable]
float4 preMult(float4 pixel) {
    return float4(pixel.rgb * pixel.a, pixel.a);
}

[Differentiable]
float4 alphaBlend(float4 pixel, float4 gval) {
    gval = preMult(gval);
    return float4(
        pixel.rgb + gval.rgb * pixel.a,
        pixel.a * (1 - gval.a));
}

float4 undoAlphaBlend(float4 pixel, float4 gval) {
    gval = preMult(gval);
    var oldPixelAlpha = pixel.a / (1 - gval.a);
    return float4(
        pixel.rgb - gval.rgb * oldPixelAlpha,
        oldPixelAlpha);
}

struct PixelState : IDifferentiable {
    float4 value;
    uint finalCount;
};

[Differentiable]
PixelState transformPixelState(PixelState pixel, float4 gval) {
    var newState = alphaBlend(pixel.value, gval);
    if (pixel.value.a < 1.f / 255.f)
        return { pixel.value, pixel.finalCount };
    return { newState, pixel.finalCount + 1 };
}

PixelState undoPixelState(PixelState nextState, uint index, float4 gval) {
    if (index > nextState.finalCount)
        return { nextState.value, nextState.finalCount };
    return { undoAlphaBlend(nextState.value, gval), nextState.finalCount - 1 };
}
1 — Premultiplied Alpha

Standard RGBA stores colour and transparency as independent values: (r, g, b, a). Premultiplied alpha folds the alpha into the colour channels: (r·a, g·a, b·a, a).

The preMult function converts straight alpha to premultiplied before every blend step. This matters because the blending formula adds the Gaussian's contribution scaled by remaining transparency — and that formula only produces the right answer when the source colour is already premultiplied.

The practical consequence is visible at transparent edges. Without premultiplying, the blending arithmetic inadvertently mixes in a dark halo from the zeroed-out RGB values at near-zero alpha. With premultiplying, those near-transparent pixels contribute almost nothing and the compositing is clean.

The effect is most dramatic at extreme alpha values — slide alpha below 0.3 or above 0.7 to see the difference clearly.

1.00
0.20
0.20
0.50

Left: without premultiplying  |  Right: with preMult

2 — The Blending Formula

Each Gaussian splatted onto a pixel is blended in using alphaBlend. The accumulator pixel starts at (0, 0, 0, 1) — black colour with full remaining transparency (not full opacity). As Gaussians are blended in, colour accumulates and the remaining transparency shrinks toward zero.

premult_gval = (gval.rgb × gval.a ,  gval.a) new_rgb   = pixel.rgb + premult_gval.rgb × pixel.a new_alpha = pixel.a × (1 − gval.a)

Build up a stack of Gaussians below. Drag layers to reorder them — because the blending is order-dependent, reordering changes the result.

3 — The Termination Condition

transformPixelState wraps alphaBlend with an early-exit guard:

[Differentiable]
PixelState transformPixelState(PixelState pixel, float4 gval) {
    var newState = alphaBlend(pixel.value, gval);
    if (pixel.value.a < 1.f / 255.f)
        return { pixel.value, pixel.finalCount };
    return { newState, pixel.finalCount + 1 };
}

When pixel.value.a falls below 1/255 ≈ 0.004, the pixel is effectively opaque — no further Gaussians can contribute any visible colour. Blending is halted and finalCount is frozen at the index of the last Gaussian that actually mattered.

Two reasons this matters:

15
0.15
4 — Reversing the Blend (Backward Pass)

During backpropagation, the renderer needs to recover the accumulator state before each Gaussian was blended — so gradients can be propagated back through each step. undoAlphaBlend inverts the blend analytically.

Deriving the inverse:

// Forward (known): new_alpha = old_alpha × (1 − gval.a)   // Invert for old_alpha: old_alpha = new_alpha ÷ (1 − gval.a)   // Forward (known): new_rgb = old_rgb + premult_gval.rgb × old_alpha   // Invert for old_rgb (substitute old_alpha from above): old_rgb = new_rgbpremult_gval.rgb × old_alpha

Note: old_alpha is divided from new_alpha. If gval.a = 1 then the denominator is zero and old_alpha would be infinite. The termination threshold in transformPixelState prevents the pixel from ever reaching a state that would require such an inversion.

Step through the forward and backward passes below. Each forward step adds a Gaussian; each backward step analytically recovers the state before that Gaussian was blended in.

Step 0 / 0
5 — undoPixelState and finalCount

The backward pass doesn't simply walk every Gaussian in reverse order. It uses finalCount — stored in PixelState — to know exactly how many blends actually happened. Gaussian indices beyond finalCount are silently skipped.

PixelState undoPixelState(PixelState nextState, uint index, float4 gval) {
    if (index > nextState.finalCount)
        return { nextState.value, nextState.finalCount };  // skip
    return { undoAlphaBlend(nextState.value, gval), nextState.finalCount - 1 };
}

A concrete example with finalCount = 7:

// Backward pass, walking from highest index down to 1:
index = 8   >  nextState.finalCount (7) → skip — this blend never happened
index = 7   ≤  nextState.finalCount (7) → undo blend, finalCount becomes 6
index = 6   ≤  nextState.finalCount (6) → undo blend, finalCount becomes 5
        ...
index = 1   ≤  nextState.finalCount (1) → undo blend, finalCount becomes 0

The check index > nextState.finalCount is evaluated on the current state, not the original. As each undo decrements finalCount, the guard naturally keeps pace with the walk — no extra bookkeeping needed.

This design also handles the degenerate case where a pixel never reached opacity saturation: finalCount just equals the total number of Gaussians, and every step is undone normally.