Implementing Volumetric Lighting with Shadow Maps

Volumetric lighting via shadow maps enhances atmospheric realism in video games by simulating the scattering of light through participating media like fog, dust, and moisture. This article explores the primary technical considerations developers face when implementing this technique, including ray marching efficiency, shadow map precision, phase function approximations, spatial-temporal filtering, and downsampled rendering pipelines.

Ray Marching Resolution and Sampling Artifacts

The core mechanism involves ray marching along the camera's view vector through participating media, sampling the shadow map at discrete intervals to determine light occlusion.

  • Step Count vs. Performance: A low step count results in severe undersampling, manifesting as distinct banding artifacts along the light volume. High step counts eliminate banding but drastically increase texture fetch overhead and GPU fill-rate demands.
  • Jittering and Noise: To combat banding without increasing step counts, developers apply stochastic jittering (such as blue noise offsets) to the ray march start positions. This converts coherent banding into high-frequency noise, which is easier to resolve temporally.

Shadow Map Precision, Filtering, and Cascades

Using standard shadow maps for volumetric evaluation introduces specific geometric and precision challenges.

  • Depth Biasing and Precision: Volumetric rays query the shadow map continuously along the volume rather than just on opaque surfaces. Standard depth bias settings tuned for surface geometry often cause clipping or light leaking near shadow boundaries in the air.
  • Cascaded Shadow Map (CSM) Transitions: When traversing across multiple shadow map cascades, sampling transitions can produce noticeable seams in volumetric density. Implementing smooth interpolation or cascade blending inside the ray march loop is necessary to maintain visual consistency.
  • Shadow Map Texture Lookups: Standard Percentage-Closer Filtering (PCF) is designed for 2D surface evaluation. For volumetric rays, point sampling the shadow map depth with subsequent post-process filtering is generally more performance-efficient than running multi-tap PCF at every ray march step.

Scattering Functions and Media Density

Simulating real-world optics requires accurate mathematical models for how light interacts with particles.

  • In-Scattering and Transmittance: Light intensity must decay along the ray path according to the Beer-Lambert law, calculating both optical depth and transmittance.
  • Phase Functions: Isotropic scattering scatters light equally in all directions, which is computationally cheap but lacks realism. Incorporating anisotropic phase functions, like the Henyey-Greenstein approximation, allows strong forward scattering that creates dramatic "god ray" effects when looking toward a light source.
  • Heterogeneous Media: While homogeneous fog assumes constant density, realistic environments require sampling 3D noise textures (e.g., animated Curl or Perlin-Worley noise) to model shifting fog pockets, requiring additional texture bandwidth.

Spatial and Temporal Optimizations

Directly ray marching at native screen resolution is rarely viable for real-time rendering budgets.

  • Half-Resolution Rendering and Bilateral Upsampling: Evaluating volumetric lighting at half or quarter resolution reduces the pixel shader workload by up to 75%. Reconstructing the final image requires depth-aware bilateral upsampling to prevent volumetric light from bleeding across sharp silhouette edges of foreground geometry.
  • Temporal Accumulation: Temporal Anti-Aliasing (TAA) or dedicated temporal reprojection smooths the stochastic noise introduced by jittered ray marching. This requires tracking camera motion vectors and history buffers while rejecting ghosting artifacts during rapid light or camera movement.
  • Frustum Voxel Grids (Froxels): Modern game engines often decouple ray marching from the screen resolution entirely by populating a 3D frustum-aligned voxel grid (froxel grid) via compute shaders. Shadow map lookups and scattering calculations are computed once per voxel cell and then integrated in a single forward pass, providing predictable performance scaling regardless of scene depth complexity.