Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Take DirectionalLight's GlobalTransform into account when calculating shadow map volume (not just direction) #6384

Closed
wants to merge 10 commits into from

Conversation

coreh
Copy link
Contributor

@coreh coreh commented Oct 27, 2022

Objective

This PR fixes #5789, by enabling movable (and scalable) directional light shadow volumes.

Solution

This PR changes ExtractedDirectionalLight to hold a copy of the DirectionalLight entity's GlobalTransform, instead of just a direction vector. This allows the shadow map volume (as defined by the light's shadow_projection field) to be transformed honoring translation and scale transforms, and not just rotation.

It also augments the texel size calculation (used to determine the shadow_normal_bias) so that it now takes into account the upper bound of the x/y/z scale of the GlobalTransform.

This change makes the directional light extraction code more consistent with point and spot lights (that already use transform), and allows easily moving and scaling the shadow volume along with a player entity based on camera distance/angle, immediately enabling more real world use cases until we have a more sophisticated adaptive implementation, such as the one described in #3629.

Note: While it was previously possible to update the projection achieving a similar effect, depending on the light direction and distance to the origin, the fact that the shadow map camera was always positioned at the origin with a hardcoded Vec3::Y up value meant you would get sub-optimal or inconsistent/incorrect results.


Changelog

Changed

  • DirectionalLight shadow volumes now honor translation and scale transforms

Migration Guide

  • If your directional lights were positioned at the origin and not scaled (the default, most common scenario) no changes are needed on your part; it just works as before;
  • If you previously had a system for dynamically updating directional light shadow projections, you might now be able to simplify your code by updating the directional light entity's transform instead;
  • In the unlikely scenario that a scene with directional lights that previously rendered shadows correctly has missing shadows, make sure your directional lights are positioned at (0, 0, 0) and are not scaled to a size that's too large or too small.

@@ -560,15 +560,15 @@ pub fn extract_lights(
directional_light.shadow_projection.top
- directional_light.shadow_projection.bottom,
);
let directional_light_texel_size =
largest_dimension / directional_light_shadow_map.size as f32;
let directional_light_texel_size = transform.radius_vec3a(Vec3A::ONE) * largest_dimension
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the largest dimension be calculated after the transform?

let directional_light_texel_size = transform.radius_vec3a(Vec3A::new(
  directional_light.shadow_projection.right - directional_light.shadow_projection.left,
  directional_light.shadow_projection.top - directional_light.shadow_projection.bottom,
  0.
)).abs().max_element() / directional_light_shadow_map.size as f32;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. Updated to use this approach, should give more accurate results.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-Rendering Drawing game state to the screen labels Oct 27, 2022
@rparrett
Copy link
Contributor

I can't review the math, but I've been testing with a modified lighting.rs that moves the whole scene around and this seems to work.

It would be great if these new behaviors were documented on DirectionalLight.

I do agree that even if this isn't the ultimate solution, it would be a huge win over the status quo.

@rparrett
Copy link
Contributor

Pinging @ramirezmike

@coreh
Copy link
Contributor Author

coreh commented Oct 28, 2022

It would be great if these new behaviors were documented on DirectionalLight.

Added a section on directional light shadows, documenting how the volume (and shadow maps) work.

Copy link
Contributor

@rparrett rparrett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been testing in this little "directional light sandbox" and this seems to be working well.

An interesting note is that prior to this PR, you can modify the translation of a DirectionalLight, and even the lighting example uses a directional light in a non-zero position. But it's not clear at all what the behavior is supposed to be. When you move the light, the shadow projection is affected, but not in any way that would seem to make sense. There may be a bug there. If so, this seems to fix it.

I'm really happy about the new docs as well.

@ramirezmike
Copy link
Contributor

I checked out this PR locally and updated one of my games to use it and it works like a charm. My game has a system to update the camera's transform based on the player's transform and I just added DirectionalLight to that query filter and now the shadows work regardless of how far I'm from the origin.

I also can't comment too much on the math involved but the parts I grasp make sense and the docs are great 👍

I'll continue testing this out but pretty happy with it so far.

@rparrett
Copy link
Contributor

rparrett commented Nov 1, 2022

I tested a bit more in the playground above with modifying the shadow projection directly and it seems like the behavior is the same before/after this PR. (when the scale is 1.)

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Nov 2, 2022
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice new docs, fixes an important limitation, reviewers seem to have thoroughly tested this. Added to the milestone.

@alice-i-cecile alice-i-cecile added this to the Bevy 0.9 milestone Nov 2, 2022
crates/bevy_pbr/src/light.rs Outdated Show resolved Hide resolved
crates/bevy_pbr/src/light.rs Outdated Show resolved Hide resolved
crates/bevy_pbr/src/light.rs Outdated Show resolved Hide resolved
crates/bevy_pbr/src/light.rs Outdated Show resolved Hide resolved
crates/bevy_pbr/src/render/light.rs Show resolved Hide resolved
@rparrett
Copy link
Contributor

rparrett commented Nov 3, 2022

@superdump Do we need to adjust DEFAULT_SHADOW_NORMAL_BIAS now? Playing in the shadow_biases example, it seems like an equivalent value to the current default would now be ~0.9

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good, but I'm conflicted on this one. This is a useful intermediate step and solves a real problem, but it is also training people to think about DirectionalLights in a non-standard way (and write code that operates under that assumption). Everywhere else in the ecosystem (ex: Godot, Blender, etc), DirectionalLight position has no bearing on how light / shadows work. I would have preferred a solution that maintains that mental model, but I also understand that the approach in this PR is way more straightforward to implement.

That being said, theres enough positive sentiment here to get me on board. This just increases the pressure to get shadow cascades / view frustum fitting working, which are sorely needed already!

@superdump
Copy link
Contributor

@cart - the approach that I implemented a while back was to create an oriented bounding box around the view frustum of the camera and adjust the light's shadow projection based on that. I abandoned it because the shadow map quality quickly degrades with the size of the frustum.

@coreh
Copy link
Contributor Author

coreh commented Nov 4, 2022

Merged and fixed the conflicts. The system now seems to split ShadowView and GpuDirectionalLight extraction in two separate loops, I believe from #6066. That actually paired nicely with this, the code is overall a bit cleaner.

I tested it and things seem to be working properly with multiple directional lights.

This is a useful intermediate step and solves a real problem, but it is also training people to think about DirectionalLights in a non-standard way (and write code that operates under that assumption).

😕 Yeah. Please let me know if you'd like me to add a note to the docs explicitly stating that things might change in the future.

Do we need to adjust DEFAULT_SHADOW_NORMAL_BIAS now? Playing in the shadow_biases example, it seems like an equivalent value to the current default would now be ~0.9

Just tried out the example, is it really supposed to look like this?

image

Tried it out in main and it looks the same, so it probably isn't a result of this PR:

image

Or is that what you mean? That we need to tweak the values somehow so that it doesn't look like this?

@rparrett
Copy link
Contributor

rparrett commented Nov 4, 2022

As far as I know, that example was used to determine the current values for the default shadow bias constants.

I believe the idea is that you use the keys to adjust the biases individually until the shadows don't look bad and then use that value for the default.

But that's what I was seeking clarity from superdump about.

@superdump
Copy link
Contributor

@superdump Do we need to adjust DEFAULT_SHADOW_NORMAL_BIAS now? Playing in the shadow_biases example, it seems like an equivalent value to the current default would now be ~0.9

Why does the normal bias need changing? What changed to need to change it? 🙂 I need to look more closely...

@rparrett
Copy link
Contributor

rparrett commented Nov 4, 2022

Why does the normal bias need changing? What changed to need to change it? 🙂 I need to look more closely...

The sqrt business, I think.

@superdump
Copy link
Contributor

@coreh yeah the shadow biases example is meant to start off looking bad with lots of shadow artifacts so you know what they look like and can learn how the biases impact them.

@superdump
Copy link
Contributor

Why does the normal bias need changing? What changed to need to change it? 🙂 I need to look more closely...

The sqrt business, I think.

We changed from sqrt(2) * max(x, y) to sqrt(x^2 + y^2) practically I think. The sqrt(2) came from assuming square fragment aspect ratio so e.g. x == y and then sqrt(x^2 + x^2) == sqrt(2 * x^2) == sqrt(2) * x == sqrt(2) * max(x, x). I feel like that shouldn't have had a significant impact in default cases. Perhaps it or something else I missed does though.

@rparrett
Copy link
Contributor

rparrett commented Nov 4, 2022

the shadow projection in shadow_biases is very not square.

@rparrett
Copy link
Contributor

rparrett commented Nov 4, 2022

The current 0.6 default still seems like a good value in the lighting example. Is it expected for users to need to tune these biases when the projection shape changes?

I just noticed the different behavior in shadow_biases where it seemed likely the current defaults were determined (because pre-this-PR the current defaults perfectly eliminate artifacts there) and thought they might need to be updated.

