Skip to content

Commit 74d39c6

Browse files
authored
Correctly handle visibility ranges in shadow maps. (#24289)
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.
1 parent fe872da commit 74d39c6

10 files changed

Lines changed: 209 additions & 24 deletions

File tree

crates/bevy_camera/src/camera.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,51 @@ impl Camera {
811811
}
812812
}
813813

814+
/// The entity that Bevy uses to resolve visibility ranges when no specific
815+
/// camera is applicable.
816+
///
817+
/// For efficiency, Bevy currently renders point and spot light shadow maps only
818+
/// once per frame, regardless of the number of cameras in use, rather than
819+
/// rendering the shadow maps anew for each camera each frame. Most of the time,
820+
/// this optimization doesn't change the result relative to a rendering that
821+
/// rendered such shadow maps separately for each camera. However, there's one
822+
/// exception: visibility ranges. Visibility ranges cause meshes to be visible
823+
/// or invisible depending on the distance from the mesh to the camera. When
824+
/// rendering a shadow map for a point or spot light, Bevy must therefore select
825+
/// an entity to use as the reference point for the purposes of visibility
826+
/// ranges. This entity is called the *LOD origin*.
827+
///
828+
/// Placing this component on an entity makes that entity the origin from which
829+
/// LOD distances are computed for the purposes of shadow mapping of point and
830+
/// spot lights. Typically, you place this component on a camera, but you may
831+
/// place it on another entity if you wish.
832+
///
833+
/// The exact algorithm that Bevy uses to determine the LOD origin is as
834+
/// follows. Once the LOD origin is determined, all further steps are skipped.
835+
///
836+
/// 1. If an entity has this [`ShadowLodOrigin`] component, then it's the LOD
837+
/// origin. If there's more than one entity with the [`ShadowLodOrigin`]
838+
/// component, one is chosen arbitrarily in a manner that's stable from frame to
839+
/// frame.
840+
///
841+
/// 2. If a camera renders to a window (that is, the camera's [`RenderTarget`]
842+
/// is [`RenderTarget::Window`]), then that camera is the shadow LOD origin. If
843+
/// there's more than one such camera that renders to a window, then one is
844+
/// chosen arbitrarily in a manner that's stable from frame to frame.
845+
///
846+
/// 3. A camera is chosen to be the LOD origin arbitrarily from all cameras in
847+
/// the scene in a manner that's stable from frame to frame.
848+
///
849+
/// This algorithm means that, in most cases, you don't need to add this
850+
/// [`ShadowLodOrigin`] component explicitly to the scene; usually, Bevy chooses
851+
/// the right origin automatically. You only need to use this component
852+
/// explicitly if you have multiple cameras rendering to the window: e.g. in a
853+
/// split-screen game.
854+
#[derive(Clone, Copy, Default, Component, Debug, Reflect)]
855+
#[reflect(Clone, Default, Component)]
856+
#[require(Transform)]
857+
pub struct ShadowLodOrigin;
858+
814859
/// Control how this [`Camera`] outputs once rendering is completed.
815860
#[derive(Debug, Clone, Copy, Reflect)]
816861
pub enum CameraOutputMode {

crates/bevy_camera/src/visibility/range.rs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bevy_app::{App, Plugin, PostUpdate};
1010
use bevy_ecs::{
1111
component::Component,
1212
entity::{Entity, EntityHashMap},
13-
query::With,
13+
query::{Or, With},
1414
reflect::ReflectComponent,
1515
resource::Resource,
1616
schedule::IntoScheduleConfigs as _,
@@ -22,7 +22,7 @@ use bevy_transform::components::GlobalTransform;
2222
use bevy_utils::Parallel;
2323

2424
use super::{check_visibility_cpu_culling, VisibilitySystems};
25-
use crate::{camera::Camera, primitives::Aabb};
25+
use crate::{camera::Camera, primitives::Aabb, ShadowLodOrigin};
2626

2727
/// A plugin that enables [`VisibilityRange`]s, which allow entities to be
2828
/// hidden or shown based on distance to the camera.
@@ -220,18 +220,6 @@ impl VisibleEntityRanges {
220220
};
221221
(visibility_bitmask & (1 << view_index)) != 0
222222
}
223-
224-
/// Returns true if the entity is in range of any view.
225-
///
226-
/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
227-
/// occlusion culling. Thus the entity might not *actually* be visible.
228-
///
229-
/// The entity is assumed to have a [`VisibilityRange`] component. If the
230-
/// entity doesn't have that component, this method will return false.
231-
#[inline]
232-
pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool {
233-
self.entities.contains_key(&entity)
234-
}
235223
}
236224

