Skip to content

Commit e59503c

Browse files
authored
Render point and spot light shadow maps only once, regardless of the number of cameras. (#23713)
The intention has long been to render shadow maps for point and spot lights only once, regardless of the number of views. This is reflected in the fact that `RetainedViewEntity::auxiliary_entity` is `Entity::PLACEHOLDER` for them. Unfortunately, this is currently inconsistently implemented, and a separate `ExtractedView` is presently spawned and rendered to for every point and spot light shadow map. The behavior of these views is inconsistent because they violate the invariant that there must only be one render-world view per `RetainedViewEntity`. This patch changes Bevy's behavior to spawn only one `ExtractedView` for point and spot lights. This required some significant rearchitecting of the render schedule because the render schedule is currently driven off cameras. Driving the rendering off cameras is incorrect for point and spot light shadow maps, which aren't associated with any camera. This PR fixes the regression on the `render_layers` test in `testbed_3d` in PR #23481, in that it renders the way it rendered before that PR. Note, however, that the rendering isn't what may have been intended: the shadows don't match the visible objects. That's because the shadows come from point lights, which aren't associated with cameras, and therefore shadows are rendered using the default set of `RenderLayers`. A future patch may want to add flags to cameras that specify that they should have their own point light and spot light shadow maps that inherit the render layer (and HLOD) behavior of their associated cameras. As this patch is fairly large, though, and because my immediate goal is to fix the regression in #23481, I think those flags are best implemented in a follow-up. <img width="2564" height="1500" alt="Screenshot 2026-04-07 215221" src="https://github.com/user-attachments/assets/2b37f35c-84e7-4473-9962-968a8cbd7619" />
1 parent 4d9302b commit e59503c

10 files changed

Lines changed: 874 additions & 728 deletions

File tree

crates/bevy_core_pipeline/src/schedule.rs

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
//! The [`camera_driver`] system is responsible for iterating over all cameras in the world
1010
//! and executing their associated schedules. In this way, the schedule for each camera is a
1111
//! sub-schedule or sub-graph of the root render graph schedule.
12+
use core::fmt::{self, Display, Formatter};
13+
1214
use bevy_camera::{ClearColor, NormalizedRenderTarget};
1315
use bevy_ecs::{
1416
entity::EntityHashSet,
1517
prelude::*,
16-
schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, SystemSet},
18+
schedule::{InternedScheduleLabel, IntoScheduleConfigs, Schedule, ScheduleLabel, SystemSet},
1719
};
1820
use bevy_log::info_span;
21+
use bevy_reflect::Reflect;
1922
use bevy_render::{
2023
camera::{ExtractedCamera, SortedCameras},
2124
render_resource::{
@@ -108,6 +111,16 @@ impl Core2d {
108111
#[derive(Resource)]
109112
struct CameraWindows(EntityHashSet);
110113

114+
/// A render-world marker component for a view that corresponds to neither a
115+
/// camera nor a camera-associated shadow map.
116+
///
117+
/// This is used for point light and spot light shadow maps, since these aren't
118+
/// associated with views.
119+
#[derive(Clone, Copy, Component, Debug, Reflect)]
120+
#[reflect(Clone, Component)]
121+
#[reflect(from_reflect = false)]
122+
pub struct RootNonCameraView(#[reflect(ignore)] pub InternedScheduleLabel);
123+
111124
/// The default entry point for camera driven rendering added to the root [`bevy_render::renderer::RenderGraph`]
112125
/// schedule. This system iterates over all cameras in the world, executing their associated
113126
/// rendering schedules defined by the [`bevy_render::camera::CameraRenderGraph`] component.
@@ -117,49 +130,71 @@ struct CameraWindows(EntityHashSet);
117130
/// operations (e.g. one-off compute passes) before or after this system in the root render
118131
/// graph schedule.
119132
pub fn camera_driver(world: &mut World) {
120-
let sorted_cameras: Vec<_> = {
133+
// Gather up all cameras and auxiliary views not associated with a camera.
134+
let root_views: Vec<_> = {
135+
let mut auxiliary_views = world.query_filtered::<Entity, With<RootNonCameraView>>();
121136
let sorted = world.resource::<SortedCameras>();
122-
sorted.0.iter().map(|c| (c.entity, c.order)).collect()
137+
auxiliary_views
138+
.iter(world)
139+
.map(RootView::Auxiliary)
140+
.chain(sorted.0.iter().map(|c| RootView::Camera {
141+
entity: c.entity,
142+
order: c.order,
143+
}))
144+
.collect()
123145
};
124146

125147
let mut camera_windows = EntityHashSet::default();
126148

127-
for camera in sorted_cameras {
128-
#[cfg(feature = "trace")]
129-
let (camera_entity, order) = camera;
130-
#[cfg(not(feature = "trace"))]
131-
let (camera_entity, _) = camera;
132-
let Some(camera) = world.get::<ExtractedCamera>(camera_entity) else {
133-
continue;
134-
};
149+
for root_view in root_views {
150+
let mut run_schedule = true;
151+
let (schedule, view_entity);
152+
153+
match root_view {
154+
RootView::Camera {
155+
entity: camera_entity,
156+
..
157+
} => {
158+
let Some(camera) = world.get::<ExtractedCamera>(camera_entity) else {
159+
continue;
160+
};
161+
162+
schedule = camera.schedule;
163+
let target = camera.target.clone();
164+
165+
if let Some(NormalizedRenderTarget::Window(window_ref)) = &target {
166+
let window_entity = window_ref.entity();
167+
let windows = world.resource::<ExtractedWindows>();
168+
if windows
169+
.windows
170+
.get(&window_entity)
171+
.is_some_and(|w| w.physical_width > 0 && w.physical_height > 0)
172+
{
173+
camera_windows.insert(window_entity);
174+
} else {
175+
run_schedule = false;
176+
}
177+
}
178+
179+
view_entity = camera_entity;
180+
}
135181

136-
let schedule = camera.schedule;
137-
let target = camera.target.clone();
182+
RootView::Auxiliary(auxiliary_view_entity) => {
183+
let Some(root_view) = world.get::<RootNonCameraView>(auxiliary_view_entity) else {
184+
continue;
185+
};
138186

139-
let mut run_schedule = true;
140-
if let Some(NormalizedRenderTarget::Window(window_ref)) = &target {
141-
let window_entity = window_ref.entity();
142-
let windows = world.resource::<ExtractedWindows>();
143-
if windows
144-
.windows
145-
.get(&window_entity)
146-
.is_some_and(|w| w.physical_width > 0 && w.physical_height > 0)
147-
{
148-
camera_windows.insert(window_entity);
149-
} else {
150-
run_schedule = false;
187+
view_entity = auxiliary_view_entity;
188+
schedule = root_view.0;
151189
}
152190
}
153191

154192
if run_schedule {
155-
world.insert_resource(CurrentView(camera_entity));
193+
world.insert_resource(CurrentView(view_entity));
156194

157195
#[cfg(feature = "trace")]
158-
let _span = bevy_log::info_span!(
159-
"camera_schedule",
160-
camera = format!("Camera {} ({:?})", order, camera_entity)
161-
)
162-
.entered();
196+
let _span =
197+
bevy_log::info_span!("camera_schedule", camera = root_view.to_string()).entered();
163198

164199
world.run_schedule(schedule);
165200
}
@@ -169,6 +204,26 @@ pub fn camera_driver(world: &mut World) {
169204
world.insert_resource(CameraWindows(camera_windows));
170205
}
171206

207+
/// A view not associated with any other camera.
208+
enum RootView {
209+
/// A camera.
210+
Camera { entity: Entity, order: isize },
211+
212+
/// An auxiliary view not associated with a camera.
213+
///
214+
/// This is currently used for point and spot light shadow maps.
215+
Auxiliary(Entity),
216+
}
217+
218+
impl Display for RootView {
219+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
220+
match *self {
221+
RootView::Camera { entity, order } => write!(f, "Camera {} ({:?})", order, entity),
222+
RootView::Auxiliary(entity) => write!(f, "Auxiliary View {:?}", entity),
223+
}
224+
}
225+
}
226+
172227
pub(crate) fn submit_pending_command_buffers(world: &mut World) {
173228
let mut pending = world.resource_mut::<PendingCommandBuffers>();
174229
let buffer_count = pending.len();

crates/bevy_pbr/src/lib.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,19 @@ impl Plugin for PbrPlugin {
423423
render_app.add_systems(
424424
Core3d,
425425
(
426-
shadow_pass::<EARLY_SHADOW_PASS>
426+
per_view_shadow_pass::<EARLY_SHADOW_PASS>
427427
.after(early_prepass_build_indirect_parameters)
428428
.before(early_downsample_depth)
429-
.before(shadow_pass::<LATE_SHADOW_PASS>),
430-
shadow_pass::<LATE_SHADOW_PASS>
429+
.before(per_view_shadow_pass::<LATE_SHADOW_PASS>),
430+
per_view_shadow_pass::<LATE_SHADOW_PASS>
431+
.after(late_prepass_build_indirect_parameters)
432+
.before(main_build_indirect_parameters)
433+
.before(Core3dSystems::MainPass),
434+
shared_shadow_pass::<EARLY_SHADOW_PASS>
435+
.after(early_prepass_build_indirect_parameters)
436+
.before(early_downsample_depth)
437+
.before(per_view_shadow_pass::<LATE_SHADOW_PASS>),
438+
shared_shadow_pass::<LATE_SHADOW_PASS>
431439
.after(late_prepass_build_indirect_parameters)
432440
.before(main_build_indirect_parameters)
433441
.before(Core3dSystems::MainPass),

crates/bevy_pbr/src/material.rs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -995,12 +995,13 @@ pub(crate) fn specialize_material_meshes(
995995
continue;
996996
}
997997

998+
// Check for material instance, mesh, and material. If any of
999+
// these fail, it's probably because the relevant asset hasn't
1000+
// loaded yet. In that case, add the entity to the list of
1001+
// pending mesh materials and bail.
9981002
let Some(material_instance) =
9991003
render_material_instances.instances.get(visible_entity)
10001004
else {
1001-
// We couldn't fetch the material instance, probably because
1002-
// the material hasn't been loaded yet. Add the entity to
1003-
// the list of pending mesh materials and bail.
10041005
view_pending_mesh_material_queues
10051006
.current_frame
10061007
.insert((*render_entity, *visible_entity));
@@ -1009,9 +1010,6 @@ pub(crate) fn specialize_material_meshes(
10091010
let Some(mesh_instance) =
10101011
render_mesh_instances.render_mesh_queue_data(*visible_entity)
10111012
else {
1012-
// We couldn't fetch the mesh, probably because it hasn't
1013-
// been loaded yet. Add the entity to the list of pending
1014-
// mesh materials and bail.
10151013
view_pending_mesh_material_queues
10161014
.current_frame
10171015
.insert((*render_entity, *visible_entity));
@@ -1021,9 +1019,6 @@ pub(crate) fn specialize_material_meshes(
10211019
continue;
10221020
};
10231021
let Some(material) = render_materials.get(material_instance.asset_id) else {
1024-
// We couldn't fetch the material, probably because the
1025-
// material hasn't been loaded yet. Add the entity to the
1026-
// list of pending mesh materials and bail.
10271022
view_pending_mesh_material_queues
10281023
.current_frame
10291024
.insert((*render_entity, *visible_entity));
@@ -1188,30 +1183,25 @@ pub fn queue_material_meshes(
11881183
continue;
11891184
};
11901185

1186+
// Check for material instance, mesh, and material. If any of these
1187+
// fail, it's probably because the relevant asset hasn't loaded yet.
1188+
// In that case, add the entity to the list of pending mesh
1189+
// materials and bail.
11911190
let Some(material_instance) = render_material_instances.instances.get(visible_entity)
11921191
else {
1193-
// We couldn't fetch the material, probably because the material
1194-
// hasn't been loaded yet. Add the entity to the list of pending
1195-
// mesh materials and bail.
11961192
view_pending_mesh_material_queues
11971193
.current_frame
11981194
.insert((*render_entity, *visible_entity));
11991195
continue;
12001196
};
12011197
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
12021198
else {
1203-
// We couldn't fetch the mesh, probably because it hasn't been
1204-
// loaded yet. Add the entity to the list of pending mesh
1205-
// materials and bail.
12061199
view_pending_mesh_material_queues
12071200
.current_frame
12081201
.insert((*render_entity, *visible_entity));
12091202
continue;
12101203
};
12111204
let Some(material) = render_materials.get(material_instance.asset_id) else {
1212-
// We couldn't fetch the material, probably because the material
1213-
// hasn't been loaded yet. Add the entity to the list of pending
1214-
// mesh materials and bail.
12151205
view_pending_mesh_material_queues
12161206
.current_frame
12171207
.insert((*render_entity, *visible_entity));

crates/bevy_pbr/src/meshlet/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use self::{
4343
},
4444
visibility_buffer_raster_node::meshlet_visibility_buffer_raster,
4545
};
46-
use crate::render::{shadow_pass, EARLY_SHADOW_PASS};
46+
use crate::render::{per_view_shadow_pass, EARLY_SHADOW_PASS};
4747
use crate::{meshlet::meshlet_mesh_manager::init_meshlet_mesh_manager, PreviousGlobalTransform};
4848
use bevy_app::{App, Plugin};
4949
use bevy_asset::{embedded_asset, AssetApp, AssetId, Handle};
@@ -198,9 +198,10 @@ impl Plugin for MeshletPlugin {
198198
.add_systems(
199199
Core3d,
200200
(
201-
meshlet_visibility_buffer_raster.before(shadow_pass::<EARLY_SHADOW_PASS>),
201+
meshlet_visibility_buffer_raster
202+
.before(per_view_shadow_pass::<EARLY_SHADOW_PASS>),
202203
meshlet_prepass
203-
.after(shadow_pass::<EARLY_SHADOW_PASS>)
204+
.after(per_view_shadow_pass::<EARLY_SHADOW_PASS>)
204205
.in_set(Core3dSystems::Prepass),
205206
meshlet_deferred_gbuffer_prepass
206207
.after(meshlet_prepass)

crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::{
22
pipelines::MeshletPipelines,
33
resource_manager::{MeshletViewBindGroups, MeshletViewResources, ResourceManager},
44
};
5-
use crate::{LightEntity, ShadowView, ViewLightEntities};
5+
use crate::{LightEntity, ShadowView};
66
use bevy_color::LinearRgba;
77
use bevy_core_pipeline::prepass::PreviousViewUniformOffset;
88
use bevy_ecs::prelude::*;
@@ -27,7 +27,6 @@ pub fn meshlet_visibility_buffer_raster(
2727
&PreviousViewUniformOffset,
2828
&MeshletViewBindGroups,
2929
&MeshletViewResources,
30-
&ViewLightEntities,
3130
)>,
3231
view_light_query: Query<(
3332
&ShadowView,
@@ -47,7 +46,6 @@ pub fn meshlet_visibility_buffer_raster(
4746
previous_view_offset,
4847
meshlet_view_bind_groups,
4948
meshlet_view_resources,
50-
lights,
5149
) = view.into_inner();
5250

5351
let Some((
@@ -194,19 +192,15 @@ pub fn meshlet_visibility_buffer_raster(
194192
ctx.command_encoder().pop_debug_group();
195193
time_span.end(ctx.command_encoder());
196194

197-
for light_entity in &lights.lights {
198-
let Ok((
199-
shadow_view,
200-
light_type,
201-
view_offset,
202-
previous_view_offset,
203-
meshlet_view_bind_groups,
204-
meshlet_view_resources,
205-
)) = view_light_query.get(*light_entity)
206-
else {
207-
continue;
208-
};
209-
195+
for (
196+
shadow_view,
197+
light_type,
198+
view_offset,
199+
previous_view_offset,
200+
meshlet_view_bind_groups,
201+
meshlet_view_resources,
202+
) in view_light_query.iter()
203+
{
210204
let shadow_visibility_buffer_hardware_raster_pipeline =
211205
if let LightEntity::Directional { .. } = light_type {
212206
visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline

0 commit comments

Comments
 (0)