How PCF Softens Shadows in DirectX Shaders

Percentage-Closer Filtering (PCF) is a real-time rendering technique used to eliminate harsh, aliased edges produced by standard shadow mapping, replacing them with smooth, visually pleasing shadow boundaries. This article details the mechanics of PCF within DirectX shadow shaders, explaining why traditional image filtering fails on depth buffers, how sampling multiple comparison results creates smooth gradients, and how hardware features in DirectX optimize the process.

The Limitation of Standard Shadow Mapping

In traditional shadow mapping, a scene is rendered from the light's point of view to generate a depth texture (shadow map). During the main rendering pass, each surface pixel's position is transformed into light space, and its distance from the light is compared against the depth stored in the shadow map.

Because the shadow map has a finite resolution, a single texel covers a finite area in world space. Standard point sampling yields a binary result: a pixel is either completely in shadow (0.0) or completely lit (1.0). This binary evaluation results in jagged, stair-stepped shadow edges known as shadow aliasing.

Why Filtering Depth Values Directly Fails

A common misconception is that shadow maps can be blurred using standard image filters (like a Gaussian blur) before the depth test is performed. Doing so averages non-linear depth values across different surfaces. When this averaged depth is tested against an actual surface depth, it introduces severe visual artifacts, such as shadow acne and "light bleeding," where shadows erroneously disappear or appear in incorrect locations.

PCF solves this problem by changing the order of operations: instead of filtering the depth values, it filters the results of the depth comparisons.

How PCF Works in the Shader

PCF softens boundaries by determining the percentage of neighboring points around a pixel that are occluded by occluders. The process in an HLSL pixel shader follows these steps:

  1. Transform Coordinates: The pixel's world-space position is transformed into the light's clip space and converted into normalized device coordinates (UV texture coordinates ranging from [0, 1] and a reference depth value \(z\)).
  2. Define a Kernel: A sample kernel (e.g., a \(3\times3\) or \(4\times4\) grid, or a randomized Poisson disk) is established around the central UV coordinate.
  3. Execute Depth Tests: The shader iterates through each offset in the kernel, sampling the shadow map depth and comparing it to the current fragment’s reference depth (with an appropriate depth bias to prevent self-shadowing).
  4. Aggregate Results: Each test returns a binary value: 1.0 if the sample is lit, or 0.0 if it is occluded.
  5. Calculate Average: The shader averages all test results together:

\[\text{Shadow Factor} = \frac{1}{N} \sum_{i=1}^{N} \text{Test}(UV + \text{Offset}_i, z)\]

If a pixel lies completely inside a shadow, all samples evaluate to 0.0 (resulting in a final factor of 0.0). If a pixel is fully exposed, all samples evaluate to 1.0. Along the shadow edges, however, only a fraction of the kernel samples will pass the depth test. This produces fractional values between 0.0 and 1.0, generating a soft transition region known as a pseudo-penumbra.

DirectX Hardware Support and Optimization

Modern GPUs and DirectX (Direct3D 10 and later) provide dedicated hardware support to accelerate PCF via comparison samplers:

  • SamplerComparisonState: DirectX allows developers to define a sampler with comparison enabled (e.g., D3D11_COMPARISON_LESS_EQUAL).
  • SampleCmp and SampleCmpLevelZero: In HLSL, invoking Texture2D.SampleCmp instructs the GPU hardware to perform four depth comparisons on the adjacent \(2\times2\) texels and bilinearly filter the comparison outcomes natively in silicon.

Using hardware comparison samplers provides a single-instruction \(2\times2\) PCF filter at virtually the same performance cost as standard bilinear filtering. For wider, softer penumbras, developers combine multiple SampleCmp calls over a larger Poisson or jittered distribution, blending hardware-accelerated bilinear PCF with multi-tap sampling to produce clean boundaries with minimal shader overhead.