Exponential Shadow Mapping to Fix Shadow Acne

Exponential Shadow Mapping (ESM) is an advanced real-time shadowing technique that replaces the binary depth test of traditional shadow mapping with an exponential function to approximate visibility. This article explains how ESM works mathematically, how it integrates into DirectX rendering pipelines, and the specific mechanisms through which it eliminates the surface self-shadowing artifacts commonly known as shadow acne while enabling smooth, pre-filtered soft shadows.

Understanding Exponential Shadow Mapping (ESM)

Standard Shadow Mapping (SSM) renders scene depth from the perspective of a light source and compares the stored receiver depth to the scene depth using a step function. This binary test causes heavy aliasing and cannot be linearly filtered using standard graphics hardware.

Exponential Shadow Mapping approximates this step function using an exponential decay curve:

\[f(d, z) \approx e^{-c(d - z)}\]

Where:

  • \(d\) is the depth of the shadow receiver from the light's view.
  • \(z\) is the depth stored in the shadow map (the caster).
  • \(c\) is a user-defined scaling constant controlling sharpness.

Because \(e^{-c(d - z)}\) can be factored into \(e^{-cd} \cdot e^{cz}\), the term \(e^{cz}\) depends solely on the shadow caster. In a DirectX pipeline, this allows developers to store the exponentiated depth directly into an offscreen render target (typically DXGI_FORMAT_R32_FLOAT or DXGI_FORMAT_R16_FLOAT) during the light-depth pass.

Why Shadow Acne Occurs in DirectX Rendering

Shadow acne manifests as alternating bands of light and dark pixels across surfaces facing light sources. It occurs due to:

  1. Quantization Errors: Depth buffers have finite precision, causing round-off errors.
  2. Surface Slope Misalignment: A single shadow map texel covers a finite area in world space. When projected onto an angled surface, the discrete depth sample does not precisely match the continuous surface geometry, causing parts of a flat polygon to erroneously fail the depth test against itself.

In standard DirectX pipelines, developers combat acne using depth biases (DepthBias and SlopeScaledDepthBias in D3D11_RASTERIZER_DESC or D3D12_RASTERIZER_DESC). However, excessive bias causes "Peter Panning," where shadows detach from the caster's base.

How ESM Reduces Shadow Acne

ESM inherently suppresses shadow acne through mathematical approximation and hardware-level filtering:

1. Continuous Visibility Instead of a Hard Step Function

Standard depth testing fails abruptly when receiver depth exceeds caster depth by a fraction of a unit. ESM's exponential function creates a smooth curve rather than a step edge. When a surface samples its own depth with slight numerical or geometric discrepancies (\(d \approx z\)), the evaluation of \(e^{-c(d - z)}\) remains close to \(1.0\) instead of dropping instantly to \(0.0\). This prevents tiny numerical oscillations from manifesting as dark acne stripes.

2. Hardware Texture Filtering and Blurring

Because the exponential values can be linearly combined, DirectX can apply standard texture filtering directly to the shadow map:

  • Separable Gaussian Blurs: The shadow map can be blurred using standard compute or pixel shader passes prior to the lighting pass.
  • Bilinear/Trilinear Filtering: The GPU's texture sampling units (Texture2D.Sample) can interpolate values natively across texel boundaries.

Blurring averages out the geometric steps caused by shadow-map grid resolution, removing the high-frequency depth discrepancies that generate acne on planar and curved surfaces.

Implementing ESM in HLSL

To achieve stable results and avoid numeric overflow, the exponential depth test is typically clamped in the pixel shader.

Shadow Map Generation Pass (HLSL):

float4 PS_ShadowMap(VS_Output input) : SV_Target
{
    float depth = input.PositionWS.z / input.PositionWS.w; // Linear depth [0, 1]
    return exp(c * depth);
}

Lighting Evaluation Pass (HLSL):

Texture2D<float> ESMTexture : register(t0);
SamplerState LinearSampler : register(s0);

float CalculateESMShadow(float2 shadowUV, float receiverDepth, float c)
{
    // Sample the pre-filtered exponential shadow map
    float expCasterDepth = ESMTexture.Sample(LinearSampler, shadowUV);
    
    // Evaluate ESM shadow factor
    float shadowFactor = expCasterDepth * exp(-c * receiverDepth);
    
    // Clamp to valid visibility range
    return saturate(shadowFactor);
}

Balancing the Sharpness Constant (\(c\))

The factor \(c\) dictates the falloff steepness:

  • High values of \(c\): Produce sharper shadow edges matching standard shadow maps, but increase the risk of arithmetic overflow and allow subtle acne to resurface.
  • Low values of \(c\): Completely eradicate shadow acne and yield smooth penumbras, but increase "light bleeding" (where shadows wash out in overlapping geometry).

Setting \(c\) between 40 and 80 using a 32-bit floating-point format provides an optimal balance, keeping surfaces clean of acne while retaining distinct shadow boundaries.