VSM Light Bleeding Reduction with Chebyshev in GLSL

This article explains how to reduce light bleeding in Variance Shadow Maps (VSM) using Chebyshev remapping in GLSL. While standard VSM produces smooth shadow boundaries by computing depth moments, overlapping occluders often produce severe light leaks. By applying a linear remapping function to the upper bound derived from Chebyshev's inequality, developers can effectively eliminate this artifact while preserving hardware-accelerated filtering and soft shadow edges.

The Variance Shadow Map Light Bleeding Problem

Variance Shadow Maps store the depth (\(z\)) and squared depth (\(z^2\)) in a two-channel texture. During rendering, these moments are retrieved to calculate the local mean \(\mu\) and variance \(\sigma^2\):

\[\mu = E(x)\] \[\sigma^2 = E(x^2) - E(x)^2\]

When a fragment at depth \(t\) is evaluated where \(t > \mu\), Chebyshev’s inequality provides an upper bound on the probability that the fragment is lit (\(p_{max}\)):

\[p_{max} = \frac{\sigma^2}{\sigma^2 + (t - \mu)^2}\]

Light bleeding occurs when multiple occluders cast shadows on the same surface, or when an occluder is far from the receiver. In these scenarios, the variance \(\sigma^2\) becomes excessively high, causing \(p_{max}\) to remain significantly above zero even when a surface is fully obscured.

The Chebyshev Remapping Algorithm

To eliminate false illumination, Lauritzen proposed Chebyshev remapping (often called the linstep clamp). The algorithm rescales the upper bound \(p_{max}\) from an original range \([p_{min}, 1.0]\) down to \([0.0, 1.0]\), forcing any values below the minimum threshold \(p_{min}\) directly to fully shadowed (\(0.0\)).

The remapping function is defined as:

\[\text{BleedReduction}(p, p_{min}) = \text{clamp}\left(\frac{p - p_{min}}{1.0 - p_{min}}, 0.0, 1.0\right)\]

Any illumination contribution smaller than \(p_{min}\) is discarded, preventing low-probability light leakage across occluded regions.

GLSL Implementation

Below is the GLSL implementation of the Chebyshev remapping algorithm integrated into a standard VSM sampling routine:

// Utility to remap and clamp values to [0.0, 1.0]
float linstep(float low, float high, float v) {
    return clamp((v - low) / (high - low), 0.0, 1.0);
}

// Calculates shadow factor with light bleeding reduction
// moments: sampled vec2 from shadow map (mean, mean of squares)
// receiverDepth: linear depth of current fragment in light space
// bleedReductionFactor: light bleeding threshold (p_min, typically 0.1 to 0.4)
float calculateVSM(vec2 moments, float receiverDepth, float bleedReductionFactor) {
    // If the fragment is in front of the occluder, it is fully lit
    if (receiverDepth <= moments.x) {
        return 1.0;
    }

    // Compute variance with a tiny epsilon to avoid division by zero
    float variance = max(moments.y - (moments.x * moments.x), 0.00002);

    // Compute Chebyshev upper bound (p_max)
    float d = receiverDepth - moments.x;
    float pMax = variance / (variance + d * d);

    // Apply Chebyshev remapping to eliminate light bleeding
    return linstep(bleedReductionFactor, 1.0, pMax);
}

Parameter Tuning

The parameter bleedReductionFactor (representing \(p_{min}\)) controls the aggressiveness of the filter:

  • Setting it to 0.0 disables the fix, yielding raw Chebyshev bounds with visible light bleeding.
  • Values between 0.1 and 0.3 remove most light bleeding artifacts without severely hardening the penumbra.
  • Setting it too high (\(> 0.5\)) will darken penumbrae prematurely and can introduce shadow aliasing along soft edges.