Skip to content

Fix CSM shadow casters incorrectly culled by camera distance; add cross-cascade blending - #1244

Merged
untoldengine merged 1 commit into
developfrom
bugfix/shadow_cascade_bug
Sep 22, 2026
Merged

untoldengine merged 1 commit into
developfrom
bugfix/shadow_cascade_bug

Conversation

@untoldengine

@untoldengine untoldengine commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Removed the per-cascade camera-distance shadow-caster cull: it could drop
    a valid caster whenever it sat outside a cascade's camera-depth interval
    but still projected a shadow into it (e.g. long shadows from a low sun).
    Casters are now culled against the fitted light-space cascade frustum,
    which is correctness-preserving. The lost draw-call optimization is
    restored via a new distance reject measured from each cascade's own
    world-space bounding sphere (direction-agnostic, so it can't reintroduce
    the original bug).
  • Added cross-cascade shadow blending: the last 10% of each cascade interval
    now cross-fades between adjacent cascade shadow maps instead of hard-
    switching, removing a visible pop at cascade splits. The blend boundary is
    computed once per frame on the CPU and uploaded to the shader as data, so
    the CPU frustum-widening and GPU cross-fade can't drift out of sync.
  • Added normal-offset shadow sampling to fix acne from normal-mapped shading
    normals, with a guard against zero-length normals so a degenerate
    G-buffer normal can't produce NaN in the shadow lookup.
  • Shadow bias and PCF filter radius are now converted to world units and
    back, keeping them physically consistent across cascades with different
    orthographic depth ranges and texel densities.

This PR addressed issue #1221

Test plan

  • swift build succeeds
  • buildkernels.sh rebuilds all .metallib targets cleanly
  • swift test --filter "ShadowSystemCascade|ShadowSystemMakeUniformsTests|CsmCascadeCountTests|ShadowDistanceCullingTests" — 38/38 pass
  • Manual visual check of shadows in an editor/demo scene (cascade transitions, low-angle sun, normal-mapped surfaces)

…ade blending

Directional-light shadow casters were culled by camera-depth distance against
each cascade's own split, but an object outside a cascade's camera-depth
interval can still project a shadow into it (e.g. a low sun casting a long
shadow from off-screen). Cull casters against the fitted light-space cascade
frustum instead, which is the correctness-preserving test; restore the lost
draw-call optimization with a direction-agnostic distance reject measured from
each cascade's own world-space bounding sphere instead of the camera.

Also:
- Cross-fade shadow visibility across the last 10% of each cascade interval
  instead of hard-switching, avoiding a visible pop as geometry crosses a
  split. The blend boundary is computed once per frame on the CPU
  (ShadowSystem.cascadeBlendStart) and uploaded to the shader, rather than
  independently re-derived on both sides.
- Push the shadow-space sample point along the surface normal (normal-offset
  shadows) instead of only biasing depth, fixing acne from normal-mapped
  shading normals; guard against a zero-length normal so a degenerate
  G-buffer normal can't NaN the shadow lookup.
- Convert shadow bias and PCF filter radius to world units and back so they
  stay physically consistent across cascades with different orthographic
  depth ranges and texel densities.
@untoldengine
untoldengine merged commit 906042b into develop Sep 22, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant