Skip to content

Correctly handle visibility ranges in shadow maps.#24289

Merged
alice-i-cecile merged 4 commits into
bevyengine:mainfrom
pcwalton:primary-camera
May 18, 2026
Merged

Correctly handle visibility ranges in shadow maps.#24289
alice-i-cecile merged 4 commits into
bevyengine:mainfrom
pcwalton:primary-camera

Conversation

@pcwalton

@pcwalton pcwalton commented May 14, 2026

Copy link
Copy Markdown
Contributor

Right now, visibility ranges are always resolved relative to the view. This is incorrect for shadow maps in two ways:

  1. Visibility ranges for meshes in directional light shadow maps should be resolved relative to the camera that the cascades are associated with.

  2. Visibility ranges for meshes in point and spot light shadow maps should be resolved relative to some camera.

To properly solve (2), this commit introduces the notion of a shadow LOD origin. The shadow LOD origin is the point that visibility ranges are relative to, when rendering views not associated with any camera. Point and spot light shadow maps are currently not associated with a camera, and therefore we need this extra notion in order to properly evaluate visibility ranges.

(As a follow-up, we should introduce the notion of own shadow maps, which will allow each camera to have separate shadow maps for point and spot lights. That feature is however out of scope for this patch, which simply seeks to make the existing semantics consistent.)

A new component, ShadowLodOrigin, has been added, which allows the developer to customize the shadow LOD origin. In the absence of this component, this PR implements a simple heuristic to determine the shadow LOD origin: to prefer an origin that coincides with cameras that render to a window. This heuristic should suffice in the vast majority of cases, so developers will rarely have to manually use the ShadowLodOrigin component.

A new field, lod_view_world_position, has been added to View to supply the position of the shadow LOD origin to the GPU. This is much simpler than introducing a new uniform or using immediates, as #24197 tried to do.

This commit is the proper fix for #23991. PR #24252 attempted to fix this problem by reverting #23115. However, this didn't actually fix the issue, because the semantics were still inconsistent. This commit constitutes the correct fix for the issue. I verified that, after un-reverting #23115 on top of this patch and modifying it to use the new lod_view_world_position, that the issue reported in #23991 disappears.

Right now, visibility ranges are always resolved relative to the view.
This is incorrect for shadow maps in two ways:

1. Visibility ranges for meshes in directional light shadow maps should
   be resolved relative to the camera that the cascades are associated
   with.

2. Visibility ranges for meshes in point and spot light shadow maps
   should be resolved relative to *some* camera.

To properly solve (2), this commit introduces the notion of a *primary
camera*. The primary camera is the one that visibility ranges are
relative to, when rendering views not associated with any camera. Point
and spot light shadow maps are currently not associated with a camera,
and therefore we need this extra notion in order to properly evaluate
visibility ranges.

(As a follow-up, we should introduce the notion of *own shadow maps*,
which will allow each camera to have separate shadow maps for point and
spot lights. That feature is however out of scope for *this* patch,
which simply seeks to make the existing semantics consistent.)

A new component, `PrimaryCamera`, has been added, which allows the
developer to customize which camera is primary. In the absence of this
component, this PR implements a simple heuristic to determine the
primary camera: to prefer cameras that render to a window. This
heuristic should suffice in the vast majority of cases, so developers
will rarely have to manually use the `PrimaryCamera` component.

A new field, `lod_view_world_position`, has been added to `View` to
supply the position of the primary camera to the GPU. This is much
simpler than introducing a new uniform or using immediates, as bevyengine#24197
tried to do.

This commit is the proper fix for bevyengine#23991. PR bevyengine#24252 attempted to fix
this problem by reverting bevyengine#23115. However, this didn't actually fix the
issue, because the semantics were still inconsistent. This commit
constitutes the correct fix for the issue. I verified that, after
un-reverting bevyengine#23115 on top of this patch and modifying it to use the new
`lod_view_world_position`, that the issue reported in bevyengine#23991 disappears.
@pcwalton pcwalton requested review from JMS55 and alice-i-cecile May 14, 2026 02:57
@pcwalton pcwalton added the A-Rendering Drawing game state to the screen label May 14, 2026
@pcwalton pcwalton added the C-Bug An unexpected or incorrect behavior label May 14, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering May 14, 2026
@pcwalton pcwalton added this to the 0.19 milestone May 14, 2026
@pcwalton

pcwalton commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

