Skip to content

Conversation

@superdump
Copy link
Contributor

@superdump superdump commented Dec 25, 2025

Objective

Solution

  • The search radius used on main simplified down to the light_size / cascade_diameter. The light size could be tweaked for this to look ok, but the softness of the shadows would be unintuitive for cascaded shadow maps given the interplay between the cascade configuration and the light size - if you change one you would have to tweak the other.
  • Instead, it seems this works much better if one uses 128.0f / shadow_map_size equivalent to 128 texels, where shadow_map_size is the texture dimensions of the cascade. At least for the shadow map size we use. Given all cascades use the same resolution, this is practically a constant: 128.0 / 2048.0 = 0.0625 in UV units. This means that each cascade will search smaller or larger regions for blockers depending on how close they are to the camera. It does feel like the blocker search. This came from this MIT-licensed code: https://github.com/PanosK92/SpartanEngine/blob/f0526aac0ed3197cc3ffb09d029f8b3bde8daf21/data/shaders/shadow_mapping.hlsl#L106
  • I observed 'ghost' shadows when only changing the search radius, so I added in random rotations of the blocker search sampling kernel to avoid this.
  • interleaved_gradient_noise is supposed to use screen UV, not light local UV. Changing this removes aliasing and moirée in the noise and significantly improves directional/spot light soft shadow quality in both non-temporal and temporal modes. Merged in Reduce aliasing and Moiré patterns in temporal shadow filtering #22400

Testing

I've tested using the pcss example on an M4 Max.


Showcase

Soft shadows off:
Screenshot 2025-12-25 at 22 57 57

PCSS main:
Screenshot 2025-12-25 at 22 58 01

PCSS on this PR:
Screenshot 2025-12-29 at 01 16 30
Note how the shadow is harder at the base of the tree and softer around the leaves. I don't have a ground truth image of this scene to compare to, but this is at least more how it should be.

@superdump
Copy link
Contributor Author

This is a draft because I'm not super convinced by this approach yet for the following reasons:

  • Why does a blocker search radius of 128 texels always work? Should it be a constant of 0.0625 UV units always? Should it be a configurable number of texels such that if people change the shadow map resolution, the PCSS blocker search radius in texels is also or can also be adjusted?
  • I think it needs testing across more different configurations and scenes. I may throw something together with a bunch of trees of different sizes and different distances from the camera, but not too dense, so that the shadows can be seen.
  • The noise patterns in the shadows aren't great. Also in the mentioned MIT code is use of a golden spiral sampling kernel. It seems that Jimenez 2014 does use spirals with interleaved gradient noise random rotations, but apparently the golden spiral is ideal. Could be worth trying. I wonder if a different random rotation approach could be less jarring too. Perhaps blue noise random rotations.

@superdump
Copy link
Contributor Author

Using interleaved_gradient_noise based on screen UV instead of light local UV significantly improved the visual quality of the noise in the shadows. It is used for the random rotations of the sampling kernel.

I think the size of the blocker search radius is supposed to be based on the tangent of the angle the light source covers. We actually have this information in our light uniform so I can try to plumb that through and make use of it so it's independent of shadow map resolution.

The blur resolution calculation still looks wrong. But I also need to figure out why making it correct breaks things. :)

After that, I think this should be ready to move out of draft and get more testing.

And perhaps next up I'll try using a 'vogel disk' (a.k.a. golden spiral) sampling kernel pattern rather than the one from Jimenez 2014 (CoD:AW) or the DirectX MSAA pattern being used for blocker search. That should give good coverage of the sample kernel radius in an arbitrary number of samples (we could use lookups for say 4/8/16 samples but fall back to calculating the sample offsets on the fly for other values).

@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Refinement Improves output quality, without fixing a clear bug or adding new functionality. labels Dec 29, 2025
@alice-i-cecile alice-i-cecile added the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Dec 29, 2025
@superdump
Copy link
Contributor Author

I've split the screen uv part out into #22400 as it affects more than just PCSS and is less subjective. It's just a bug pretty much.

This results in the PCSS soft shadows being harder close to the contact of the object with the receiving surface, and softer when there is more distance between them.
This removes some 'ghost' shadows that otherwise appear.
@superdump superdump marked this pull request as ready for review January 7, 2026 13:33
@superdump
Copy link
Contributor Author

I think the size of the blocker search radius is supposed to be based on the tangent of the angle the light source covers. We actually have this information in our light uniform so I can try to plumb that through and make use of it so it's independent of shadow map resolution.

I've tried tweaking this to use the tangent of the light angle, but it doesn't seem to work better.

The blur resolution calculation still looks wrong. But I also need to figure out why making it correct breaks things. :)

I've tried to correct the blur calculation, but it too seems to have issues I haven't been able to resolve yet.

And perhaps next up I'll try using a 'vogel disk' (a.k.a. golden spiral) sampling kernel pattern rather than the one from Jimenez 2014 (CoD:AW) or the DirectX MSAA pattern being used for blocker search. That should give good coverage of the sample kernel radius in an arbitrary number of samples (we could use lookups for say 4/8/16 samples but fall back to calculating the sample offsets on the fly for other values).

I've tried vogel disk sampling a bit and it's not convincingly better yet. So I think a more thorough and separate investigation needs to be done.

I'm marking this as ready for review. More tweaks can come later, but I think this is already much better.

It needs testing on more scenes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Refinement Improves output quality, without fixing a clear bug or adding new functionality. S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants