PCSS Penumbra Width Math in GLSL Shaders

This article breaks down how the penumbra width is mathematically derived and evaluated within a Percentage-Closer Soft Shadows (PCSS) GLSL fragment shader. It covers the core geometric principles of similar triangles, the required depth transformations, and the direct GLSL implementation used to map light source dimensions to a dynamic Percentage-Closer Filtering (PCF) search radius.

The Geometric Model: Similar Triangles

PCSS models the light source as a parallel, planar area light facing the occluder (blocker) and receiver surfaces. The shadow penumbra is the region where the light source is only partially occluded.

By applying the intercept theorem (similar triangles) along the light projection axis:

\[\frac{w_{penumbra}}{d_{receiver} - d_{blocker}} = \frac{w_{light}}{d_{blocker}}\]

Solving for the penumbra width (\(w_{penumbra}\)) yields:

\[w_{penumbra} = \frac{(d_{receiver} - d_{blocker}) \cdot w_{light}}{d_{blocker}}\]

Where:

  • \(d_{receiver}\) is the distance from the light source to the fragment being shaded.
  • \(d_{blocker}\) is the average depth of occluders casting shadows on the fragment (determined during the initial blocker search step).
  • \(w_{light}\) is the physical size or radius of the area light source.

Depth Space Considerations

For the ratio \((d_{receiver} - d_{blocker}) / d_{blocker}\) to remain mathematically correct, depths must be linear. If you are using a standard perspective projection matrix for the light:

  1. Non-Linear Depth Buffers: Values sampled from a default OpenGL hardware depth buffer are non-linear (ranging from \([0, 1]\) or \([-1, 1]\)) due to the perspective divide (\(z_{ndc} = z_{eye} \cdot A + B / -z_{eye}\)). Using non-linear depth values distorts the triangle proportions, resulting in unnatural contact hardening and erratic penumbra scaling.
  2. Linearization: Before evaluating the formula, both \(d_{receiver}\) and \(d_{blocker}\) must either be converted into view-space distances (eye-space \(z\)) or stored directly as linear depths during the shadow map generation pass:

\[z_{linear} = \frac{z_{near} \cdot z_{far}}{z_{far} - z_{depth} \cdot (z_{far} - z_{near})}\]

Texture Space Transformation

The computed \(w_{penumbra}\) represents a physical scale or view-space diameter. To use this value as a sampling radius for Percentage-Closer Filtering in a GLSL shadow map, it must be mapped into normalized texture coordinate (UV) space:

\[w_{uv} = w_{penumbra} \cdot \left(\frac{1}{\text{Frustum Width at } d_{receiver}}\right)\]

Alternatively, developers often pre-scale \(w_{light}\) into shadow map UV space relative to the near plane, allowing the evaluation to directly yield a UV coordinate offset:

\[\text{Radius}_{UV} = \frac{d_{receiver} - d_{blocker}}{d_{blocker}} \cdot \text{LightSize}_{UV}\]

GLSL Implementation

Below is the GLSL function implementing the mathematical evaluation:

// Calculates the PCF filter radius in shadow map UV coordinates
float CalculatePenumbraWidth(
    float zReceiver,     // Linear depth of current fragment from light
    float zBlocker,      // Average linear depth of occluders
    float lightSizeUV,   // Light size in shadow map UV units
    float maxRadiusUV    // Maximum filter radius clamp
) {
    // Prevent division by zero and invalid penumbra sizes behind blockers
    if (zBlocker <= 0.0 || zReceiver <= zBlocker) {
        return 0.0;
    }

    // Evaluate the similar triangles formula
    float penumbra = ((zReceiver - zBlocker) * lightSizeUV) / zBlocker;

    // Clamp the filter radius to prevent undersampling artifacts and extreme blurring
    return clamp(penumbra, 0.0, maxRadiusUV);
}

Key Execution Details

  • Early Out / Hard Shadow Limit: When \(z_{receiver} \approx z_{blocker}\), the penumbra width approaches zero, producing a sharp contact shadow directly beneath the occluding object.
  • Clamping: Real-time shadow implementations must clamp the resulting value using a maximum radius (maxRadiusUV). Because the kernel size for the final PCF step uses a fixed number of samples (e.g., Poisson disk or regular grid), excessively large penumbra widths lead to visible noise and undersampling artifacts.