Weighted Blended Order-Independent Transparency GLSL

Weighted Blended Order-Independent Transparency (WBOIT) approximates the transmission and blending of overlapping transparent surfaces in a single geometry pass without sorting polygons. By utilizing Multiple Render Targets (MRT) and additive blending hardware, WBOIT separates the rendering equation into color accumulation and visibility transmission. A depth-dependent weighting function dynamically scales each surface's contribution based on camera distance and opacity, allowing complex, intersecting transparent geometry to be composited realistically over an opaque background in a subsequent fullscreen resolve step.

The Challenge of Real-Time Transmission

Standard alpha blending requires rendering primitives in strict back-to-front order using the formula:

\[C = C_{src} \cdot \alpha_{src} + C_{dst} \cdot (1 - \alpha_{src})\]

Sorting geometry on the CPU per frame is computationally expensive, fails with intersecting geometry, and cannot handle non-convex cyclic overlaps. WBOIT bypasses sorting by making the blending operations commutative, reformulating the composite so that all transparent fragments can be rasterized in arbitrary order.

The Accumulation Pass Architecture

During the transparent geometry pass, depth writing is disabled while depth testing remains enabled to discard surfaces hidden behind opaque objects. Two render targets are bound simultaneously using OpenGL or Vulkan MRT:

  1. Accumulation Buffer (accumTexture, typically RGBA16F or RGBA32F): Stores the weighted sum of pre-multiplied transparent colors in RGB channels and the sum of weights in the Alpha channel.
  2. Revealage Buffer (revealTexture, typically R8 or R16F): Tracks total light transmittance (how much the background remains visible).

Hardware blend states are configured independently for each target:

  • Accumulation Target: Configured with additive blending (GL_ONE, GL_ONE).
  • Revealage Target: Configured to multiply incoming transmittance values (GL_ZERO, GL_ONE_MINUS_SRC_COLOR).

Depth Weighting Function

The core heuristic of WBOIT is the weighting function \(w(z, \alpha)\), which assigns higher significance to fragments closer to the camera to approximate occlusion. A standard formulation proposed by Morgan McGuire and Louis Bavoil is:

float computeWeight(float z, float alpha) {
    // Clamp to avoid floating-point extremes
    return clamp(pow(min(1.0, alpha * 10.0) + 0.01, 3.0) * 1e8 * 
                 pow(1.0 - z * 0.9, 3.0), 1e-2, 3e3);
}

A common linear view-space variant simplifies this calculation:

float computeWeight(float linearZ, float alpha) {
    return clamp(0.03 / (1e-5 + pow(linearZ / 200.0, 4.0)), 1e-2, 3e3) * alpha;
}

Fragment Shader Implementation

In the geometry pass, the fragment shader computes the surface color, alpha, and corresponding weight before outputting to the two targets.

#version 330 core
layout (location = 0) out vec4 accumOutput;
layout (location = 1) out float revealOutput;

in vec3 vNormal;
in vec3 vViewPosition;

uniform vec4 uColor; // Base transparent material color (RGBA)

void main() {
    vec4 color = uColor;
    
    // Linearize view depth or calculate normalized distance
    float linearDepth = abs(vViewPosition.z);
    
    // Compute heuristic weight
    float weight = computeWeight(linearDepth, color.a);

    // Accumulation: premultiplied weighted color and weighted alpha
    accumOutput = vec4(color.rgb * color.a, color.a) * weight;

    // Revealage: tracks how much background light transmits through (1 - alpha)
    revealOutput = color.a;
}

Because of the GL_ZERO, GL_ONE_MINUS_SRC_COLOR blend state, the revealage target updates as:

\[\text{Revealage}_{new} = \text{Revealage}_{old} \cdot (1 - \alpha)\]

Initialized to 1.0, this target continuously multiplies transmission factors regardless of draw order.

The Composite Pass

Once all transparent objects have rendered into the accumulation and revealage buffers, a fullscreen quad resolves the final image over the opaque background:

#version 330 core
out vec4 fragColor;
in vec2 uv;

uniform sampler2D opaqueTexture;
uniform sampler2D accumTexture;
uniform sampler2D revealTexture;

void main() {
    float revealage = texture(revealTexture, uv).r;
    
    // Skip composition if no transparent surfaces were rasterized
    if (revealage >= 1.0) {
        discard;
    }

    vec4 accum = texture(accumTexture, uv);
    
    // Prevent divide-by-zero errors
    vec3 averageColor = accum.rgb / max(accum.a, 0.00001);
    
    vec3 opaqueColor = texture(opaqueTexture, uv).rgb;

    // Blend accumulated transparent color with transmitted background color
    fragColor = vec4(averageColor * (1.0 - revealage) + opaqueColor * revealage, 1.0);
}

By computing transmittance as a product and average color via weighted normalization, WBOIT produces a plausible transmission approximation that entirely removes geometry sorting overhead.