You may be wondering why reverting #23115 seemed to work. The answer is that, when CPU culling is in use, all meshes are submitted to the GPU if they're visible in any camera, as determined by the visibility ranges. This means that the visibility ranges mostly work for the shadows in the visibility_ranges example, but they will break if multiple cameras are present, or if point or spot light shadow maps are used. This PR constitutes the correct fix.

After this PR lands, I intend to submit a follow-up PR to unrevert #23115, updated to use the new lod_view_world_position field.

@beicause

Copy link
Copy Markdown
Member

Should CPU culling for point light shadow maps be changed to be relative to PrimaryCamera, too?

@pcwalton

Copy link
Copy Markdown
Contributor Author

It should effectively be already, because the "CPU culling" is really just culling shadow maps that aren't in view of any camera (see VisibilityRange::is_visible_at_all), and that is compatible with the semantics here.

@alice-i-cecile alice-i-cecile requested a review from kfc35 May 14, 2026 05:33
@alice-i-cecile alice-i-cecile added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label May 14, 2026
@beicause

Copy link
Copy Markdown
Member

It should effectively be already, because the "CPU culling" is really just culling shadow maps that aren't in view of any camera (see VisibilityRange::is_visible_at_all), and that is compatible with the semantics here.

Maybe I'm wrong, but shouldn't we cull meshes that aren't in view of PrimaryCamera here instead of any camera?

!visible_entity_ranges.entity_is_in_range_of_any_view(entity)

Comment thread crates/bevy_camera/src/camera.rs Outdated
/// split-screen game.
#[derive(Clone, Copy, Component, Default, Debug, Reflect)]
#[reflect(Clone, Component, Default)]
pub struct PrimaryCamera;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm nervous about the introduction of this. My understanding is that per-camera shadow maps is likely to always be too expensive.

This design feels fundamentally wrong for split-screen games, even though it works for many others.

Could we change the semantics here to allow for tagging multiple cameras, and then taking the union of shadows that should be seen by any of these cameras? We'd need to rename this component obviously. We can even bound the max number of cameras used for this comfortably: 4 or 8 is plenty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That won't work for GPU driven because you would have a sequential loop over an array for every mesh in the shader. The right solution is has_own_point_and_spot_light_shadow_maps, which is something I'll implement after 0.19.

@pcwalton pcwalton May 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In other words, per-camera shadow maps aren't necessarily too expensive. It really depends on the scene. There's a general feeling that they're too expensive by default, but we should support them at least on an opt-in basis. cart and Griffin disagree as to whether they should be enabled by default; I'm neutral on that question and simply think we should support both.

Comment thread crates/bevy_camera/src/camera.rs Outdated
/// Placing this component on a [`Camera`] makes that camera the primary one for
/// the purposes of shadow mapping of point and spot lights.
///
/// The exact algorithm that Bevy uses to determine a primary camera is as

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I really like this fallback strategy: good defaults with manual control.

@kfc35

kfc35 commented May 14, 2026

Copy link
Copy Markdown
Contributor

It should effectively be already, because the "CPU culling" is really just culling shadow maps that aren't in view of any camera (see VisibilityRange::is_visible_at_all), and that is compatible with the semantics here.

Maybe I'm wrong, but shouldn't we cull meshes that aren't in view of PrimaryCamera here instead of any camera?

!visible_entity_ranges.entity_is_in_range_of_any_view(entity)

IIUC this isn't absolutely necessary because that filter isnt excessively filtering out meshes, and the logic inside this PR will cover any meshes / shadows that absolutely should be culled. It would be redundant to put the logic there too (but i could understand wanting the logic to be consistent for consistency's sake). This is just my understanding of the code though, may be flawed!

@pcwalton

Copy link
Copy Markdown
Contributor Author

That check is redundant (or would be if we had proper CPU culling for shadow maps--but I'll do that in a followup), but I would still like to keep it because it avoids overhead of render world extraction for meshes that aren't going to be visible anyway. Extraction from the main world to the render world can be expensive and it's nice to be able to avoid it when we can.

camera; fix CPU visibility range computation; pick the lowest entity ID
in the case of a tie
@pcwalton

Copy link
Copy Markdown
Contributor Author

Maybe I'm wrong, but shouldn't we cull meshes that aren't in view of PrimaryCamera here instead of any camera?

!visible_entity_ranges.entity_is_in_range_of_any_view(entity)

Yes, I changed this.

@pcwalton

Copy link
Copy Markdown
Contributor Author
  1. I renamed PrimaryCamera to ShadowLodOrigin per discussion in #rendering-dev on Discord.

  2. ShadowLodOrigin no longer needs to be a camera.

  3. We now sort the query by entity ID and choose the lowest one in the case of a tie. This ensures that the chosen camera remains consistent.

  4. A warning is now emitted if point/spot lights are present and neither the ShadowLodOrigin nor any camera that renders to the window are present.

Comment thread crates/bevy_light/src/lib.rs Outdated
@urben1680

urben1680 commented May 15, 2026

Copy link
Copy Markdown
Contributor

We now sort the query by entity ID and choose the lowest one in the case of a tie. This ensures that the chosen camera remains consistent.

I may miss some context here but consistent in which way? PartialOrd of Entity seems to work on the entity index value as the higher bits and the generation as the lower bits. If I spawn a new camera that may have a lower or higher entity index than existing cameras.

@kfc35 kfc35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great! I tested out the example with a spotlight and it works as expected

Comment thread crates/bevy_camera/src/camera.rs
Comment thread crates/bevy_camera/src/camera.rs Outdated
@kfc35 kfc35 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 15, 2026
@pcwalton

Copy link
Copy Markdown
Contributor Author

@urben1680 I mean consistent if you never spawn or despawn cameras, even if you do all sorts of other things to change archetypes and so forth.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 18, 2026
Merged via the queue into bevyengine:main with commit 74d39c6 May 18, 2026
42 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in Rendering May 18, 2026
pull Bot pushed a commit to VitalyAnkh/bevy that referenced this pull request May 19, 2026
…evyengine#24343)

This PR undoes the revert of bevyengine#23115 that was done in bevyengine#24252. As part of
doing so, it makes GPU-driven visibility range evaluation use the same
semantics, introduced in bevyengine#24289, as CPU-driven visibility range
evaluation.

The first commit in this PR is the un-revert, and the second commit is
the change to use the new machinery bevyengine#24289. This means that you can
simply review the second commit (which is very short).

To test, run the `visibility_range` example with `--no-cpu-culling`, and
use the WASD keys to move behind the flight helmet and zoom out, while
closely watching the shadow. Note that the shadow now properly reflects
the LOD of the model.
@laundmo

laundmo commented May 19, 2026

Copy link
Copy Markdown
Member

This causes a panic on startup when using bevy_render but not bevy_pbr, for example when using the 2d feature, due to the new RenderShadowLodOrigin only being registered in PbrPlugin but used by ViewPlugin

MrGVSV pushed a commit to MrGVSV/bevy that referenced this pull request May 20, 2026
… rendering. (bevyengine#24359)

PR bevyengine#24289 introduced the `RenderShadowLodOrigin` resource, which is used
to construct the `View` GPU resource. Because shadow maps are a PBR
concept, that resource is only constructed if the PBR plugin is present.
But we share the `View` structure between the 2D and 3D paths. Thus,
when constructing the `View` structure, we should gracefully handle the
potential absence of the `RenderShadowLodOrigin` resource. This PR does
that.

Closes bevyengine#24352.
mockersf pushed a commit to mockersf/bevy that referenced this pull request May 21, 2026
Right now, visibility ranges are always resolved relative to the view.
This is incorrect for shadow maps in two ways:

1. Visibility ranges for meshes in directional light shadow maps should
be resolved relative to the camera that the cascades are associated
with.

2. Visibility ranges for meshes in point and spot light shadow maps
should be resolved relative to *some* camera.

To properly solve (2), this commit introduces the notion of a *shadow
LOD origin*. The shadow LOD origin is the point that visibility ranges
are relative to, when rendering views not associated with any camera.
Point and spot light shadow maps are currently not associated with a
camera, and therefore we need this extra notion in order to properly
evaluate visibility ranges.

(As a follow-up, we should introduce the notion of *own shadow maps*,
which will allow each camera to have separate shadow maps for point and
spot lights. That feature is however out of scope for *this* patch,
which simply seeks to make the existing semantics consistent.)

A new component, `ShadowLodOrigin`, has been added, which allows the
developer to customize the shadow LOD origin. In the absence of this
component, this PR implements a simple heuristic to determine the shadow
LOD origin: to prefer an origin that coincides with cameras that render
to a window. This heuristic should suffice in the vast majority of
cases, so developers will rarely have to manually use the
`ShadowLodOrigin` component.

A new field, `lod_view_world_position`, has been added to `View` to
supply the position of the shadow LOD origin to the GPU. This is much
simpler than introducing a new uniform or using immediates, as bevyengine#24197
tried to do.

This commit is the proper fix for bevyengine#23991. PR bevyengine#24252 attempted to fix
this problem by reverting bevyengine#23115. However, this didn't actually fix the
issue, because the semantics were still inconsistent. This commit
constitutes the correct fix for the issue. I verified that, after
un-reverting bevyengine#23115 on top of this patch and modifying it to use the new
`lod_view_world_position`, that the issue reported in bevyengine#23991 disappears.
mockersf pushed a commit to mockersf/bevy that referenced this pull request May 21, 2026
…evyengine#24343)

This PR undoes the revert of bevyengine#23115 that was done in bevyengine#24252. As part of
doing so, it makes GPU-driven visibility range evaluation use the same
semantics, introduced in bevyengine#24289, as CPU-driven visibility range
evaluation.

The first commit in this PR is the un-revert, and the second commit is
the change to use the new machinery bevyengine#24289. This means that you can
simply review the second commit (which is very short).

To test, run the `visibility_range` example with `--no-cpu-culling`, and
use the WASD keys to move behind the flight helmet and zoom out, while
closely watching the shadow. Note that the shadow now properly reflects
the LOD of the model.
mockersf pushed a commit to mockersf/bevy that referenced this pull request May 21, 2026
… rendering. (bevyengine#24359)

PR bevyengine#24289 introduced the `RenderShadowLodOrigin` resource, which is used
to construct the `View` GPU resource. Because shadow maps are a PBR
concept, that resource is only constructed if the PBR plugin is present.
But we share the `View` structure between the 2D and 3D paths. Thus,
when constructing the `View` structure, we should gracefully handle the
potential absence of the `RenderShadowLodOrigin` resource. This PR does
that.

Closes bevyengine#24352.
mockersf pushed a commit that referenced this pull request May 21, 2026
Right now, visibility ranges are always resolved relative to the view.
This is incorrect for shadow maps in two ways:

1. Visibility ranges for meshes in directional light shadow maps should
be resolved relative to the camera that the cascades are associated
with.

2. Visibility ranges for meshes in point and spot light shadow maps
should be resolved relative to *some* camera.

To properly solve (2), this commit introduces the notion of a *shadow
LOD origin*. The shadow LOD origin is the point that visibility ranges
are relative to, when rendering views not associated with any camera.
Point and spot light shadow maps are currently not associated with a
camera, and therefore we need this extra notion in order to properly
evaluate visibility ranges.

(As a follow-up, we should introduce the notion of *own shadow maps*,
which will allow each camera to have separate shadow maps for point and
spot lights. That feature is however out of scope for *this* patch,
which simply seeks to make the existing semantics consistent.)

A new component, `ShadowLodOrigin`, has been added, which allows the
developer to customize the shadow LOD origin. In the absence of this
component, this PR implements a simple heuristic to determine the shadow
LOD origin: to prefer an origin that coincides with cameras that render
to a window. This heuristic should suffice in the vast majority of
cases, so developers will rarely have to manually use the
`ShadowLodOrigin` component.

A new field, `lod_view_world_position`, has been added to `View` to
supply the position of the shadow LOD origin to the GPU. This is much
simpler than introducing a new uniform or using immediates, as #24197
tried to do.

This commit is the proper fix for #23991. PR #24252 attempted to fix
this problem by reverting #23115. However, this didn't actually fix the
issue, because the semantics were still inconsistent. This commit
constitutes the correct fix for the issue. I verified that, after
un-reverting #23115 on top of this patch and modifying it to use the new
`lod_view_world_position`, that the issue reported in #23991 disappears.
mockersf pushed a commit that referenced this pull request May 21, 2026
…24343)

This PR undoes the revert of #23115 that was done in #24252. As part of
doing so, it makes GPU-driven visibility range evaluation use the same
semantics, introduced in #24289, as CPU-driven visibility range
evaluation.

The first commit in this PR is the un-revert, and the second commit is
the change to use the new machinery #24289. This means that you can
simply review the second commit (which is very short).

To test, run the `visibility_range` example with `--no-cpu-culling`, and
use the WASD keys to move behind the flight helmet and zoom out, while
closely watching the shadow. Note that the shadow now properly reflects
the LOD of the model.
mockersf pushed a commit that referenced this pull request May 21, 2026
… rendering. (#24359)

PR #24289 introduced the `RenderShadowLodOrigin` resource, which is used
to construct the `View` GPU resource. Because shadow maps are a PBR
concept, that resource is only constructed if the PBR plugin is present.
But we share the `View` structure between the 2D and 3D paths. Thus,
when constructing the `View` structure, we should gracefully handle the
potential absence of the `RenderShadowLodOrigin` resource. This PR does
that.

Closes #24352.
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

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants