Skip to content

[Feature] Directional Light Shadow MappingΒ #44

Description

@Winteradio

[Feature] Directional Light Shadow Mapping β€” Complete Implementation

Status: 🚧 In Progress
Priority: High
Relates To: Rendering Pipeline, Lighting System
Started: 2026-06-21
Last Updated: 2026-06-25


Overview

Complete directional light shadow mapping system for deferred rendering pipeline.
Camera frustum β†’ light space AABB β†’ orthographic shadow map β†’ PCF + slope-based bias β†’ per-light shading.

Current Status:

  • βœ… Phase 1 (Complete): Core shadow pass, projection setup, basic depth comparison
  • βœ… Phase 2 (Complete): Slope-based dynamic bias, PCF (9-sample), boundary fade
  • βœ… Phase 3 (Complete): Texel snapping for temporal stability
  • ❓ Phase 4 (Investigation): Depth bias origin (CPU vs GPU), Tangent ratio normalization
  • 🚧 Phase 5 (Planned): Cascade Shadow Maps, Perspective Shadow Mapping

Implementation Details

Phase 1: Core Shadow Mapping βœ…

Features:

  • Orthographic projection from camera frustum perspective
  • Shadow map render pass β†’ depth texture
  • Depth comparison with fragment depth
  • Border color handling (GL_CLAMP_TO_BORDER)

Files:

  • Engine/Private/Renderer/Proxy/LightProxy.cpp β€” DirectionalLightProxy::UpdateView()
  • Engine/Public/Renderer/RenderPass/ShadowPass.cpp β€” Shadow rendering
  • asset/shader/shadow_directional.vs.glsl β€” Vertex transformation

Result: Basic shadow rendering functional.


Phase 2: Slope-Based Bias + PCF βœ…

Features:

  • Dynamic Bias: shadowBias = texelRatio Γ— tangentRatio

    • texelRatio: Shadow map texel size relative to projection
    • tangentRatio: Surface normal angle vs light direction
    • Eliminates shadow acne on steep surfaces
  • PCF (Percentage Closer Filtering): 9-sample filter (3Γ—3 neighborhood)

    • Soft shadow edges without performance penalty
    • Samples at texel boundaries
  • Boundary Fade: Smooth transition at shadow map edges

    • smoothstep() blending at clip boundaries
    • Prevents hard shadow cutoff artifacts

Files:

  • asset/shader/lighting_directional.ps.glsl β€” Fragment shader bias + PCF

Result: High-quality shadows with soft edges, no acne on angled surfaces.


Phase 3: Texel Snapping for Temporal Stability βœ…

Features:

  • Texel Grid Quantization: Snap shadow map lookup position to discrete texel boundaries
  • Snap Grid Size: texelSize = frustumRadius * 2.f / shadowMapSize
  • Implementation:
    1. Transform frustum center to light space
    2. Quantize position: floor(pos / texelSize + 0.5) * texelSize
    3. Transform back to world space
    4. Reconstruct light view matrix from snapped position

Solves: Shadow flicker during camera movement (shadow grid no longer drifts relative to world).

Files:

  • Engine/Private/Renderer/Proxy/LightProxy.cpp β€” Lines 375-390

Result: Stable shadow placement across frames. βœ…

Screenshot: asset/screenshot/fixed_shadow_shimmering.gif


Sub-Issues & Resolutions

Sub-Issue 1: Depth Bias Origin (CPU vs GPU Calculation) ❓

Status: Requires Investigation

Current Observation:

  • CPU snaps at grid size: texelSize = frustumRadius * 2.f / shadowMapSize β‰ˆ 0.02148
  • GPU calculates texelRatio from: 1.0 / max(shadowSize.x, shadowSize.y) β‰ˆ 0.000977
  • 22Γ— difference but shadow blinking appears independent of bias magnitude

Question: Is the bias calculation actually the cause, or is this a symptom of deeper GPU/CPU synchronization issue?

Next Steps:

  • Test with fixed bias value (bypass tangentRatio)
  • Verify CPU and GPU use same snap grid for fragment position
  • Check if issue persists with unified texelRatio

Candidate Fixes (if confirmed as root cause):

// Option A: Pass texelRatio from C++ to shader
m_directional->data.texelRatio = 1.0f / texelSize;