237225
/// Checks all entities against all views in order to determine which entities
@@ -241,7 +229,7 @@ impl VisibleEntityRanges {
241229
/// cull.
242230
pub fn check_visibility_ranges(
243231
mut visible_entity_ranges: ResMut<VisibleEntityRanges>,
244-
view_query: Query<(Entity, &GlobalTransform), With<Camera>>,
232+
view_query: Query<(Entity, &GlobalTransform), Or<(With<Camera>, With<ShadowLodOrigin>)>>,
245233
mut par_local: Local<Parallel<Vec<(Entity, u32)>>>,
246234
entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>,
247235
) {

crates/bevy_light/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ bevy_color = { path = "../bevy_color", version = "0.19.0-dev", features = [
2525
"serialize",
2626
] }
2727
bevy_gizmos = { path = "../bevy_gizmos", version = "0.19.0-dev", optional = true }
28+
bevy_log = { path = "../bevy_log", version = "0.19.0-dev" }
2829

2930
# other
3031
tracing = { version = "0.1", default-features = false }

crates/bevy_light/src/lib.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ use bevy_camera::{
1313
NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems,
1414
VisibleEntities, VisibleEntityRanges, VisibleMeshEntities,
1515
},
16-
Camera3d, CameraUpdateSystems,
16+
Camera, Camera3d, CameraUpdateSystems, RenderTarget, ShadowLodOrigin,
1717
};
18-
use bevy_ecs::{entity::EntityHashSet, prelude::*};
18+
use bevy_ecs::{entity::EntityHashSet, prelude::*, system::QueryLens};
1919
#[cfg(feature = "bevy_gizmos")]
2020
use bevy_gizmos::frustum::FrustumGizmoSystems;
21+
use bevy_log::warn_once;
2122
use bevy_math::Vec3A;
2223
use bevy_mesh::Mesh3d;
2324
use bevy_reflect::prelude::*;
2425
use bevy_transform::{components::GlobalTransform, TransformSystems};
2526
use bevy_utils::Parallel;
2627
use core::{any::TypeId, mem, ops::DerefMut};
28+
use smallvec::{smallvec, SmallVec};
2729

2830
pub mod cluster;
2931
use cluster::assign::assign_objects_to_clusters;
@@ -533,13 +535,22 @@ pub fn check_point_light_mesh_visibility(
533535
With<Mesh3d>,
534536
),
535537
>,
538+
mut camera_query: Query<(Entity, &RenderTarget), With<Camera>>,
539+
mut shadow_lod_origin_query: Query<Entity, With<ShadowLodOrigin>>,
540+
mut point_and_spot_light_query: Query<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
536541
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
537542
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
538543
mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
539544
mut checked_lights: Local<EntityHashSet>,
540545
) {
541546
checked_lights.clear();
542547

548+
let shadow_lod_origin = get_shadow_lod_origin(
549+
camera_query.transmute_lens_filtered(),
550+
shadow_lod_origin_query.transmute_lens_filtered(),
551+
point_and_spot_light_query.transmute_lens_filtered(),
552+
);
553+
543554
let visible_entity_ranges = visible_entity_ranges.as_deref();
544555
for visible_lights in &visible_point_lights {
545556
for &light_entity in visible_lights.get(TypeId::of::<ClusterVisibilityClass>()) {
@@ -589,7 +600,10 @@ pub fn check_point_light_mesh_visibility(
589600
}
590601
if has_visibility_range
591602
&& visible_entity_ranges.is_some_and(|visible_entity_ranges| {
592-
!visible_entity_ranges.entity_is_in_range_of_any_view(entity)
603+
shadow_lod_origin.is_none_or(|shadow_lod_origin| {
604+
!visible_entity_ranges
605+
.entity_is_in_range_of_view(entity, shadow_lod_origin)
606+
})
593607
})
594608
{
595609
return;
@@ -679,7 +693,10 @@ pub fn check_point_light_mesh_visibility(
679693
// Check visibility ranges.
680694
if has_visibility_range
681695
&& visible_entity_ranges.is_some_and(|visible_entity_ranges| {
682-
!visible_entity_ranges.entity_is_in_range_of_any_view(entity)
696+
shadow_lod_origin.is_none_or(|shadow_lod_origin| {
697+
!visible_entity_ranges
698+
.entity_is_in_range_of_view(entity, shadow_lod_origin)
699+
})
683700
})
684701
{
685702
return;
@@ -717,3 +734,54 @@ pub fn check_point_light_mesh_visibility(
717734
}
718735
}
719736
}
737+
738+
/// Determines the LOD origin for spot and point light shadow maps.
739+
///
740+
/// The selection priority is, from highest to lowest:
741+
///
742+
/// 1. An entity explicitly marked with the [`ShadowLodOrigin`] component.
743+
///
744+
/// 2. A camera that renders to a window.
745+
///
746+
/// 3. Any camera.
747+
pub fn get_shadow_lod_origin(
748+
mut camera_query: QueryLens<(Entity, &RenderTarget), With<Camera>>,
749+
mut shadow_lod_origin_query: QueryLens<Entity, With<ShadowLodOrigin>>,
750+
mut lights_query: QueryLens<Entity, Or<(With<PointLight>, With<SpotLight>)>>,
751+
) -> Option<Entity> {
752+
let (camera_query, shadow_lod_origin_query) =
753+
(camera_query.query(), shadow_lod_origin_query.query());
754+
755+
let mut entities: SmallVec<[Entity; 4]> = smallvec![];
756+
entities.extend(shadow_lod_origin_query.iter());
757+
if let Some(lod_origin) = entities.iter().min() {
758+
return Some(*lod_origin);
759+
}
760+
761+
entities.extend(
762+
camera_query
763+
.iter()
764+
.filter_map(|(main_entity, render_target)| match *render_target {
765+
RenderTarget::Window(_) => Some(main_entity),
766+
_ => None,
767+
}),
768+
);
769+
if let Some(lod_origin) = entities.iter().min() {
770+
return Some(*lod_origin);
771+
};
772+
773+
entities.extend(camera_query.iter().map(|(main_entity, _)| main_entity));
774+
if let Some(lod_origin) = entities.iter().min() {
775+
if !lights_query.query().is_empty() {
776+
warn_once!(
777+
"Point lights and/or spot lights are present, but no entity has \
778+
`ShadowLodOrigin`, and no camera that renders to the window has been found. \
779+
Consider using the `ShadowLodOrigin` component to set a LOD origin."
780+
);
781+
}
782+
783+
return Some(*lod_origin);
784+
};
785+
786+
None
787+
}

crates/bevy_pbr/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ mod gltf;
3232
use bevy_light::cluster::GlobalClusterSettings;
3333
use bevy_render::{
3434
sync_component::SyncComponent,
35-
view::{RenderExtractedShadowMapVisibleEntities, RenderShadowMapVisibleEntities},
35+
view::{
36+
RenderExtractedShadowMapVisibleEntities, RenderShadowLodOrigin,
37+
RenderShadowMapVisibleEntities,
38+
},
3639
};
3740
pub use contact_shadows::{
3841
ContactShadows, ContactShadowsBuffer, ContactShadowsPlugin, ContactShadowsUniform,
@@ -386,6 +389,7 @@ impl Plugin for PbrPlugin {
386389
extract_ambient_light_resource,
387390
extract_ambient_light,
388391
extract_shadow_filtering_method,
392+
extract_shadow_lod_origin,
389393
late_sweep_material_instances,
390394
),
391395
)
@@ -406,6 +410,7 @@ impl Plugin for PbrPlugin {
406410
)
407411
.init_gpu_resource::<LightMeta>()
408412
.init_gpu_resource::<RenderMaterialBindings>()
413+
.init_resource::<RenderShadowLodOrigin>()
409414
.allow_ambiguous_resource::<RenderMaterialBindings>();
410415

411416
render_app.world_mut().add_observer(add_light_view_entities);

crates/bevy_pbr/src/render/light.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use bevy_camera::visibility::{
88
CascadesVisibleEntities, CubemapVisibleEntities, RenderLayers, ViewVisibility,
99
VisibleMeshEntities,
1010
};
11-
use bevy_camera::Camera3d;
11+
use bevy_camera::{Camera, Camera3d, RenderTarget, ShadowLodOrigin};
1212
use bevy_color::ColorToComponents;
1313
use bevy_core_pipeline::core_3d::CORE_3D_DEPTH_FORMAT;
1414
use bevy_core_pipeline::schedule::RootNonCameraView;
@@ -47,8 +47,8 @@ use bevy_render::occlusion_culling::{
4747
};
4848
use bevy_render::sync_world::{MainEntity, MainEntityHashMap, RenderEntity};
4949
use bevy_render::view::{
50-
RenderExtractedShadowMapVisibleEntities, RenderShadowMapVisibleEntities, RenderVisibleEntities,
51-
VisibilityExtractionSystemParam,
50+
RenderExtractedShadowMapVisibleEntities, RenderShadowLodOrigin, RenderShadowMapVisibleEntities,
51+
RenderVisibleEntities, VisibilityExtractionSystemParam,
5252
};
5353
use bevy_render::{
5454
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
@@ -2888,3 +2888,30 @@ fn get_shadow_map_visible_entities<'w, 's: 'w>(
28882888
}
28892889
}
28902890
}
2891+
2892+
/// An extraction system that determines the origin for LOD computation for
2893+
/// point and spot light shadow maps and updates the [`RenderShadowLodOrigin`]
2894+
/// with the result.
2895+
///
2896+
/// See [`ShadowLodOrigin`] for more details on the algorithm that this system
2897+
/// uses.
2898+
pub fn extract_shadow_lod_origin(
2899+
global_transform_query: Extract<Query<&GlobalTransform>>,
2900+
mut camera_query: Extract<Query<(Entity, &RenderTarget), With<Camera>>>,
2901+
mut shadow_lod_origin_query: Extract<Query<Entity, With<ShadowLodOrigin>>>,
2902+
mut lights_query: Extract<Query<Entity, Or<(With<PointLight>, With<SpotLight>)>>>,
2903+
mut render_shadow_lod_origin: ResMut<RenderShadowLodOrigin>,
2904+
) {
2905+
match bevy_light::get_shadow_lod_origin(
2906+
camera_query.transmute_lens_filtered(),
2907+
shadow_lod_origin_query.transmute_lens_filtered(),
2908+
lights_query.transmute_lens_filtered(),
2909+
)
2910+
.and_then(|shadow_lod_origin_entity| global_transform_query.get(shadow_lod_origin_entity).ok())
2911+
{
2912+
Some(global_transform) => {
2913+
render_shadow_lod_origin.0 = global_transform.translation();
2914+
}
2915+
None => render_shadow_lod_origin.0 = Default::default(),
2916+
}
2917+
}

crates/bevy_pbr/src/render/mesh_functions.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4<f
146146
}
147147

148148
let lod_range = visibility_ranges[visibility_buffer_index];
149-
let camera_distance = length(view.world_position.xyz - world_position.xyz);
149+
let camera_distance = length(view.lod_view_world_position.xyz - world_position.xyz);
150150

151151
// This encodes the following mapping:
152152
//

crates/bevy_render/src/camera.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ pub fn extract_cameras(
520520
NoIndirectDrawing,
521521
ViewUniformOffset,
522522
);
523+
523524
for (
524525
main_entity,
525526
render_entity,

0 commit comments

Comments
 (0)