Really don't want to derail this if it's very unlikely to affect normal users though.

@cart
Copy link
Member

cart commented Nov 4, 2022

I'm down to merge this. Changing shadow bias behavior (for a given value) should be avoided unless absolutely necessary as artists will carefully tune the value to their specific scenes. It seems like doing it here was necessary though. And changing the default value is less destructive than changing the algorithm, so I'm happy to change that later if we later find a better value.

@cart
Copy link
Member

cart commented Nov 4, 2022

bors r+

bors bot pushed a commit that referenced this pull request Nov 4, 2022
… shadow map volume (not just direction) (#6384)

# Objective

This PR fixes #5789, by enabling movable (and scalable) directional light shadow volumes.

## Solution

This PR changes `ExtractedDirectionalLight` to hold a copy of the `DirectionalLight` entity's `GlobalTransform`, instead of just a `direction` vector. This allows the shadow map volume (as defined by the light's `shadow_projection` field) to be transformed honoring translation _and_ scale transforms, and not just rotation.

It also augments the texel size calculation (used to determine the `shadow_normal_bias`) so that it now takes into account the upper bound of the x/y/z scale of the `GlobalTransform`.

This change makes the directional light extraction code more consistent with point and spot lights (that already use `transform`), and allows easily moving and scaling the shadow volume along with a player entity based on camera distance/angle, immediately enabling more real world use cases until we have a more sophisticated adaptive implementation, such as the one described in #3629.

**Note:** While it was previously possible to update the projection achieving a similar effect, depending on the light direction and distance to the origin, the fact that the shadow map camera was always positioned at the origin with a hardcoded `Vec3::Y` up value meant you would get sub-optimal or inconsistent/incorrect results.

---

## Changelog

### Changed

- `DirectionalLight` shadow volumes now honor translation and scale transforms

## Migration Guide

- If your directional lights were positioned at the origin and not scaled (the default, most common scenario) no changes are needed on your part; it just works as before;
- If you previously had a system for dynamically updating directional light shadow projections, you might now be able to simplify your code by updating the directional light entity's transform instead;
- In the unlikely scenario that a scene with directional lights that previously rendered shadows correctly has missing shadows, make sure your directional lights are positioned at (0, 0, 0) and are not scaled to a size that's too large or too small.
@bors bors bot changed the title Take DirectionalLight's GlobalTransform into account when calculating shadow map volume (not just direction) [Merged by Bors] - Take DirectionalLight's GlobalTransform into account when calculating shadow map volume (not just direction) Nov 4, 2022
@bors bors bot closed this Nov 4, 2022
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
… shadow map volume (not just direction) (bevyengine#6384)

# Objective

This PR fixes bevyengine#5789, by enabling movable (and scalable) directional light shadow volumes.

## Solution

This PR changes `ExtractedDirectionalLight` to hold a copy of the `DirectionalLight` entity's `GlobalTransform`, instead of just a `direction` vector. This allows the shadow map volume (as defined by the light's `shadow_projection` field) to be transformed honoring translation _and_ scale transforms, and not just rotation.

It also augments the texel size calculation (used to determine the `shadow_normal_bias`) so that it now takes into account the upper bound of the x/y/z scale of the `GlobalTransform`.

This change makes the directional light extraction code more consistent with point and spot lights (that already use `transform`), and allows easily moving and scaling the shadow volume along with a player entity based on camera distance/angle, immediately enabling more real world use cases until we have a more sophisticated adaptive implementation, such as the one described in bevyengine#3629.

**Note:** While it was previously possible to update the projection achieving a similar effect, depending on the light direction and distance to the origin, the fact that the shadow map camera was always positioned at the origin with a hardcoded `Vec3::Y` up value meant you would get sub-optimal or inconsistent/incorrect results.

---

## Changelog

### Changed

- `DirectionalLight` shadow volumes now honor translation and scale transforms

## Migration Guide

- If your directional lights were positioned at the origin and not scaled (the default, most common scenario) no changes are needed on your part; it just works as before;
- If you previously had a system for dynamically updating directional light shadow projections, you might now be able to simplify your code by updating the directional light entity's transform instead;
- In the unlikely scenario that a scene with directional lights that previously rendered shadows correctly has missing shadows, make sure your directional lights are positioned at (0, 0, 0) and are not scaled to a size that's too large or too small.
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-Bug An unexpected or incorrect behavior S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Moveable DirectionalLights
8 participants