// Option B: Snap fragment position in shader using same grid
lightPos.xy = floor(lightPos.xy / texelRatio + 0.5) * texelRatio;

Sub-Issue 2: Tangent Ratio Magnitude ❓

Status: Requires Investigation

Current Observation:
tangentRatio = length(light.direction - worldNor * dot(light.direction, worldNor))

Ranges from 0 to ~2, producing bias variance of [0, 0.043] when multiplied by texelRatio.

Question: Is this variance causing depth comparison instability, or is it functioning as designed for slope-based bias?

Next Steps:

  • Compare shadow quality with fixed tangentRatio (e.g., always 0.5)
  • Visualize bias map to identify over/under-shadowing regions
  • Determine if saturate(tangentRatio) improves stability

Candidate Fix (if confirmed as root cause):

tangentRatio = saturate(tangentRatio);  // Clamp to [0, 1]

Sub-Issue 3: Frustum Radius & Snap Grid Stability βœ…

Status: Resolved (as of Phase 3)

Confirmed: Frustum radius is stable and correctly applies to texel size calculation.

frustumRadius = distance(frustumCenter, furthest_frustum_corner);
frustumRadius = floor(frustumRadius + 0.5f);  // Snap to integer for stability

const float texelSize = frustumRadius * 2.f / shadowMapSize;  // βœ“ Correct

Result: Snap grid is consistent and correctly scaled. βœ…


Future Phases

Phase 5: Cascade Shadow Maps (Planned) 🚧

Goal: Multiple shadow maps at varying resolutions for different camera distances.

Implementation:

  • Split camera frustum into N cascades (e.g., near/mid/far)
  • Render each to separate shadow map at appropriate resolution
  • Blend transitions at cascade boundaries via depth-based lerp

Expected Benefit: High shadow resolution near camera, lower at distance (less memory/bandwidth).


Phase 6: Perspective Shadow Mapping (Planned) 🚧

Goal: Shadow shimmering mitigation for perspective camera (not orthographic).

Note: PSM unsuitable for directional lights (infinite distance); consider alternative approaches:

  • Re-projection techniques
  • Temporal filtering (previous frames)

Testing Checklist

  • Core Rendering: Shadow pass renders correctly to shadow map
  • Depth Comparison: Basic depth test (no bias) produces shadows
  • Slope-Based Bias: No shadow acne on steep surfaces
  • PCF: Soft shadow edges (no aliasing)
  • Boundary Fade: No hard cutoff at shadow map edges
  • Texel Snapping: Shadow position stable across frames (stationary camera)
  • Camera Movement: No flicker during smooth camera pan
  • Geometry Changes: Shadows remain stable when objects added/removed
  • RenderDoc: Single frame captures show correct shadow depth and bias
  • Multiple Lights: Shadows from multiple directional lights don't interfere

Performance Considerations

Operation Cost Notes
Shadow Pass (1 directional light) ~0.5ms Renders entire scene depth to 1024Γ—1024 map
Frustum Calculation <0.1ms Per-frame, O(8) frustum corners
Texel Snapping <0.01ms Arithmetic only, no branching
PCF (9-sample) ~0.1ms per pixel Modest cost; necessary for quality
Cascade Maps (if implemented) ~1.5ms 3 cascades Γ— shadow pass cost

Optimization Opportunities:

  • Shadow pass early-exit culling (front-to-back rendering)
  • Compute shader for frustum/sphere calculation
  • Deferred light culling (only shade pixels lit by shadow-casting lights)

Decision Log

Date Decision Rationale
2026-06-21 Slope-based bias + PCF Industry standard; eliminates acne without custom bias logic
2026-06-25 Texel snapping for stability Solves temporal drift; proven in major engines
2026-06-25 Defer bias investigation Origin unclear (CPU? GPU? both?); requires systematic testing
2026-06-25 Separate shadow blinking as distinct issue Bug is orthogonal to feature; tracked in Bug_ShadowBlinking.md

Notes

  • Frustum Stability: Confirmed that frustum radius and snap grid size are consistent; not the root cause of blinking.
  • Bias Origin: Unclear whether bias divergence or tangent ratio variance is problematic. Both need investigation via controlled testing.
  • Shadow Blinking: Separate tracked issue (see Docs/Bug_ShadowBlinking.md). May be GPU/CPU sync race unrelated to bias.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions