From 176b3ee420e934b9bc9776a0a2323a05e1704b17 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Wed, 10 Jun 2026 23:20:17 -0400 Subject: [PATCH 01/29] Add flag to Camera for its own shadow maps --- crates/bevy_camera/src/camera.rs | 12 ++++++++++++ crates/bevy_render/src/camera.rs | 2 ++ 2 files changed, 14 insertions(+) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 38ecb39a016cb..732af2f39583b 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -410,6 +410,17 @@ pub struct Camera { pub invert_culling: bool, /// If set, this camera will be a sub camera of a large view, defined by a [`SubCameraView`]. pub sub_camera_view: Option, + /// Whether this camera will generate its own shadow maps for any lights in the scene. + /// + /// If true, shadow maps unique to this camera will be generated. + /// The shadow maps will be generated using the same render layers and HLOD configuration as this camera. + /// The light's render layers must be a superset of this camera's render layers. + /// + /// Enabling this setting can have a negative impact on performance, as this camera will + /// not be relying on the camera-agnostic shadow maps generated for certain lights (i.e. SpotLight and PointLight) + /// + /// Defaults to `false`. + pub has_own_shadow_maps: bool, } impl Default for Camera { @@ -424,6 +435,7 @@ impl Default for Camera { clear_color: Default::default(), invert_culling: false, sub_camera_view: None, + has_own_shadow_maps: false, } } } diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 29d15a00b0b97..15dde88c65bc4 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -468,6 +468,7 @@ pub struct ExtractedCamera { /// When [`CompositingSpace::Srgb`], the main texture uses linear storage (`Rgba8Unorm`) /// and shaders output sRGB-encoded values for gamma-encoded blending. pub compositing_space: Option, + pub has_own_shadow_maps: bool, } pub fn extract_cameras( @@ -639,6 +640,7 @@ pub fn extract_cameras( .unwrap_or_else(|| Exposure::default().exposure()), hdr, compositing_space: compositing_space.copied(), + has_own_shadow_maps: camera.has_own_shadow_maps, }, ExtractedView { retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), From df777278978f102c6bb96e912967a8b4c50b7828 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 11 Jun 2026 21:37:10 -0400 Subject: [PATCH 02/29] wip: additional point and spot light shadow maps per opt-in view --- crates/bevy_pbr/src/render/gpu_preprocess.rs | 55 +- crates/bevy_pbr/src/render/light.rs | 541 ++++++++++++------- crates/bevy_render/src/view/mod.rs | 5 +- 3 files changed, 399 insertions(+), 202 deletions(-) diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 33617abac86dc..3067d424a3b8d 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -72,7 +72,10 @@ use bitflags::bitflags; use smallvec::{smallvec, SmallVec}; use tracing::warn; -use crate::{LightEntity, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform}; +use crate::{ + LightEntity, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform, + PointLightShadowViewEntities, +}; use super::{ShadowView, ViewLightEntities}; @@ -524,7 +527,13 @@ pub fn clear_indirect_parameters_metadata( /// to frame, we avoid having to perform a CPU-side traversal of every mesh /// instance every frame. pub fn unpack_bins( - current_view: ViewQuery, Without>, + current_view: ViewQuery< + ( + Option<&ViewLightEntities>, + Option<&PointLightShadowViewEntities>, + ), + Without, + >, view_query: Query<&ExtractedView, Without>, light_query: Query<&LightEntity>, batched_instance_buffers: Res>, @@ -546,9 +555,13 @@ pub fn unpack_bins( // Gather up all views. let view_entity = current_view.entity(); - let shadow_cascade_views = current_view.into_inner(); - let all_views = - gather_shadow_cascades_for_view(view_entity, shadow_cascade_views, &light_query); + let (shadow_cascade_views, point_light_shadow_views) = current_view.into_inner(); + let all_views = gather_shadow_cascades_for_view( + view_entity, + shadow_cascade_views, + point_light_shadow_views, + &light_query, + ); // Don't run if the shaders haven't been compiled yet. if let Some(bin_unpacking_pipeline_id) = preprocess_pipelines.bin_unpacking.pipeline_id @@ -600,7 +613,13 @@ pub fn unpack_bins( } pub fn early_gpu_preprocess( - current_view: ViewQuery, Without>, + current_view: ViewQuery< + ( + Option<&ViewLightEntities>, + Option<&PointLightShadowViewEntities>, + ), + Without, + >, view_query: Query< ( &ExtractedView, @@ -630,9 +649,13 @@ pub fn early_gpu_preprocess( let pass_span = diagnostics.pass_span(&mut compute_pass, "early_mesh_preprocessing"); let view_entity = current_view.entity(); - let shadow_cascade_views = current_view.into_inner(); - let all_views = - gather_shadow_cascades_for_view(view_entity, shadow_cascade_views, &light_query); + let (shadow_cascade_views, point_light_shadow_views) = current_view.into_inner(); + let all_views = gather_shadow_cascades_for_view( + view_entity, + shadow_cascade_views, + point_light_shadow_views, + &light_query, + ); // Run the compute passes. for view_entity in all_views { @@ -795,6 +818,7 @@ pub fn early_gpu_preprocess( fn gather_shadow_cascades_for_view( view_entity: Entity, shadow_cascade_views: Option<&ViewLightEntities>, + point_light_shadow_views: Option<&PointLightShadowViewEntities>, light_query: &Query<&LightEntity>, ) -> SmallVec<[Entity; 8]> { let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); @@ -812,6 +836,19 @@ fn gather_shadow_cascades_for_view( .copied(), ); } + if let Some(point_light_shadow_views) = point_light_shadow_views { + all_views.extend( + point_light_shadow_views + .shadow_view_entities + .iter() + .filter(|light_entity| { + light_query.get(**light_entity).is_ok_and(|light_entity| { + matches!(*light_entity, LightEntity::Point { .. }) + }) + }) + .copied(), + ); + } all_views } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 980c008ae0d1b..1420d03faa209 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -39,7 +39,7 @@ use bevy_math::{ use bevy_mesh::{Mesh3d, MeshVertexBufferLayoutRef}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; -use bevy_render::camera::{DirtySpecializations, PendingQueues}; +use bevy_render::camera::{DirtySpecializations, ExtractedCamera, PendingQueues}; use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::mesh::allocator::MeshSlabs; use bevy_render::occlusion_culling::{ @@ -919,8 +919,15 @@ pub(crate) fn remove_point_and_spot_light_view_entities( ) { if let Ok(entities) = query.get(remove.entity) { for e in entities.0.iter().copied() { - if let Ok(mut v) = commands.get_entity(e) { - v.despawn(); + if let Ok(mut ec) = commands.get_entity(e) { + ec.despawn(); + } + } + for v in entities.1.values() { + for e in v.iter().copied() { + if let Ok(mut ec) = commands.get_entity(e) { + ec.despawn(); + } } } } @@ -929,10 +936,11 @@ pub(crate) fn remove_point_and_spot_light_view_entities( /// A component that stores the shadow maps associated with a point or spot /// light. /// -/// This component is placed on the light, because these types of shadow maps -/// aren't associated with views. -#[derive(Component, Default, Deref, DerefMut)] -pub struct PointAndSpotLightViewEntities(Vec); +/// The first entry stores shadow maps that are not specific to any view. +/// The second entry stores shadow maps keyed by a camera view entity that +/// is configured to have its own shadow map. +#[derive(Component, Default)] +pub struct PointAndSpotLightViewEntities(Vec, EntityHashMap>); #[derive(Component)] pub struct ShadowView { @@ -963,6 +971,24 @@ pub struct ViewLightEntities { pub lights: Vec, } +/// A component that holds the shadow views generated by a pointlight +/// associated with a specific camera. +#[derive(Component)] +pub struct PointLightShadowViewEntities { + /// The pointlight shadow views associated with a camera that has opted + /// in to its own shadow maps. + pub shadow_view_entities: Vec, +} + +/// A component that holds the shadow view generated by a spotlight +/// associated with a specific camera. +#[derive(Component)] +pub struct SpotLightShadowViewEntity { + /// The spotlight shadow view associated with a camera that has opted + /// in to its own shadow maps. + pub shadow_view_entity: Entity, +} + #[derive(Component)] pub struct ViewLightsUniformOffset { pub offset: u32, @@ -1003,6 +1029,7 @@ pub fn prepare_lights( Option<&RenderLayers>, Has, Option<&AmbientLight>, + Option<&ExtractedCamera>, ), With, >, @@ -1339,6 +1366,7 @@ pub fn prepare_lights( maybe_layers, _no_indirect_drawing, _maybe_ambient_override, + _extracted_camera, ) in sorted_cameras .0 .iter() @@ -1488,7 +1516,26 @@ pub fn prepare_lights( continue; } - if point_and_spot_light_view_entities.0.is_empty() { + let mut auxiliary_entities: Vec> = sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + .filter(|(_, _, _, _, _, _, _, camera)| { + camera.is_some_and(|camera| camera.has_own_shadow_maps) + }) + .map(|(entity, main_entity, _, _, _, _, _, _)| { + Some((entity, MainEntity::from(main_entity))) + }) + .collect(); + if views_count - auxiliary_entities.len() > 0 { + // There exist views that necessitate creating the shared shadow map. + // The shared shadow map has an auxiliary entity of None. + auxiliary_entities.push(None); + } + + if point_and_spot_light_view_entities.0.is_empty() + && point_and_spot_light_view_entities.1.is_empty() + { let light_index = *global_clusterable_object_meta .entity_to_index .get(light_entity) @@ -1498,30 +1545,29 @@ pub fn prepare_lights( // and ignore rotation because we want the shadow map projections to align with the axes let view_translation = GlobalTransform::from_translation(light.transform.translation()); - // for each face of a cube we spawn a light entity - let light_view_entities: Vec<_> = (0..6).map(|_| commands.spawn_empty().id()).collect(); - let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, 1.0, light.shadow_map_near_z, ); - for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations - .iter() - .zip(&point_light_frusta.unwrap().frusta) - .zip(light_view_entities.iter().copied()) - .enumerate() - { - let base_array_layer = (light_index * 6 + face_index) as u32; + for auxiliary_entity in auxiliary_entities.iter() { + let light_view_entities: Vec<_> = + (0..6).map(|_| commands.spawn_empty().id()).collect(); + for (face_index, ((view_rotation, frustum), view_light_entity)) in + cube_face_rotations + .iter() + .zip(&point_light_frusta.unwrap().frusta) + .zip(light_view_entities.iter().copied()) + .enumerate() + { + let base_array_layer = (light_index * 6 + face_index) as u32; - let depth_attachment = point_light_depth_attachments - .entry(base_array_layer) - .or_insert_with(|| { - let depth_texture_view = - point_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { + let depth_attachment = point_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + let depth_texture_view = point_light_depth_texture.texture.create_view( + &TextureViewDescriptor { label: Some("point_light_shadow_map_texture_view"), format: None, dimension: Some(TextureViewDimension::D2), @@ -1531,105 +1577,148 @@ pub fn prepare_lights( mip_level_count: None, base_array_layer, array_layer_count: Some(1u32), - }); - - DepthAttachment::new(depth_texture_view, Some(0.0)) - }) - .clone(); + }, + ); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + face_index as u32, + ); - // Point light shadow maps are shared across all cameras, - // so the retained view entity must not include the camera. - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index as u32); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_point_light_{}_{}_aux_{:?}", + light_index, + face_index_to_name(face_index), + auxiliary_entity.map(|(_, main_entity)| main_entity), + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *frustum, + LightEntity::Point { + light_entity: *light_entity, + face_index, + }, + )); + if auxiliary_entity.is_none() { + commands + .entity(view_light_entity) + .insert(RootNonCameraView(Core3d.intern())); + } - commands.entity(view_light_entity).insert(( - ShadowView { - depth_attachment, - pass_name: format!( - "shadow_point_light_{}_{}", - light_index, - face_index_to_name(face_index) - ), - }, - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - point_light_shadow_map.size as u32, - point_light_shadow_map.size as u32, - ), - world_from_view: view_translation * *view_rotation, - clip_from_world: None, - clip_from_view: cube_face_projection, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *frustum, - LightEntity::Point { - light_entity: *light_entity, - face_index, - }, - RootNonCameraView(Core3d.intern()), - )); + if !matches!( + gpu_preprocessing_support.max_supported_mode, + GpuPreprocessingMode::Culling + ) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + } - if !matches!( - gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) { - commands.entity(view_light_entity).insert(NoIndirectDrawing); + if let Some((entity, main_entity)) = auxiliary_entity { + point_and_spot_light_view_entities.1.insert( + main_entity.entity(), + light_view_entities.iter().copied().collect(), + ); + commands + .entity(*entity) + .insert(PointLightShadowViewEntities { + shadow_view_entities: light_view_entities, + }); + } else { + point_and_spot_light_view_entities.0 = light_view_entities; } } - - point_and_spot_light_view_entities.0 = light_view_entities; } else if changed_point_lights.get(*light_entity).is_ok() { - // If the point light was changed, update the `ExtractedView` only. + // If the point light was changed, update the `ExtractedView` and frustum only. let view_translation = GlobalTransform::from_translation(light.transform.translation()); let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, 1.0, light.shadow_map_near_z, ); - for (face_index, (view_rotation, frustum)) in cube_face_rotations - .iter() - .zip(&point_light_frusta.unwrap().frusta) - .enumerate() - { - let view_light_entity = point_and_spot_light_view_entities.0[face_index]; - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index as u32); - commands.entity(view_light_entity).insert(( - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - point_light_shadow_map.size as u32, - point_light_shadow_map.size as u32, - ), - world_from_view: view_translation * *view_rotation, - clip_from_world: None, - clip_from_view: cube_face_projection, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *frustum, - )); + for auxiliary_entity in auxiliary_entities.iter() { + let light_view_entities = if let Some((entity, main_entity)) = auxiliary_entity { + let entry = point_and_spot_light_view_entities + .1 + .get(&main_entity.entity()); + if let Some(entities) = entry { + entities + } else { + continue; + } + } else { + &point_and_spot_light_view_entities.0 + }; + for (face_index, ((view_rotation, frustum), view_light_entity)) in + cube_face_rotations + .iter() + .zip(&point_light_frusta.unwrap().frusta) + .zip(light_view_entities.iter().copied()) + .enumerate() + { + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + face_index as u32, + ); + commands.entity(view_light_entity).insert(( + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *frustum, + )); + } } } + // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. // Initialize the shadow render phases. We have to do this even if we've // already created the views in order to clear out old data. - for face_index in 0..6 { - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index); - shadow_render_phases.prepare_for_new_frame( - retained_view_entity, - gpu_preprocessing_support.max_supported_mode, - ); - live_shadow_mapping_lights.insert(retained_view_entity); + for auxiliary_entity in auxiliary_entities.iter() { + for face_index in 0..6 { + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + face_index, + ); + shadow_render_phases.prepare_for_new_frame( + retained_view_entity, + gpu_preprocessing_support.max_supported_mode, + ); + live_shadow_mapping_lights.insert(retained_view_entity); + } } } @@ -1656,11 +1745,24 @@ pub fn prepare_lights( continue; } - // Spot light shadow maps are shared across all cameras, - // so the retained view entity must not include the camera. - let retained_view_entity = RetainedViewEntity::new(*light_main_entity, None, 0); + let mut auxiliary_entities: Vec> = sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + .filter(|(_, _, _, _, _, _, _, camera)| { + camera.is_some_and(|camera| camera.has_own_shadow_maps) + }) + .map(|(_, main_entity, _, _, _, _, _, _)| Some(MainEntity::from(main_entity))) + .collect(); + if views_count - auxiliary_entities.len() > 0 { + // There exist views that necessitate creating the shared shadow map. + // The shared shadow map has an auxiliary entity of None. + auxiliary_entities.push(None); + } - if point_and_spot_light_view_entities.0.is_empty() { + if point_and_spot_light_view_entities.0.is_empty() + && point_and_spot_light_view_entities.1.is_empty() + { let spot_world_from_view = spot_light_world_from_view(&light.transform); let spot_world_from_view = spot_world_from_view.into(); @@ -1670,64 +1772,81 @@ pub fn prepare_lights( let base_array_layer = (num_directional_cascades_enabled + light_index) as u32; - let depth_attachment = directional_light_depth_attachments - .entry(base_array_layer) - .or_insert_with(|| { - let depth_texture_view = directional_light_depth_texture.texture.create_view( - &TextureViewDescriptor { - label: Some("spot_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - usage: None, - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer, - array_layer_count: Some(1u32), - }, - ); + for auxiliary_entity in auxiliary_entities.iter() { + let depth_attachment = directional_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + let depth_texture_view = directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("spot_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); - DepthAttachment::new(depth_texture_view, Some(0.0)) - }) - .clone(); + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); - let view_light_entity = commands.spawn_empty().id(); + let view_light_entity = commands.spawn_empty().id(); + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); - commands.entity(view_light_entity).insert(( - ShadowView { - depth_attachment, - pass_name: format!("shadow_spot_light_{light_index}"), - }, - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: spot_world_from_view, - clip_from_view: spot_projection, - clip_from_world: None, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *spot_light_frustum.unwrap(), - LightEntity::Spot { - light_entity: *light_entity, - }, - RootNonCameraView(Core3d.intern()), - )); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_spot_light_{}_aux_{:?}", + light_index, *auxiliary_entity, + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { + light_entity: *light_entity, + }, + )); + if auxiliary_entity.is_none() { + commands + .entity(view_light_entity) + .insert(RootNonCameraView(Core3d.intern())); + } - if !matches!( - gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) { - commands.entity(view_light_entity).insert(NoIndirectDrawing); - } + if !matches!( + gpu_preprocessing_support.max_supported_mode, + GpuPreprocessingMode::Culling + ) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } - point_and_spot_light_view_entities.0 = vec![view_light_entity]; + if let Some(entity) = auxiliary_entity { + point_and_spot_light_view_entities + .1 + .insert(entity.entity(), vec![view_light_entity]); + } else { + point_and_spot_light_view_entities.0 = vec![view_light_entity]; + } + } } else if changed_point_lights.get(*light_entity).is_ok() { // If the spot light was changed, update the `ExtractedView` only. let spot_world_from_view = spot_light_world_from_view(&light.transform); @@ -1737,33 +1856,54 @@ pub fn prepare_lights( [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); - // There should be only one `view_light_entity` for spotlights. - let view_light_entity = point_and_spot_light_view_entities.0[0]; - commands.entity(view_light_entity).insert(( - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: spot_world_from_view, - clip_from_view: spot_projection, - clip_from_world: None, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *spot_light_frustum.unwrap(), - )); + for auxiliary_entity in auxiliary_entities.iter() { + // There should be only one `view_light_entity` for spotlights. + let view_light_entity = if let Some(main_entity) = auxiliary_entity { + let entry = point_and_spot_light_view_entities + .1 + .get(&main_entity.entity()); + if let Some(v) = entry { + v[0] + } else { + continue; + } + } else { + point_and_spot_light_view_entities.0[0] + }; + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); + commands.entity(view_light_entity).insert(( + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *spot_light_frustum.unwrap(), + )); + } } - shadow_render_phases.prepare_for_new_frame( - retained_view_entity, - gpu_preprocessing_support.max_supported_mode, - ); - live_shadow_mapping_lights.insert(retained_view_entity); + // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. + + for auxiliary_entity in auxiliary_entities.iter() { + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); + shadow_render_phases.prepare_for_new_frame( + retained_view_entity, + gpu_preprocessing_support.max_supported_mode, + ); + live_shadow_mapping_lights.insert(retained_view_entity); + } } // set up light data for each view @@ -1775,6 +1915,7 @@ pub fn prepare_lights( maybe_layers, no_indirect_drawing, maybe_ambient_override, + _extracted_camera, ) in sorted_cameras .0 .iter() @@ -2769,15 +2910,16 @@ pub fn shared_shadow_pass( /// Renders the shadow maps that are associated with a specific view. /// -/// At present, these consist of the directional light shadows. +/// At present, these consist of the directional light shadows +/// and opted-in camera-specific point and spot light shadows pub fn per_view_shadow_pass( world: &World, - view: ViewQuery<&ViewLightEntities>, + view: ViewQuery<(&ViewLightEntities, &PointLightShadowViewEntities)>, view_light_query: Query<(&ShadowView, &ExtractedView, Has)>, shadow_render_phases: Res>, mut ctx: RenderContext, ) { - let view_lights = view.into_inner(); + let (view_lights, point_shadow_views) = view.into_inner(); for view_light_entity in view_lights.lights.iter().copied() { if let Ok((view_light, extracted_light_view, occlusion_culling)) = @@ -2794,6 +2936,21 @@ pub fn per_view_shadow_pass( ); } } + for shadow_view_entity in point_shadow_views.shadow_view_entities.iter().copied() { + if let Ok((view_light, extracted_light_view, occlusion_culling)) = + view_light_query.get(shadow_view_entity) + { + view_shadow_pass::( + shadow_view_entity, + view_light, + extracted_light_view, + occlusion_culling, + world, + &shadow_render_phases, + &mut ctx, + ); + } + } } /// A common helper function to render a shadow map. diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index ddabb60e7d408..c5cbca1aab3f8 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -278,12 +278,15 @@ pub struct RetainedViewEntity { /// Another entity associated with the view entity. /// - /// This is currently used for shadow cascades. If there are multiple + /// This is used for shadow cascades. If there are multiple /// cameras, each camera needs to have its own set of shadow cascades. Thus /// the light and subview index aren't themselves enough to uniquely /// identify a shadow cascade: we need the camera that the cascade is /// associated with as well. This entity stores that camera. /// + /// This is also used for point and spot shadow views that + /// are specific to a camera, configurable via `has_own_shadow_maps` per camera. + /// /// If not present, this will be `MainEntity(Entity::PLACEHOLDER)`. pub auxiliary_entity: MainEntity, From d3bd80168289d8656a9168f6629906fc059f6f35 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 13 Jun 2026 14:31:24 -0400 Subject: [PATCH 03/29] point lights are fixed! testbed 3d render layers looks good --- crates/bevy_pbr/src/render/light.rs | 101 ++++++++++++------ crates/bevy_pbr/src/render/pbr_functions.wgsl | 5 +- .../bevy_pbr/src/render/shadow_sampling.wgsl | 86 +++++++-------- crates/bevy_pbr/src/render/shadows.wgsl | 7 +- .../src/volumetric_fog/volumetric_fog.wgsl | 8 +- crates/bevy_render/src/view/mod.rs | 27 ++++- crates/bevy_render/src/view/view.wgsl | 3 + 7 files changed, 150 insertions(+), 87 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1420d03faa209..80218536073ce 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1087,6 +1087,23 @@ pub fn prepare_lights( ) { let views_iter = views.iter(); let views_count = views_iter.len(); + let mut auxiliary_entities: Vec> = sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + .filter(|(_, _, _, _, _, _, _, camera)| { + camera.is_some_and(|camera| camera.has_own_shadow_maps) + }) + .map(|(entity, main_entity, _, _, _, _, _, _)| { + Some((entity, MainEntity::from(main_entity))) + }) + .collect(); + if views_count - auxiliary_entities.len() > 0 { + // There exist views that necessitate creating the shared shadow map. + // The shared shadow map has an auxiliary entity of None. + auxiliary_entities.push(None); + } + let Some(mut view_gpu_lights_writer) = light_meta .view_gpu_lights @@ -1406,7 +1423,9 @@ pub fn prepare_lights( size: Extent3d { width: point_light_shadow_map.size as u32, height: point_light_shadow_map.size as u32, - depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, + depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 + * auxiliary_entities.len() as u32 + * 6, }, mip_level_count: 1, sample_count: 1, @@ -1551,7 +1570,7 @@ pub fn prepare_lights( light.shadow_map_near_z, ); - for auxiliary_entity in auxiliary_entities.iter() { + for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { let light_view_entities: Vec<_> = (0..6).map(|_| commands.spawn_empty().id()).collect(); for (face_index, ((view_rotation, frustum), view_light_entity)) in @@ -1561,8 +1580,9 @@ pub fn prepare_lights( .zip(light_view_entities.iter().copied()) .enumerate() { - let base_array_layer = (light_index * 6 + face_index) as u32; - + let base_array_layer = (light_index * auxiliary_entities.len() * 6 + + aux_entity_index * 6 + + face_index) as u32; let depth_attachment = point_light_depth_attachments .entry(base_array_layer) .or_insert_with(|| { @@ -1579,10 +1599,10 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }, ); - DepthAttachment::new(depth_texture_view, Some(0.0)) }) .clone(); + let retained_view_entity = RetainedViewEntity::new( *light_main_entity, auxiliary_entity.map(|(_, main_entity)| main_entity), @@ -1624,6 +1644,17 @@ pub fn prepare_lights( commands .entity(view_light_entity) .insert(RootNonCameraView(Core3d.intern())); + } else if let Some((entity, _)) = auxiliary_entity + && let Some((_, _, _, _, maybe_render_layers, _, _, _)) = + views.get(*entity).ok() + && let Some(render_layers) = maybe_render_layers + { + // When render_layers is fixed for lights, this should probably make sure + // that the resulting render_layers is the intersection of the light's and the view's + println!("{render_layers:?}"); + commands + .entity(view_light_entity) + .insert(render_layers.clone()); } if !matches!( @@ -1657,7 +1688,7 @@ pub fn prepare_lights( light.shadow_map_near_z, ); for auxiliary_entity in auxiliary_entities.iter() { - let light_view_entities = if let Some((entity, main_entity)) = auxiliary_entity { + let light_view_entities = if let Some((_, main_entity)) = auxiliary_entity { let entry = point_and_spot_light_view_entities .1 .get(&main_entity.entity()); @@ -1745,21 +1776,6 @@ pub fn prepare_lights( continue; } - let mut auxiliary_entities: Vec> = sorted_cameras - .0 - .iter() - .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) - .filter(|(_, _, _, _, _, _, _, camera)| { - camera.is_some_and(|camera| camera.has_own_shadow_maps) - }) - .map(|(_, main_entity, _, _, _, _, _, _)| Some(MainEntity::from(main_entity))) - .collect(); - if views_count - auxiliary_entities.len() > 0 { - // There exist views that necessitate creating the shared shadow map. - // The shared shadow map has an auxiliary entity of None. - auxiliary_entities.push(None); - } - if point_and_spot_light_view_entities.0.is_empty() && point_and_spot_light_view_entities.1.is_empty() { @@ -1770,9 +1786,11 @@ pub fn prepare_lights( [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); - let base_array_layer = (num_directional_cascades_enabled + light_index) as u32; + for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + // TODO take into consideration the current aux_entity too + let base_array_layer = + (num_directional_cascades_enabled + light_index + aux_entity_index) as u32; - for auxiliary_entity in auxiliary_entities.iter() { let depth_attachment = directional_light_depth_attachments .entry(base_array_layer) .or_insert_with(|| { @@ -1795,15 +1813,19 @@ pub fn prepare_lights( .clone(); let view_light_entity = commands.spawn_empty().id(); - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + 0, + ); commands.entity(view_light_entity).insert(( ShadowView { depth_attachment, pass_name: format!( "shadow_spot_light_{}_aux_{:?}", - light_index, *auxiliary_entity, + light_index, + auxiliary_entity.map(|(_, main_entity)| main_entity), ), }, ExtractedView { @@ -1839,10 +1861,10 @@ pub fn prepare_lights( commands.entity(view_light_entity).insert(NoIndirectDrawing); } - if let Some(entity) = auxiliary_entity { + if let Some((_, main_entity)) = auxiliary_entity { point_and_spot_light_view_entities .1 - .insert(entity.entity(), vec![view_light_entity]); + .insert(main_entity.entity(), vec![view_light_entity]); } else { point_and_spot_light_view_entities.0 = vec![view_light_entity]; } @@ -1858,7 +1880,7 @@ pub fn prepare_lights( for auxiliary_entity in auxiliary_entities.iter() { // There should be only one `view_light_entity` for spotlights. - let view_light_entity = if let Some(main_entity) = auxiliary_entity { + let view_light_entity = if let Some((_, main_entity)) = auxiliary_entity { let entry = point_and_spot_light_view_entities .1 .get(&main_entity.entity()); @@ -1870,8 +1892,11 @@ pub fn prepare_lights( } else { point_and_spot_light_view_entities.0[0] }; - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + 0, + ); commands.entity(view_light_entity).insert(( ExtractedView { retained_view_entity, @@ -1896,8 +1921,11 @@ pub fn prepare_lights( // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. for auxiliary_entity in auxiliary_entities.iter() { - let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, *auxiliary_entity, 0); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + 0, + ); shadow_render_phases.prepare_for_new_frame( retained_view_entity, gpu_preprocessing_support.max_supported_mode, @@ -2535,6 +2563,7 @@ pub(crate) fn specialize_shadows( { continue; } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else { continue; }; @@ -2712,8 +2741,10 @@ pub fn queue_shadows( let mesh_layers = mesh_instance.render_layers.as_ref().unwrap_or_default(); let view_render_layers = maybe_view_render_layers.unwrap_or_default(); if !view_render_layers.intersects(mesh_layers) { + println!("{view_render_layers:?} {mesh_layers:?} render layers do not intersect"); continue; } + println!("{view_render_layers:?} {mesh_layers:?} render layers do intersect"); let Some(material_instance) = render_material_instances.instances.get(main_entity) else { @@ -2910,8 +2941,8 @@ pub fn shared_shadow_pass( /// Renders the shadow maps that are associated with a specific view. /// -/// At present, these consist of the directional light shadows -/// and opted-in camera-specific point and spot light shadows +/// These consist of the directional light shadows +/// and opt-in camera-specific point and spot light shadows pub fn per_view_shadow_pass( world: &World, view: ViewQuery<(&ViewLightEntities, &PointLightShadowViewEntities)>, diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index ae7703f883914..9df3403d80562 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -465,6 +465,7 @@ fn apply_pbr_lighting( view_bindings::view.view_from_world[2].z, view_bindings::view.view_from_world[3].z ), in.world_position); + let point_shadow_map_offset = view_bindings::view.point_shadow_map_index_offset; let cluster_index = clustering::view_fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index); @@ -493,7 +494,7 @@ fn apply_pbr_lighting( var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal, in.frag_coord.xy); + shadow = shadows::fetch_point_shadow(light_id, point_shadow_map_offset, in.world_position, in.world_normal, in.frag_coord.xy); } #ifdef CONTACT_SHADOWS @@ -523,7 +524,7 @@ fn apply_pbr_lighting( var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + transmitted_shadow = shadows::fetch_point_shadow(light_id, point_shadow_map_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); } let transmitted_light_contrib = diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index d4f0751e17e68..a373f8cd1fb93 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -321,7 +321,7 @@ fn sample_shadow_map_pcss( // behavior due to some of the fragments in a quad (2x2 fragments) being // processed not being sampled, and this messing with mip-mapping functionality. // The shadow maps have no mipmaps so Level just samples from LOD 0. -fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: u32) -> f32 { +fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_index: u32) -> f32 { #ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT return textureSampleCompare( view_bindings::point_shadow_textures, @@ -334,7 +334,7 @@ fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_comparison_sampler, light_local, - i32(light_id), + i32(light_index), depth ); #endif @@ -345,7 +345,7 @@ fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: fn search_for_blockers_in_shadow_cubemap_hardware( light_local: vec3, depth: f32, - light_id: u32, + light_index: u32, ) -> vec2 { #ifdef WEBGL2 // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled @@ -366,7 +366,7 @@ fn search_for_blockers_in_shadow_cubemap_hardware( view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_linear_sampler, light_local, - i32(light_id), + i32(light_index), ); #endif @@ -386,12 +386,12 @@ fn sample_shadow_cubemap_at_offset( y_basis: vec3, light_local: vec3, depth: f32, - light_id: u32, + light_index: u32, ) -> f32 { return sample_shadow_cubemap_hardware( light_local + position.x * x_basis + position.y * y_basis, depth, - light_id + light_index ) * coeff; } @@ -406,12 +406,12 @@ fn search_for_blockers_in_shadow_cubemap_at_offset( y_basis: vec3, light_local: vec3, depth: f32, - light_id: u32, + light_index: u32, ) -> vec2 { return search_for_blockers_in_shadow_cubemap_hardware( light_local + position.x * x_basis + position.y * y_basis, depth, - light_id + light_index ); } @@ -425,7 +425,7 @@ fn sample_shadow_cubemap_gaussian( depth: f32, scale: f32, distance_to_light: f32, - light_id: u32, + light_index: u32, ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. @@ -434,28 +434,28 @@ fn sample_shadow_cubemap_gaussian( var sum: f32 = 0.0; sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7], - basis[0], basis[1], light_local, depth, light_id); + basis[0], basis[1], light_local, depth, light_index); return sum; } @@ -468,7 +468,7 @@ fn sample_shadow_cubemap_jittered( frag_coord_xy: vec2, scale: f32, distance_to_light: f32, - light_id: u32, + light_index: u32, temporal: bool, ) -> f32 { let rotation_matrix = random_rotation_matrix(frag_coord_xy, temporal); @@ -496,21 +496,21 @@ fn sample_shadow_cubemap_jittered( var sum: f32 = 0.0; sum += sample_shadow_cubemap_at_offset( - sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_index); sum += sample_shadow_cubemap_at_offset( - sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_id); + sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_index); return sum; } @@ -518,17 +518,17 @@ fn sample_shadow_cubemap( light_local: vec3, distance_to_light: f32, depth: f32, - light_id: u32, + light_index: u32, frag_coord_xy: vec2, ) -> f32 { #ifdef SHADOW_FILTER_METHOD_GAUSSIAN return sample_shadow_cubemap_gaussian( - light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id); + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_index); #else ifdef SHADOW_FILTER_METHOD_TEMPORAL return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE, distance_to_light, light_id, true); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE, distance_to_light, light_index, true); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 - return sample_shadow_cubemap_hardware(light_local, depth, light_id); + return sample_shadow_cubemap_hardware(light_local, depth, light_index); #else // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) @@ -550,7 +550,7 @@ fn search_for_blockers_in_shadow_cubemap( depth: f32, scale: f32, distance_to_light: f32, - light_id: u32, + light_index: u32, ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. @@ -558,21 +558,21 @@ fn search_for_blockers_in_shadow_cubemap( var sum: vec2 = vec2(0.0); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_id); + D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_index); if (sum.y == 0.0) { return 0.0; @@ -589,21 +589,21 @@ fn sample_shadow_cubemap_pcss( light_local: vec3, distance_to_light: f32, depth: f32, - light_id: u32, + light_index: u32, light_size: f32, frag_coord_xy: vec2, ) -> f32 { let z_blocker = search_for_blockers_in_shadow_cubemap( - light_local, depth, light_size, distance_to_light, light_id); + light_local, depth, light_size, distance_to_light, light_index); // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); #ifdef SHADOW_FILTER_METHOD_TEMPORAL return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, true); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_index, true); #else return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, false); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_index, false); #endif } diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index d10a960c221b1..baabd1ff111b6 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -18,6 +18,7 @@ const flip_z: vec3 = vec3(1.0, 1.0, -1.0); fn fetch_point_shadow( light_id: u32, + point_shadow_map_offset: u32, frag_position: vec4, surface_normal: vec3, frag_coord_xy: vec2, @@ -52,12 +53,13 @@ fn fetch_point_shadow( // If soft shadows are enabled, use the PCSS path. Cubemaps assume a // left-handed coordinate space, so we have to flip the z-axis when // sampling. + let light_index = light_id + point_shadow_map_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_cubemap_pcss( frag_ls * flip_z, distance_to_light, depth, - light_id, + light_index, (*light).soft_shadow_size, frag_coord_xy, ); @@ -65,7 +67,7 @@ fn fetch_point_shadow( // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed // coordinate space, so we have to flip the z-axis when sampling. - return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id, frag_coord_xy); + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_index, frag_coord_xy); } fn fetch_spot_shadow( @@ -112,6 +114,7 @@ fn fetch_spot_shadow( let depth = near_z / -projected_position.z; // If soft shadows are enabled, use the PCSS path. + // TODO array_index needs to be updated here. let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_map_pcss( diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index a64f85d434682..3eb7561db0e99 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -129,6 +129,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the view. let exposure = view.exposure; + let point_shadow_map_offset = view.point_shadow_map_offset; // Sample the depth to put an upper bound on the length of the ray (as we // shouldn't trace through solid objects). If this is multisample, just use @@ -392,7 +393,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_point_shadow_without_normal(light_id, vec4(P_world, 1.0), position.xy); + shadow = fetch_point_shadow_without_normal(light_id, point_shadow_map_offset, vec4(P_world, 1.0), position.xy); } local_light_attenuation *= shadow; } else { @@ -444,7 +445,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { return vec4(accumulated_color, 1.0 - background_alpha); } -fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { +fn fetch_point_shadow_without_normal(light_id: u32, point_shadow_map_offset: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { let light = &clustered_lights.data[light_id]; // because the shadow maps align with the axes and the frustum planes are at 45 degrees @@ -474,7 +475,8 @@ fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4, fr // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, // so we have to flip the z-axis when sampling. let flip_z = vec3(1.0, 1.0, -1.0); - return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id, frag_coord_xy); + let light_index = light_id + point_shadow_map_offset; + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_index, frag_coord_xy); } fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index c5cbca1aab3f8..91d6cfcd27224 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -10,7 +10,9 @@ pub use visibility::*; pub use window::*; use crate::{ - camera::{ExtractedCamera, MipBias, NormalizedRenderTargetExt as _, TemporalJitter}, + camera::{ + ExtractedCamera, MipBias, NormalizedRenderTargetExt as _, SortedCameras, TemporalJitter, + }, extract_component::ExtractComponentPlugin, occlusion_culling::OcclusionCulling, render_asset::RenderAssets, @@ -28,7 +30,7 @@ use alloc::sync::{Arc, Weak}; use bevy_app::{App, Plugin}; use bevy_color::{LinearRgba, Oklaba, Srgba}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{prelude::*, VariantDefaults}; +use bevy_ecs::{entity::EntityHashMap, prelude::*, VariantDefaults}; use bevy_image::ToExtents; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_platform::collections::{hash_map::Entry, HashMap}; @@ -669,6 +671,9 @@ pub struct ViewUniform { pub color_grading: ColorGradingUniform, pub mip_bias: f32, pub frame_count: u32, + /// This offset is used to fetch the correct point shadow map to use for this view. + /// This is used to accommodates views that may be configured to have their own point shadow maps. + pub point_shadow_map_index_offset: u32, } #[derive(Resource)] @@ -1014,9 +1019,23 @@ pub fn prepare_view_uniforms( )>, frame_count: Res, shadow_lod_origin: Option>, + sorted_cameras: Res, ) { let view_iter = views.iter(); let view_count = view_iter.len(); + // The logic here (i.e. the usage of sorted cameras) must preserve the ordering used + // to generate the list of auxiliary entities for RetainedViewEntities in prepare_lights + let own_shadow_map_view_to_index: EntityHashMap = sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + .filter(|(_, extracted_camera, _, _, _, _, _)| { + extracted_camera.is_some_and(|camera| camera.has_own_shadow_maps) + }) + .map(|(entity, _, _, _, _, _, _)| entity) + .enumerate() + .map(|(index, entity)| (entity, index)) + .collect(); let Some(mut writer) = view_uniforms .uniforms @@ -1122,6 +1141,10 @@ pub fn prepare_view_uniforms( color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, + point_shadow_map_index_offset: *own_shadow_map_view_to_index + .get(&entity) + .unwrap_or(&(own_shadow_map_view_to_index.len())) + as u32, }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 308e9231369b8..56219b4d15d5b 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -70,6 +70,9 @@ struct View { color_grading: ColorGrading, mip_bias: f32, frame_count: u32, + // This offset is used to fetch the correct point shadow map to use for this view. + // This is used to accommodates views that may be configured to have their own point shadow maps. + point_shadow_map_index_offset: u32, }; /// World space: From 0c40e7793b62cd4329faa3abab74e28e853ef5a4 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 13 Jun 2026 16:17:39 -0400 Subject: [PATCH 04/29] refactor: create_point_shadow_maps function extracted --- crates/bevy_pbr/src/render/light.rs | 292 ++++++++++++++++------------ 1 file changed, 167 insertions(+), 125 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 80218536073ce..959545d95b410 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1414,7 +1414,7 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); - let mut point_light_depth_attachments = HashMap::::default(); + let mut point_light_depth_attachments = HashMap::<(u32, Option), DepthAttachment>::default(); let mut directional_light_depth_attachments = HashMap::::default(); let point_light_depth_texture = texture_cache.get( @@ -1535,23 +1535,6 @@ pub fn prepare_lights( continue; } - let mut auxiliary_entities: Vec> = sorted_cameras - .0 - .iter() - .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) - .filter(|(_, _, _, _, _, _, _, camera)| { - camera.is_some_and(|camera| camera.has_own_shadow_maps) - }) - .map(|(entity, main_entity, _, _, _, _, _, _)| { - Some((entity, MainEntity::from(main_entity))) - }) - .collect(); - if views_count - auxiliary_entities.len() > 0 { - // There exist views that necessitate creating the shared shadow map. - // The shared shadow map has an auxiliary entity of None. - auxiliary_entities.push(None); - } - if point_and_spot_light_view_entities.0.is_empty() && point_and_spot_light_view_entities.1.is_empty() { @@ -1571,113 +1554,22 @@ pub fn prepare_lights( ); for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { - let light_view_entities: Vec<_> = - (0..6).map(|_| commands.spawn_empty().id()).collect(); - for (face_index, ((view_rotation, frustum), view_light_entity)) in - cube_face_rotations - .iter() - .zip(&point_light_frusta.unwrap().frusta) - .zip(light_view_entities.iter().copied()) - .enumerate() - { - let base_array_layer = (light_index * auxiliary_entities.len() * 6 - + aux_entity_index * 6 - + face_index) as u32; - let depth_attachment = point_light_depth_attachments - .entry(base_array_layer) - .or_insert_with(|| { - let depth_texture_view = point_light_depth_texture.texture.create_view( - &TextureViewDescriptor { - label: Some("point_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - usage: None, - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer, - array_layer_count: Some(1u32), - }, - ); - DepthAttachment::new(depth_texture_view, Some(0.0)) - }) - .clone(); - - let retained_view_entity = RetainedViewEntity::new( - *light_main_entity, - auxiliary_entity.map(|(_, main_entity)| main_entity), - face_index as u32, - ); - - commands.entity(view_light_entity).insert(( - ShadowView { - depth_attachment, - pass_name: format!( - "shadow_point_light_{}_{}_aux_{:?}", - light_index, - face_index_to_name(face_index), - auxiliary_entity.map(|(_, main_entity)| main_entity), - ), - }, - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - point_light_shadow_map.size as u32, - point_light_shadow_map.size as u32, - ), - world_from_view: view_translation * *view_rotation, - clip_from_world: None, - clip_from_view: cube_face_projection, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *frustum, - LightEntity::Point { - light_entity: *light_entity, - face_index, - }, - )); - if auxiliary_entity.is_none() { - commands - .entity(view_light_entity) - .insert(RootNonCameraView(Core3d.intern())); - } else if let Some((entity, _)) = auxiliary_entity - && let Some((_, _, _, _, maybe_render_layers, _, _, _)) = - views.get(*entity).ok() - && let Some(render_layers) = maybe_render_layers - { - // When render_layers is fixed for lights, this should probably make sure - // that the resulting render_layers is the intersection of the light's and the view's - println!("{render_layers:?}"); - commands - .entity(view_light_entity) - .insert(render_layers.clone()); - } - - if !matches!( - gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) { - commands.entity(view_light_entity).insert(NoIndirectDrawing); - } - } - - if let Some((entity, main_entity)) = auxiliary_entity { - point_and_spot_light_view_entities.1.insert( - main_entity.entity(), - light_view_entities.iter().copied().collect(), - ); - commands - .entity(*entity) - .insert(PointLightShadowViewEntities { - shadow_view_entities: light_view_entities, - }); - } else { - point_and_spot_light_view_entities.0 = light_view_entities; - } + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + views, + (&cube_face_rotations, point_light_frusta), + &point_light_depth_texture, + (light_entity, light_main_entity, light_index), + ( + point_light_shadow_map.size, + view_translation, + cube_face_projection, + ), + (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + gpu_preprocessing_support.max_supported_mode, + ); } } else if changed_point_lights.get(*light_entity).is_ok() { // If the point light was changed, update the `ExtractedView` and frustum only. @@ -1734,6 +1626,7 @@ pub fn prepare_lights( } } // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. + // This also has implications for the stale depth attachments indices... sigh. // Initialize the shadow render phases. We have to do this even if we've // already created the views in order to clear out old data. @@ -2340,6 +2233,155 @@ pub fn prepare_lights( .retain(|entity, _| live_shadow_mapping_lights.contains(entity)); } +/// Creates six point shadow map for retained_view_entities identified by the light_main_entity, +/// the auxiliary_entity (the main part), and the six cube face rotation indices. +/// +/// Six new depth attachments are created per unique auxiliary entity, identified by its +/// its `aux_entity_index`. +/// +/// An auxiliary_entity of `None` creates a shadow map that will be shared across cameras. +/// If a camera requests for its own shadow maps, the auxiliary entity is the requesting camera. +fn create_point_shadow_maps( + commands: &mut Commands, + point_and_spot_light_view_entities: &mut Mut, + point_light_depth_attachments: &mut HashMap<(u32, Option), DepthAttachment>, + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + Option<&ExtractedCamera>, + ), + With, + >, + (cube_face_rotations, point_light_frusta): (&Vec, Option<&CubemapFrusta>), + point_light_depth_texture: &CachedTexture, + (light_entity, light_main_entity, light_index): (&Entity, &MainEntity, usize), + (point_light_shadow_map_size, view_translation, cube_face_projection): ( + usize, + GlobalTransform, + Mat4, + ), + (auxiliary_entities_size, auxiliary_entity, aux_entity_index): ( + usize, + &Option<(Entity, MainEntity)>, + usize, + ), + gpu_preprocessing_support_max_supported_mode: GpuPreprocessingMode, +) { + let light_view_entities: Vec<_> = (0..6).map(|_| commands.spawn_empty().id()).collect(); + for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations + .iter() + .zip(&point_light_frusta.unwrap().frusta) + .zip(light_view_entities.iter().copied()) + .enumerate() + { + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + face_index as u32, + ); + + let base_array_layer = + (light_index * auxiliary_entities_size * 6 + aux_entity_index * 6 + face_index) as u32; + let depth_attachment_key = ((light_index + face_index) as u32, auxiliary_entity.map(|(entity, _)| entity)); + let depth_attachment = point_light_depth_attachments + .entry(depth_attachment_key) + .or_insert_with(|| { + let depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_point_light_{}_{}_aux_{:?}", + light_index, + face_index_to_name(face_index), + auxiliary_entity.map(|(_, main_entity)| main_entity), + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map_size as u32, + point_light_shadow_map_size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *frustum, + LightEntity::Point { + light_entity: *light_entity, + face_index, + }, + )); + + if auxiliary_entity.is_none() { + commands + .entity(view_light_entity) + .insert(RootNonCameraView(Core3d.intern())); + } else if let Some((entity, _)) = auxiliary_entity + && let Some((_, _, _, _, maybe_render_layers, _, _, _)) = views.get(*entity).ok() + && let Some(render_layers) = maybe_render_layers + { + // When render_layers is fixed for lights, this should make sure + // that the resulting render_layers is the intersection of the light's and the view's + commands + .entity(view_light_entity) + .insert(render_layers.clone()); + } + + if !matches!( + gpu_preprocessing_support_max_supported_mode, + GpuPreprocessingMode::Culling + ) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + } + + if let Some((entity, main_entity)) = auxiliary_entity { + point_and_spot_light_view_entities.1.insert( + main_entity.entity(), + light_view_entities.iter().copied().collect(), + ); + + // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass` + commands + .entity(*entity) + .insert(PointLightShadowViewEntities { + shadow_view_entities: light_view_entities, + }); + } else { + point_and_spot_light_view_entities.0 = light_view_entities; + } +} + fn despawn_entities(commands: &mut Commands, entities: Vec) { if entities.is_empty() { return; From 9d48c134a7c32d3c1f934128acb369d587a39698 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sat, 13 Jun 2026 23:59:20 -0400 Subject: [PATCH 05/29] finish impl for point light shadow maps --- crates/bevy_pbr/src/render/light.rs | 153 ++++++++++++++++++++++------ 1 file changed, 121 insertions(+), 32 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 959545d95b410..29ef24f369b1a 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1414,7 +1414,8 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); - let mut point_light_depth_attachments = HashMap::<(u32, Option), DepthAttachment>::default(); + let mut point_light_depth_attachments = + HashMap::<(u32, Option), DepthAttachment>::default(); let mut directional_light_depth_attachments = HashMap::::default(); let point_light_depth_texture = texture_cache.get( @@ -1535,24 +1536,24 @@ pub fn prepare_lights( continue; } - if point_and_spot_light_view_entities.0.is_empty() - && point_and_spot_light_view_entities.1.is_empty() - { - let light_index = *global_clusterable_object_meta - .entity_to_index - .get(light_entity) - .unwrap(); - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = GlobalTransform::from_translation(light.transform.translation()); - - let cube_face_projection = Mat4::perspective_infinite_reverse_rh( - core::f32::consts::FRAC_PI_2, - 1.0, - light.shadow_map_near_z, - ); + // Some common data used when creating point shadow maps + let light_index = *global_clusterable_object_meta + .entity_to_index + .get(light_entity) + .unwrap(); + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = GlobalTransform::from_translation(light.transform.translation()); + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + let init = point_and_spot_light_view_entities.0.is_empty() + && point_and_spot_light_view_entities.1.is_empty(); + if init { for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { create_point_shadow_maps( &mut commands, @@ -1571,8 +1572,91 @@ pub fn prepare_lights( gpu_preprocessing_support.max_supported_mode, ); } - } else if changed_point_lights.get(*light_entity).is_ok() { - // If the point light was changed, update the `ExtractedView` and frustum only. + } else { + // Remove any view-specific shadow maps that are no longer requested + for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter() { + if !auxiliary_entities + .iter() + .any(|aux_entity| aux_entity.is_some_and(|(entity, _)| entity == *view_entity)) + { + for view_light_entity in light_view_entities.iter() { + commands.entity(*view_light_entity).despawn(); + } + commands + .entity(*view_entity) + .remove::(); + for face_index in 0..cube_face_rotations.len() { + point_light_depth_attachments + .remove(&((light_index + face_index) as u32, Some(*view_entity))); + } + } + } + + // Remove the non view specific shadow map if it is no longer needed + if !point_and_spot_light_view_entities.0.is_empty() + && views_count - auxiliary_entities.len() == 0 + { + for view_light_entity in point_and_spot_light_view_entities.0.iter() { + commands.entity(*view_light_entity).despawn(); + } + point_and_spot_light_view_entities.0.clear(); + } + + // Add any view-specific shadow maps that are missing + for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + if let Some((entity, _)) = auxiliary_entity + && point_and_spot_light_view_entities.1.get(entity).is_none() + { + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + views, + (&cube_face_rotations, point_light_frusta), + &point_light_depth_texture, + (light_entity, light_main_entity, light_index), + ( + point_light_shadow_map.size, + view_translation, + cube_face_projection, + ), + // The non view specific shadow map is at the end of the auxiliary_entities vec. + (auxiliary_entities.len(), &None, aux_entity_index), + gpu_preprocessing_support.max_supported_mode, + ); + } + } + + // Add the non view specific shadow map if it is needed. + if point_and_spot_light_view_entities.0.is_empty() + && auxiliary_entities[auxiliary_entities.len() - 1] == None + { + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + views, + (&cube_face_rotations, point_light_frusta), + &point_light_depth_texture, + (light_entity, light_main_entity, light_index), + ( + point_light_shadow_map.size, + view_translation, + cube_face_projection, + ), + // The non view specific shadow map is at the end of the auxiliary_entities vec. + ( + auxiliary_entities.len(), + &None, + auxiliary_entities.len() - 1, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } + } + + // If the point light was changed, update the `ExtractedView` and frustum of any shadow maps. + if !init && changed_point_lights.get(*light_entity).is_ok() { let view_translation = GlobalTransform::from_translation(light.transform.translation()); let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, @@ -1580,13 +1664,16 @@ pub fn prepare_lights( light.shadow_map_near_z, ); for auxiliary_entity in auxiliary_entities.iter() { - let light_view_entities = if let Some((_, main_entity)) = auxiliary_entity { - let entry = point_and_spot_light_view_entities - .1 - .get(&main_entity.entity()); + let light_view_entities = if let Some((entity, _)) = auxiliary_entity { + let entry = point_and_spot_light_view_entities.1.get(entity); if let Some(entities) = entry { entities } else { + // This should not happen - point_and_spot_light_view_entities.1 + // should be synced with auxiliary_entities at this point + // with the logic in the "if init else" block, + // but it is not a fatal error if it does happen. It should have + // been inserted with the updated `ExtractedView` and frustum anyway. continue; } } else { @@ -1625,8 +1712,6 @@ pub fn prepare_lights( } } } - // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. - // This also has implications for the stale depth attachments indices... sigh. // Initialize the shadow render phases. We have to do this even if we've // already created the views in order to clear out old data. @@ -1812,6 +1897,8 @@ pub fn prepare_lights( } // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. + // Update depth attachment. + // despawn the PointLightShadowViewEntities too. for auxiliary_entity in auxiliary_entities.iter() { let retained_view_entity = RetainedViewEntity::new( @@ -2288,7 +2375,10 @@ fn create_point_shadow_maps( let base_array_layer = (light_index * auxiliary_entities_size * 6 + aux_entity_index * 6 + face_index) as u32; - let depth_attachment_key = ((light_index + face_index) as u32, auxiliary_entity.map(|(entity, _)| entity)); + let depth_attachment_key = ( + (light_index + face_index) as u32, + auxiliary_entity.map(|(entity, _)| entity), + ); let depth_attachment = point_light_depth_attachments .entry(depth_attachment_key) .or_insert_with(|| { @@ -2365,11 +2455,10 @@ fn create_point_shadow_maps( } } - if let Some((entity, main_entity)) = auxiliary_entity { - point_and_spot_light_view_entities.1.insert( - main_entity.entity(), - light_view_entities.iter().copied().collect(), - ); + if let Some((entity, _)) = auxiliary_entity { + point_and_spot_light_view_entities + .1 + .insert(*entity, light_view_entities.iter().copied().collect()); // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass` commands From 975df5a65389e1042397b377153439042df25b6f Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 01:44:24 -0400 Subject: [PATCH 06/29] simplify some logic --- crates/bevy_pbr/src/render/light.rs | 49 +++++++++-------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 29ef24f369b1a..dc82d792e8d6d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -972,7 +972,8 @@ pub struct ViewLightEntities { } /// A component that holds the shadow views generated by a pointlight -/// associated with a specific camera. +/// associated with a specific camera. This is placed on the +/// camera entity. #[derive(Component)] pub struct PointLightShadowViewEntities { /// The pointlight shadow views associated with a camera that has opted @@ -981,7 +982,8 @@ pub struct PointLightShadowViewEntities { } /// A component that holds the shadow view generated by a spotlight -/// associated with a specific camera. +/// associated with a specific camera. This is placed on the +/// camera entity. #[derive(Component)] pub struct SpotLightShadowViewEntity { /// The spotlight shadow view associated with a camera that has opted @@ -1602,11 +1604,14 @@ pub fn prepare_lights( point_and_spot_light_view_entities.0.clear(); } - // Add any view-specific shadow maps that are missing + // Add any shadow maps that are missing for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { - if let Some((entity, _)) = auxiliary_entity - && point_and_spot_light_view_entities.1.get(entity).is_none() - { + let insert_shadow_map = match auxiliary_entity { + Some((entity, _)) => point_and_spot_light_view_entities.1.get(entity).is_none(), + None => point_and_spot_light_view_entities.0.is_empty(), + }; + + if insert_shadow_map { create_point_shadow_maps( &mut commands, &mut point_and_spot_light_view_entities, @@ -1621,38 +1626,11 @@ pub fn prepare_lights( cube_face_projection, ), // The non view specific shadow map is at the end of the auxiliary_entities vec. - (auxiliary_entities.len(), &None, aux_entity_index), + (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), gpu_preprocessing_support.max_supported_mode, ); } } - - // Add the non view specific shadow map if it is needed. - if point_and_spot_light_view_entities.0.is_empty() - && auxiliary_entities[auxiliary_entities.len() - 1] == None - { - create_point_shadow_maps( - &mut commands, - &mut point_and_spot_light_view_entities, - &mut point_light_depth_attachments, - views, - (&cube_face_rotations, point_light_frusta), - &point_light_depth_texture, - (light_entity, light_main_entity, light_index), - ( - point_light_shadow_map.size, - view_translation, - cube_face_projection, - ), - // The non view specific shadow map is at the end of the auxiliary_entities vec. - ( - auxiliary_entities.len(), - &None, - auxiliary_entities.len() - 1, - ), - gpu_preprocessing_support.max_supported_mode, - ); - } } // If the point light was changed, update the `ExtractedView` and frustum of any shadow maps. @@ -2460,7 +2438,8 @@ fn create_point_shadow_maps( .1 .insert(*entity, light_view_entities.iter().copied().collect()); - // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass` + // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass`. + // This is placed on the auxiliary view entity. commands .entity(*entity) .insert(PointLightShadowViewEntities { From 8c5a50b5ea658a03ca430da78d4583fc4a18384a Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 02:20:47 -0400 Subject: [PATCH 07/29] refactor: separate out create spot light shadow map into fn --- crates/bevy_pbr/src/render/light.rs | 252 ++++++++++++++++++---------- 1 file changed, 164 insertions(+), 88 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index dc82d792e8d6d..3f77ee36c4445 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1566,7 +1566,7 @@ pub fn prepare_lights( &point_light_depth_texture, (light_entity, light_main_entity, light_index), ( - point_light_shadow_map.size, + point_light_shadow_map.size as u32, view_translation, cube_face_projection, ), @@ -1621,7 +1621,7 @@ pub fn prepare_lights( &point_light_depth_texture, (light_entity, light_main_entity, light_index), ( - point_light_shadow_map.size, + point_light_shadow_map.size as u32, view_translation, cube_face_projection, ), @@ -1743,87 +1743,23 @@ pub fn prepare_lights( let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { - // TODO take into consideration the current aux_entity too - let base_array_layer = - (num_directional_cascades_enabled + light_index + aux_entity_index) as u32; - - let depth_attachment = directional_light_depth_attachments - .entry(base_array_layer) - .or_insert_with(|| { - let depth_texture_view = directional_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("spot_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - usage: None, - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer, - array_layer_count: Some(1u32), - }); - - DepthAttachment::new(depth_texture_view, Some(0.0)) - }) - .clone(); - - let view_light_entity = commands.spawn_empty().id(); - let retained_view_entity = RetainedViewEntity::new( - *light_main_entity, - auxiliary_entity.map(|(_, main_entity)| main_entity), - 0, - ); - - commands.entity(view_light_entity).insert(( - ShadowView { - depth_attachment, - pass_name: format!( - "shadow_spot_light_{}_aux_{:?}", - light_index, - auxiliary_entity.map(|(_, main_entity)| main_entity), - ), - }, - ExtractedView { - retained_view_entity, - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: spot_world_from_view, - clip_from_view: spot_projection, - clip_from_world: None, - target_format: CORE_3D_DEPTH_FORMAT, - color_grading: Default::default(), - invert_culling: false, - }, - *spot_light_frustum.unwrap(), - LightEntity::Spot { - light_entity: *light_entity, - }, - )); - if auxiliary_entity.is_none() { - commands - .entity(view_light_entity) - .insert(RootNonCameraView(Core3d.intern())); - } - - if !matches!( + create_spot_shadow_map( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut directional_light_depth_attachments, + views, + num_directional_cascades_enabled, + &directional_light_depth_texture, + (light_entity, light_main_entity, light_index), + ( + directional_light_shadow_map.size as u32, + spot_world_from_view, + spot_projection, + spot_light_frustum, + ), + (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) { - commands.entity(view_light_entity).insert(NoIndirectDrawing); - } - - if let Some((_, main_entity)) = auxiliary_entity { - point_and_spot_light_view_entities - .1 - .insert(main_entity.entity(), vec![view_light_entity]); - } else { - point_and_spot_light_view_entities.0 = vec![view_light_entity]; - } + ); } } else if changed_point_lights.get(*light_entity).is_ok() { // If the spot light was changed, update the `ExtractedView` only. @@ -2299,7 +2235,7 @@ pub fn prepare_lights( } /// Creates six point shadow map for retained_view_entities identified by the light_main_entity, -/// the auxiliary_entity (the main part), and the six cube face rotation indices. +/// the auxiliary_entity (the main_entity part), and the six cube face rotation indices. /// /// Six new depth attachments are created per unique auxiliary entity, identified by its /// its `aux_entity_index`. @@ -2327,7 +2263,7 @@ fn create_point_shadow_maps( point_light_depth_texture: &CachedTexture, (light_entity, light_main_entity, light_index): (&Entity, &MainEntity, usize), (point_light_shadow_map_size, view_translation, cube_face_projection): ( - usize, + u32, GlobalTransform, Mat4, ), @@ -2393,8 +2329,8 @@ fn create_point_shadow_maps( viewport: UVec4::new( 0, 0, - point_light_shadow_map_size as u32, - point_light_shadow_map_size as u32, + point_light_shadow_map_size, + point_light_shadow_map_size, ), world_from_view: view_translation * *view_rotation, clip_from_world: None, @@ -2450,6 +2386,141 @@ fn create_point_shadow_maps( } } +/// Creates the spot shadow map for retained_view_entities identified by the light_main_entity and +/// the auxiliary_entity (the main_entity part). +/// +/// An auxiliary_entity of `None` creates a shadow map that will be shared across cameras. +/// If a camera requests for its own shadow maps, the auxiliary entity is the requesting camera. +fn create_spot_shadow_map( + commands: &mut Commands, + point_and_spot_light_view_entities: &mut Mut<'_, PointAndSpotLightViewEntities>, + directional_light_depth_attachments: &mut HashMap, + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + Option<&ExtractedCamera>, + ), + With, + >, + num_directional_cascades_enabled: usize, + directional_light_depth_texture: &CachedTexture, + (light_entity, light_main_entity, light_index): (&Entity, &MainEntity, usize), + (directional_light_shadow_map_size, spot_world_from_view, spot_projection, spot_light_frustum): (u32, + GlobalTransform, + Mat4,Option<&Frustum>), + (auxiliary_entities_size, auxiliary_entity, aux_entity_index): ( + usize, + &Option<(Entity, MainEntity)>, + usize, + ), + gpu_preprocessing_support_max_supported_mode: GpuPreprocessingMode, +) { + let base_array_layer = (num_directional_cascades_enabled + + light_index * auxiliary_entities_size + + aux_entity_index) as u32; + + let depth_attachment = directional_light_depth_attachments + // TODO change how the map key is structured + .entry(base_array_layer) + .or_insert_with(|| { + let depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("spot_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + let view_light_entity = commands.spawn_empty().id(); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + auxiliary_entity.map(|(_, main_entity)| main_entity), + 0, + ); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_spot_light_{}_aux_{:?}", + light_index, + auxiliary_entity.map(|(_, main_entity)| main_entity), + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map_size, + directional_light_shadow_map_size, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + target_format: CORE_3D_DEPTH_FORMAT, + color_grading: Default::default(), + invert_culling: false, + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { + light_entity: *light_entity, + }, + )); + if auxiliary_entity.is_none() { + commands + .entity(view_light_entity) + .insert(RootNonCameraView(Core3d.intern())); + } else if let Some((entity, _)) = auxiliary_entity + && let Some((_, _, _, _, maybe_render_layers, _, _, _)) = views.get(*entity).ok() + && let Some(render_layers) = maybe_render_layers + { + // When render_layers is fixed for lights, this should make sure + // that the resulting render_layers is the intersection of the light's and the view's + commands + .entity(view_light_entity) + .insert(render_layers.clone()); + } + + if !matches!( + gpu_preprocessing_support_max_supported_mode, + GpuPreprocessingMode::Culling + ) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + if let Some((entity, _)) = auxiliary_entity { + point_and_spot_light_view_entities + .1 + .insert(*entity, vec![view_light_entity]); + + // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass`. + // This is placed on the auxiliary view entity. + commands.entity(*entity).insert(SpotLightShadowViewEntity { + shadow_view_entity: view_light_entity, + }); + } else { + point_and_spot_light_view_entities.0 = vec![view_light_entity]; + } +} + fn despawn_entities(commands: &mut Commands, entities: Vec) { if entities.is_empty() { return; @@ -3055,12 +3126,17 @@ pub fn shared_shadow_pass( /// and opt-in camera-specific point and spot light shadows pub fn per_view_shadow_pass( world: &World, - view: ViewQuery<(&ViewLightEntities, &PointLightShadowViewEntities)>, + // TODO I should split this... + view: ViewQuery<( + &ViewLightEntities, + &PointLightShadowViewEntities, + &SpotLightShadowViewEntity, + )>, view_light_query: Query<(&ShadowView, &ExtractedView, Has)>, shadow_render_phases: Res>, mut ctx: RenderContext, ) { - let (view_lights, point_shadow_views) = view.into_inner(); + let (view_lights, point_shadow_views, spot_shadow_views) = view.into_inner(); for view_light_entity in view_lights.lights.iter().copied() { if let Ok((view_light, extracted_light_view, occlusion_culling)) = From 219bf587dd5fb2aa256a5383d534ecf9a72d9545 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 02:21:17 -0400 Subject: [PATCH 08/29] include spot shadow views in gpu preprocess --- crates/bevy_pbr/src/render/gpu_preprocess.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 3067d424a3b8d..3b2ed8ca32633 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -74,7 +74,7 @@ use tracing::warn; use crate::{ LightEntity, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform, - PointLightShadowViewEntities, + PointLightShadowViewEntities, SpotLightShadowViewEntity, }; use super::{ShadowView, ViewLightEntities}; @@ -531,6 +531,7 @@ pub fn unpack_bins( ( Option<&ViewLightEntities>, Option<&PointLightShadowViewEntities>, + Option<&SpotLightShadowViewEntity>, ), Without, >, @@ -555,11 +556,13 @@ pub fn unpack_bins( // Gather up all views. let view_entity = current_view.entity(); - let (shadow_cascade_views, point_light_shadow_views) = current_view.into_inner(); + let (shadow_cascade_views, point_light_shadow_views, spot_light_shadow_view) = + current_view.into_inner(); let all_views = gather_shadow_cascades_for_view( view_entity, shadow_cascade_views, point_light_shadow_views, + spot_light_shadow_view, &light_query, ); @@ -617,6 +620,7 @@ pub fn early_gpu_preprocess( ( Option<&ViewLightEntities>, Option<&PointLightShadowViewEntities>, + Option<&SpotLightShadowViewEntity>, ), Without, >, @@ -649,11 +653,13 @@ pub fn early_gpu_preprocess( let pass_span = diagnostics.pass_span(&mut compute_pass, "early_mesh_preprocessing"); let view_entity = current_view.entity(); - let (shadow_cascade_views, point_light_shadow_views) = current_view.into_inner(); + let (shadow_cascade_views, point_light_shadow_views, spot_light_shadow_view) = + current_view.into_inner(); let all_views = gather_shadow_cascades_for_view( view_entity, shadow_cascade_views, point_light_shadow_views, + spot_light_shadow_view, &light_query, ); @@ -819,6 +825,7 @@ fn gather_shadow_cascades_for_view( view_entity: Entity, shadow_cascade_views: Option<&ViewLightEntities>, point_light_shadow_views: Option<&PointLightShadowViewEntities>, + spot_light_shadow_view: Option<&SpotLightShadowViewEntity>, light_query: &Query<&LightEntity>, ) -> SmallVec<[Entity; 8]> { let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); @@ -849,6 +856,13 @@ fn gather_shadow_cascades_for_view( .copied(), ); } + if let Some(spot_light_shadow_view) = spot_light_shadow_view + && light_query + .get(spot_light_shadow_view.shadow_view_entity) + .is_ok_and(|light_entity| matches!(*light_entity, LightEntity::Spot { .. })) + { + all_views.push(spot_light_shadow_view.shadow_view_entity); + } all_views } From 0acefb49876b229d8b88b833421b32db68e1481f Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 09:32:08 -0400 Subject: [PATCH 09/29] add spot light to shadow pass code --- crates/bevy_pbr/src/render/light.rs | 81 ++++++++++++++++++----------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 3f77ee36c4445..cfe6107c24a0b 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -3126,48 +3126,67 @@ pub fn shared_shadow_pass( /// and opt-in camera-specific point and spot light shadows pub fn per_view_shadow_pass( world: &World, - // TODO I should split this... view: ViewQuery<( - &ViewLightEntities, - &PointLightShadowViewEntities, - &SpotLightShadowViewEntity, + Option<&ViewLightEntities>, + Option<&PointLightShadowViewEntities>, + Option<&SpotLightShadowViewEntity>, )>, view_light_query: Query<(&ShadowView, &ExtractedView, Has)>, shadow_render_phases: Res>, mut ctx: RenderContext, ) { - let (view_lights, point_shadow_views, spot_shadow_views) = view.into_inner(); + let (view_lights, point_shadow_views, spot_shadow_view) = view.into_inner(); - for view_light_entity in view_lights.lights.iter().copied() { - if let Ok((view_light, extracted_light_view, occlusion_culling)) = - view_light_query.get(view_light_entity) - { - view_shadow_pass::( - view_light_entity, - view_light, - extracted_light_view, - occlusion_culling, - world, - &shadow_render_phases, - &mut ctx, - ); + if let Some(view_lights) = view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + if let Ok((view_light, extracted_light_view, occlusion_culling)) = + view_light_query.get(view_light_entity) + { + view_shadow_pass::( + view_light_entity, + view_light, + extracted_light_view, + occlusion_culling, + world, + &shadow_render_phases, + &mut ctx, + ); + } } } - for shadow_view_entity in point_shadow_views.shadow_view_entities.iter().copied() { - if let Ok((view_light, extracted_light_view, occlusion_culling)) = - view_light_query.get(shadow_view_entity) - { - view_shadow_pass::( - shadow_view_entity, - view_light, - extracted_light_view, - occlusion_culling, - world, - &shadow_render_phases, - &mut ctx, - ); + + if let Some(point_shadow_views) = point_shadow_views { + for shadow_view_entity in point_shadow_views.shadow_view_entities.iter().copied() { + if let Ok((view_light, extracted_light_view, occlusion_culling)) = + view_light_query.get(shadow_view_entity) + { + view_shadow_pass::( + shadow_view_entity, + view_light, + extracted_light_view, + occlusion_culling, + world, + &shadow_render_phases, + &mut ctx, + ); + } } } + + if let Some(spot_shadow_view) = spot_shadow_view + && let Ok((view_light, extracted_light_view, occlusion_culling)) = + view_light_query.get(spot_shadow_view.shadow_view_entity) + { + view_shadow_pass::( + spot_shadow_view.shadow_view_entity, + view_light, + extracted_light_view, + occlusion_culling, + world, + &shadow_render_phases, + &mut ctx, + ); + } } /// A common helper function to render a shadow map. From 335a1323fae100832efff22e5a0077167ec065a6 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 10:28:55 -0400 Subject: [PATCH 10/29] finish impl for prepare spot light shadow maps --- crates/bevy_pbr/src/render/light.rs | 130 +++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 22 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index cfe6107c24a0b..dde77dd747cf8 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1418,7 +1418,8 @@ pub fn prepare_lights( let mut point_light_depth_attachments = HashMap::<(u32, Option), DepthAttachment>::default(); - let mut directional_light_depth_attachments = HashMap::::default(); + let mut directional_light_depth_attachments = + HashMap::<(u32, Option), DepthAttachment>::default(); let point_light_depth_texture = texture_cache.get( &render_device, @@ -1479,8 +1480,8 @@ pub fn prepare_lights( height: (directional_light_shadow_map.size as u32) .min(render_device.limits().max_texture_dimension_2d), depth_or_array_layers: (num_directional_cascades_enabled - + spot_light_shadow_maps_count) - .max(1) as u32, + + spot_light_shadow_maps_count * auxiliary_entities.len()) + .max(1) as u32, }, mip_level_count: 1, sample_count: 1, @@ -1516,6 +1517,7 @@ pub fn prepare_lights( let mut live_views = EntityHashSet::with_capacity(views_count); + // point lights // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query for light_entity in point_light_entities .iter() @@ -1576,7 +1578,10 @@ pub fn prepare_lights( } } else { // Remove any view-specific shadow maps that are no longer requested - for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter() { + let mut to_remove = vec![]; + for (view_entity, light_view_entities) in + point_and_spot_light_view_entities.1.iter_mut() + { if !auxiliary_entities .iter() .any(|aux_entity| aux_entity.is_some_and(|(entity, _)| entity == *view_entity)) @@ -1591,8 +1596,13 @@ pub fn prepare_lights( point_light_depth_attachments .remove(&((light_index + face_index) as u32, Some(*view_entity))); } + light_view_entities.clear(); + to_remove.push(*view_entity); } } + to_remove.iter().for_each(|entity| { + point_and_spot_light_view_entities.1.remove(entity); + }); // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() @@ -1732,16 +1742,22 @@ pub fn prepare_lights( continue; } - if point_and_spot_light_view_entities.0.is_empty() - && point_and_spot_light_view_entities.1.is_empty() - { - let spot_world_from_view = spot_light_world_from_view(&light.transform); - let spot_world_from_view = spot_world_from_view.into(); + let spot_world_from_view = spot_light_world_from_view(&light.transform); + let spot_world_from_view = spot_world_from_view.into(); - let angle = light.spot_light_angles.expect("lights should be sorted so that \ - [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; - let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); + let angle = light + .spot_light_angles + .expect( + "lights should be sorted so that \ + [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights", + ) + .1; + let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); + let init = point_and_spot_light_view_entities.0.is_empty() + && point_and_spot_light_view_entities.1.is_empty(); + + if init { for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { create_spot_shadow_map( &mut commands, @@ -1761,8 +1777,75 @@ pub fn prepare_lights( gpu_preprocessing_support.max_supported_mode, ); } - } else if changed_point_lights.get(*light_entity).is_ok() { - // If the spot light was changed, update the `ExtractedView` only. + } else { + // Remove any view-specific shadow maps that are no longer requested + let mut to_remove = vec![]; + for (view_entity, light_view_entities) in + point_and_spot_light_view_entities.1.iter_mut() + { + if !auxiliary_entities + .iter() + .any(|aux_entity| aux_entity.is_some_and(|(entity, _)| entity == *view_entity)) + { + for view_light_entity in light_view_entities.iter() { + commands.entity(*view_light_entity).despawn(); + } + commands + .entity(*view_entity) + .remove::(); + directional_light_depth_attachments.remove(&( + (num_directional_cascades_enabled + light_index) as u32, + Some(*view_entity), + )); + light_view_entities.clear(); + to_remove.push(*view_entity); + } + } + to_remove.iter().for_each(|entity| { + point_and_spot_light_view_entities.1.remove(entity); + }); + + // Remove the non view specific shadow map if it is no longer needed + if !point_and_spot_light_view_entities.0.is_empty() + && views_count - auxiliary_entities.len() == 0 + { + for view_light_entity in point_and_spot_light_view_entities.0.iter() { + commands.entity(*view_light_entity).despawn(); + } + point_and_spot_light_view_entities.0.clear(); + } + + // Add any shadow maps that are missing + for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + let insert_shadow_map = match auxiliary_entity { + Some((entity, _)) => point_and_spot_light_view_entities.1.get(entity).is_none(), + None => point_and_spot_light_view_entities.0.is_empty(), + }; + + if insert_shadow_map { + create_spot_shadow_map( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut directional_light_depth_attachments, + views, + num_directional_cascades_enabled, + &directional_light_depth_texture, + (light_entity, light_main_entity, light_index), + ( + directional_light_shadow_map.size as u32, + spot_world_from_view, + spot_projection, + spot_light_frustum, + ), + (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + gpu_preprocessing_support.max_supported_mode, + ); + } + } + } + + if !init && changed_point_lights.get(*light_entity).is_ok() { + // If the spot light was changed, update the `ExtractedView` and the frustum let spot_world_from_view = spot_light_world_from_view(&light.transform); let spot_world_from_view = spot_world_from_view.into(); @@ -1779,6 +1862,11 @@ pub fn prepare_lights( if let Some(v) = entry { v[0] } else { + // This should not happen - point_and_spot_light_view_entities.1 + // should be synced with auxiliary_entities at this point + // with the logic in the "if init else" block, + // but it is not a fatal error if it does happen. It should have + // been inserted with the updated `ExtractedView` and frustum anyway. continue; } } else { @@ -1810,10 +1898,6 @@ pub fn prepare_lights( } } - // TODO if there were added views or removed views, we need to also spawn/despawn the appropriate light view entity. - // Update depth attachment. - // despawn the PointLightShadowViewEntities too. - for auxiliary_entity in auxiliary_entities.iter() { let retained_view_entity = RetainedViewEntity::new( *light_main_entity, @@ -2394,7 +2478,7 @@ fn create_point_shadow_maps( fn create_spot_shadow_map( commands: &mut Commands, point_and_spot_light_view_entities: &mut Mut<'_, PointAndSpotLightViewEntities>, - directional_light_depth_attachments: &mut HashMap, + directional_light_depth_attachments: &mut HashMap<(u32, Option), DepthAttachment>, views: Query< ( Entity, @@ -2424,10 +2508,12 @@ fn create_spot_shadow_map( let base_array_layer = (num_directional_cascades_enabled + light_index * auxiliary_entities_size + aux_entity_index) as u32; - + let depth_attachment_key = ( + (num_directional_cascades_enabled + light_index) as u32, + auxiliary_entity.map(|(entity, _)| entity), + ); let depth_attachment = directional_light_depth_attachments - // TODO change how the map key is structured - .entry(base_array_layer) + .entry(depth_attachment_key) .or_insert_with(|| { let depth_texture_view = directional_light_depth_texture From d01ff65b2b925088b8a8001156dc210121d59210 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 10:41:31 -0400 Subject: [PATCH 11/29] use correct index for spot shadows --- crates/bevy_pbr/src/render/pbr_functions.wgsl | 8 +++++--- crates/bevy_pbr/src/render/shadows.wgsl | 4 ++-- crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl | 10 +++++----- crates/bevy_render/src/view/mod.rs | 8 ++++---- crates/bevy_render/src/view/view.wgsl | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 9df3403d80562..76a20fb3db844 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -465,7 +465,7 @@ fn apply_pbr_lighting( view_bindings::view.view_from_world[2].z, view_bindings::view.view_from_world[3].z ), in.world_position); - let point_shadow_map_offset = view_bindings::view.point_shadow_map_index_offset; + let point_spot_shadow_map_offset = view_bindings::view.point_spot_shadow_map_index_offset; let cluster_index = clustering::view_fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index); @@ -494,7 +494,7 @@ fn apply_pbr_lighting( var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = shadows::fetch_point_shadow(light_id, point_shadow_map_offset, in.world_position, in.world_normal, in.frag_coord.xy); + shadow = shadows::fetch_point_shadow(light_id, point_spot_shadow_map_offset, in.world_position, in.world_normal, in.frag_coord.xy); } #ifdef CONTACT_SHADOWS @@ -524,7 +524,7 @@ fn apply_pbr_lighting( var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - transmitted_shadow = shadows::fetch_point_shadow(light_id, point_shadow_map_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + transmitted_shadow = shadows::fetch_point_shadow(light_id, point_spot_shadow_map_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); } let transmitted_light_contrib = @@ -555,6 +555,7 @@ fn apply_pbr_lighting( mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_spot_shadow( light_id, + point_spot_shadow_map_offset, in.world_position, in.world_normal, view_bindings::clustered_lights.data[light_id].shadow_map_near_z, @@ -591,6 +592,7 @@ fn apply_pbr_lighting( && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_spot_shadow( light_id, + point_spot_shadow_map_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, view_bindings::clustered_lights.data[light_id].shadow_map_near_z, diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index baabd1ff111b6..8166e1a27ae3f 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -72,6 +72,7 @@ fn fetch_point_shadow( fn fetch_spot_shadow( light_id: u32, + view_specific_spot_shadow_map_offset: u32, frag_position: vec4, surface_normal: vec3, near_z: f32, @@ -114,8 +115,7 @@ fn fetch_spot_shadow( let depth = near_z / -projected_position.z; // If soft shadows are enabled, use the PCSS path. - // TODO array_index needs to be updated here. - let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset; + let array_index = i32(light_id) + i32(view_specific_spot_shadow_map_offset) + view_bindings::lights.spot_light_shadowmap_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_map_pcss( shadow_uv, diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 3eb7561db0e99..6668b8567d33f 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -129,7 +129,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the view. let exposure = view.exposure; - let point_shadow_map_offset = view.point_shadow_map_offset; + let point_spot_shadow_map_offset = view.point_spot_shadow_map_offset; // Sample the depth to put an upper bound on the length of the ray (as we // shouldn't trace through solid objects). If this is multisample, just use @@ -393,7 +393,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_point_shadow_without_normal(light_id, point_shadow_map_offset, vec4(P_world, 1.0), position.xy); + shadow = fetch_point_shadow_without_normal(light_id, point_spot_shadow_map_offset, vec4(P_world, 1.0), position.xy); } local_light_attenuation *= shadow; } else { @@ -414,7 +414,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_spot_shadow_without_normal(light_id, vec4(P_world, 1.0), position.xy); + shadow = fetch_spot_shadow_without_normal(light_id, point_spot_shadow_map_offset, vec4(P_world, 1.0), position.xy); } local_light_attenuation *= spot_attenuation * shadow; } @@ -479,7 +479,7 @@ fn fetch_point_shadow_without_normal(light_id: u32, point_shadow_map_offset: u32 return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_index, frag_coord_xy); } -fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { +fn fetch_spot_shadow_without_normal(light_id: u32, view_specific_spot_shadow_map_offset: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { let light = &clustered_lights.data[light_id]; let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; @@ -518,7 +518,7 @@ fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4, fra return sample_shadow_map( shadow_uv, depth, - i32(light_id) + lights.spot_light_shadowmap_offset, + i32(light_id) + i32(view_specific_spot_shadow_map_offset) + lights.spot_light_shadowmap_offset, frag_coord_xy, SPOT_SHADOW_TEXEL_SIZE ); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 91d6cfcd27224..a460d36794f19 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -671,9 +671,9 @@ pub struct ViewUniform { pub color_grading: ColorGradingUniform, pub mip_bias: f32, pub frame_count: u32, - /// This offset is used to fetch the correct point shadow map to use for this view. - /// This is used to accommodates views that may be configured to have their own point shadow maps. - pub point_shadow_map_index_offset: u32, + /// This offset is used to fetch the correct point or spot shadow map to use for this view. + /// This is used to accommodates views that may be configured to have their own point or spot shadow maps. + pub point_spot_shadow_map_index_offset: u32, } #[derive(Resource)] @@ -1141,7 +1141,7 @@ pub fn prepare_view_uniforms( color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, - point_shadow_map_index_offset: *own_shadow_map_view_to_index + point_spot_shadow_map_index_offset: *own_shadow_map_view_to_index .get(&entity) .unwrap_or(&(own_shadow_map_view_to_index.len())) as u32, diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 56219b4d15d5b..2c966e79aa9eb 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -72,7 +72,7 @@ struct View { frame_count: u32, // This offset is used to fetch the correct point shadow map to use for this view. // This is used to accommodates views that may be configured to have their own point shadow maps. - point_shadow_map_index_offset: u32, + point_spot_shadow_map_index_offset: u32, }; /// World space: From 63fc417767f5f8eba4ad33245b0d6188592b522c Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 10:44:59 -0400 Subject: [PATCH 12/29] refactor: light_index -> array_index --- .../bevy_pbr/src/render/shadow_sampling.wgsl | 86 +++++++++---------- crates/bevy_pbr/src/render/shadows.wgsl | 6 +- .../src/volumetric_fog/volumetric_fog.wgsl | 4 +- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index a373f8cd1fb93..0e3b2ae2ed192 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -321,7 +321,7 @@ fn sample_shadow_map_pcss( // behavior due to some of the fragments in a quad (2x2 fragments) being // processed not being sampled, and this messing with mip-mapping functionality. // The shadow maps have no mipmaps so Level just samples from LOD 0. -fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_index: u32) -> f32 { +fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, array_index: u32) -> f32 { #ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT return textureSampleCompare( view_bindings::point_shadow_textures, @@ -334,7 +334,7 @@ fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_inde view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_comparison_sampler, light_local, - i32(light_index), + i32(array_index), depth ); #endif @@ -345,7 +345,7 @@ fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_inde fn search_for_blockers_in_shadow_cubemap_hardware( light_local: vec3, depth: f32, - light_index: u32, + array_index: u32, ) -> vec2 { #ifdef WEBGL2 // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled @@ -366,7 +366,7 @@ fn search_for_blockers_in_shadow_cubemap_hardware( view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_linear_sampler, light_local, - i32(light_index), + i32(array_index), ); #endif @@ -386,12 +386,12 @@ fn sample_shadow_cubemap_at_offset( y_basis: vec3, light_local: vec3, depth: f32, - light_index: u32, + array_index: u32, ) -> f32 { return sample_shadow_cubemap_hardware( light_local + position.x * x_basis + position.y * y_basis, depth, - light_index + array_index ) * coeff; } @@ -406,12 +406,12 @@ fn search_for_blockers_in_shadow_cubemap_at_offset( y_basis: vec3, light_local: vec3, depth: f32, - light_index: u32, + array_index: u32, ) -> vec2 { return search_for_blockers_in_shadow_cubemap_hardware( light_local + position.x * x_basis + position.y * y_basis, depth, - light_index + array_index ); } @@ -425,7 +425,7 @@ fn sample_shadow_cubemap_gaussian( depth: f32, scale: f32, distance_to_light: f32, - light_index: u32, + array_index: u32, ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. @@ -434,28 +434,28 @@ fn sample_shadow_cubemap_gaussian( var sum: f32 = 0.0; sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7], - basis[0], basis[1], light_local, depth, light_index); + basis[0], basis[1], light_local, depth, array_index); return sum; } @@ -468,7 +468,7 @@ fn sample_shadow_cubemap_jittered( frag_coord_xy: vec2, scale: f32, distance_to_light: f32, - light_index: u32, + array_index: u32, temporal: bool, ) -> f32 { let rotation_matrix = random_rotation_matrix(frag_coord_xy, temporal); @@ -496,21 +496,21 @@ fn sample_shadow_cubemap_jittered( var sum: f32 = 0.0; sum += sample_shadow_cubemap_at_offset( - sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset0, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset1, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset2, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset3, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset4, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset5, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset6, 0.125, basis[0], basis[1], light_local, depth, array_index); sum += sample_shadow_cubemap_at_offset( - sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_index); + sample_offset7, 0.125, basis[0], basis[1], light_local, depth, array_index); return sum; } @@ -518,17 +518,17 @@ fn sample_shadow_cubemap( light_local: vec3, distance_to_light: f32, depth: f32, - light_index: u32, + array_index: u32, frag_coord_xy: vec2, ) -> f32 { #ifdef SHADOW_FILTER_METHOD_GAUSSIAN return sample_shadow_cubemap_gaussian( - light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_index); + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, array_index); #else ifdef SHADOW_FILTER_METHOD_TEMPORAL return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE, distance_to_light, light_index, true); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE, distance_to_light, array_index, true); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 - return sample_shadow_cubemap_hardware(light_local, depth, light_index); + return sample_shadow_cubemap_hardware(light_local, depth, array_index); #else // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) @@ -550,7 +550,7 @@ fn search_for_blockers_in_shadow_cubemap( depth: f32, scale: f32, distance_to_light: f32, - light_index: u32, + array_index: u32, ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. @@ -558,21 +558,21 @@ fn search_for_blockers_in_shadow_cubemap( var sum: vec2 = vec2(0.0); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, array_index); sum += search_for_blockers_in_shadow_cubemap_at_offset( - D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_index); + D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, array_index); if (sum.y == 0.0) { return 0.0; @@ -589,21 +589,21 @@ fn sample_shadow_cubemap_pcss( light_local: vec3, distance_to_light: f32, depth: f32, - light_index: u32, + array_index: u32, light_size: f32, frag_coord_xy: vec2, ) -> f32 { let z_blocker = search_for_blockers_in_shadow_cubemap( - light_local, depth, light_size, distance_to_light, light_index); + light_local, depth, light_size, distance_to_light, array_index); // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); #ifdef SHADOW_FILTER_METHOD_TEMPORAL return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_index, true); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, array_index, true); #else return sample_shadow_cubemap_jittered( - light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_index, false); + light_local, depth, frag_coord_xy, POINT_SHADOW_SCALE * blur_size, distance_to_light, array_index, false); #endif } diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 8166e1a27ae3f..91a2e9aa19b48 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -53,13 +53,13 @@ fn fetch_point_shadow( // If soft shadows are enabled, use the PCSS path. Cubemaps assume a // left-handed coordinate space, so we have to flip the z-axis when // sampling. - let light_index = light_id + point_shadow_map_offset; + let array_index = light_id + point_shadow_map_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_cubemap_pcss( frag_ls * flip_z, distance_to_light, depth, - light_index, + array_index, (*light).soft_shadow_size, frag_coord_xy, ); @@ -67,7 +67,7 @@ fn fetch_point_shadow( // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed // coordinate space, so we have to flip the z-axis when sampling. - return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_index, frag_coord_xy); + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, array_index, frag_coord_xy); } fn fetch_spot_shadow( diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 6668b8567d33f..984f11f69303b 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -475,8 +475,8 @@ fn fetch_point_shadow_without_normal(light_id: u32, point_shadow_map_offset: u32 // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, // so we have to flip the z-axis when sampling. let flip_z = vec3(1.0, 1.0, -1.0); - let light_index = light_id + point_shadow_map_offset; - return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_index, frag_coord_xy); + let array_index = light_id + point_shadow_map_offset; + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, array_index, frag_coord_xy); } fn fetch_spot_shadow_without_normal(light_id: u32, view_specific_spot_shadow_map_offset: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { From 979ede1ebc46af23201ff8bc8e1b2494e7ff7348 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:13:04 -0400 Subject: [PATCH 13/29] more words for has_own_shadow_maps doc comment --- crates/bevy_camera/src/camera.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 732af2f39583b..f96f868305044 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -414,10 +414,10 @@ pub struct Camera { /// /// If true, shadow maps unique to this camera will be generated. /// The shadow maps will be generated using the same render layers and HLOD configuration as this camera. - /// The light's render layers must be a superset of this camera's render layers. + /// The light's render layers **must** be a superset of this camera's render layers in order for this to work properly. /// - /// Enabling this setting can have a negative impact on performance, as this camera will - /// not be relying on the camera-agnostic shadow maps generated for certain lights (i.e. SpotLight and PointLight) + /// Enabling this setting can have a negative impact on performance. PointLights in particular + /// will generate 6 additional shadow maps per camera that opts to have its own shadow maps. /// /// Defaults to `false`. pub has_own_shadow_maps: bool, From e877e1787c7ab1f248cfe0efe0dfa2fddb166407 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:13:27 -0400 Subject: [PATCH 14/29] Fix 3d render layers testbed --- examples/testbed/3d.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs index c4553dbb09c89..1ca9a3aba0479 100644 --- a/examples/testbed/3d.rs +++ b/examples/testbed/3d.rs @@ -779,6 +779,8 @@ mod render_layers { shadow_maps_enabled: true, ..default() }, + // The light can create shadows for all three cubes. + RenderLayers::layer(0).with(1).with(2), Transform::from_xyz(4.0, 8.0, 4.0), DespawnOnExit(CURRENT_SCENE), )); @@ -800,6 +802,7 @@ mod render_layers { physical_size: window_half_size, ..default() }), + has_own_shadow_maps: true, ..default() }, DespawnOnExit(CURRENT_SCENE), From c6ea45d02b2af9d1b53fab30e370ca11f02fa830 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:21:45 -0400 Subject: [PATCH 15/29] remove println statements --- crates/bevy_pbr/src/render/light.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index dde77dd747cf8..232f48a7b58f8 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -3008,10 +3008,8 @@ pub fn queue_shadows( let mesh_layers = mesh_instance.render_layers.as_ref().unwrap_or_default(); let view_render_layers = maybe_view_render_layers.unwrap_or_default(); if !view_render_layers.intersects(mesh_layers) { - println!("{view_render_layers:?} {mesh_layers:?} render layers do not intersect"); continue; } - println!("{view_render_layers:?} {mesh_layers:?} render layers do intersect"); let Some(material_instance) = render_material_instances.instances.get(main_entity) else { From 34ccba83f294567eae328b31afdf0cbbf01bcc35 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:38:22 -0400 Subject: [PATCH 16/29] clean up additional shadow maps if light does not have them enabled --- crates/bevy_pbr/src/render/light.rs | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 232f48a7b58f8..90f250ecf7e7d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1537,6 +1537,18 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); + for (view_entity, mut light_view_entities) in + point_and_spot_light_view_entities.1.iter_mut() + { + commands + .entity(*view_entity) + .remove::(); + despawn_entities( + &mut commands, + mem::take(&mut light_view_entities), + ); + } + mem::take(&mut point_and_spot_light_view_entities.1); continue; } @@ -1592,10 +1604,6 @@ pub fn prepare_lights( commands .entity(*view_entity) .remove::(); - for face_index in 0..cube_face_rotations.len() { - point_light_depth_attachments - .remove(&((light_index + face_index) as u32, Some(*view_entity))); - } light_view_entities.clear(); to_remove.push(*view_entity); } @@ -1739,6 +1747,18 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); + for (view_entity, mut light_view_entities) in + point_and_spot_light_view_entities.1.iter_mut() + { + commands + .entity(*view_entity) + .remove::(); + despawn_entities( + &mut commands, + mem::take(&mut light_view_entities), + ); + } + mem::take(&mut point_and_spot_light_view_entities.1); continue; } @@ -1793,10 +1813,6 @@ pub fn prepare_lights( commands .entity(*view_entity) .remove::(); - directional_light_depth_attachments.remove(&( - (num_directional_cascades_enabled + light_index) as u32, - Some(*view_entity), - )); light_view_entities.clear(); to_remove.push(*view_entity); } From 692cf822c1eb11545a32149a99eab1f38cf1eb26 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:47:30 -0400 Subject: [PATCH 17/29] add comment on potentially confusing code in prepare_view_uniforms --- crates/bevy_render/src/view/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index a460d36794f19..9816708bb7449 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -1025,6 +1025,8 @@ pub fn prepare_view_uniforms( let view_count = view_iter.len(); // The logic here (i.e. the usage of sorted cameras) must preserve the ordering used // to generate the list of auxiliary entities for RetainedViewEntities in prepare_lights + // Note that the view-agnostic shadow map, if it exists, is created after all the view-specific shadow + // maps are created, and therefore exists at index own_shadow_map_view_to_index.len() let own_shadow_map_view_to_index: EntityHashMap = sorted_cameras .0 .iter() @@ -1143,6 +1145,8 @@ pub fn prepare_view_uniforms( frame_count: frame_count.0, point_spot_shadow_map_index_offset: *own_shadow_map_view_to_index .get(&entity) + // Refer to the view agnostic point/spot shadow map, + // which is after all of the view-specific ones. .unwrap_or(&(own_shadow_map_view_to_index.len())) as u32, }), From 57c7b8d7258b214cd1c33b4c0df90ff220093820 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:52:30 -0400 Subject: [PATCH 18/29] fmt --- crates/bevy_pbr/src/render/light.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 90f250ecf7e7d..f342295151be9 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1541,12 +1541,9 @@ pub fn prepare_lights( point_and_spot_light_view_entities.1.iter_mut() { commands - .entity(*view_entity) - .remove::(); - despawn_entities( - &mut commands, - mem::take(&mut light_view_entities), - ); + .entity(*view_entity) + .remove::(); + despawn_entities(&mut commands, mem::take(&mut light_view_entities)); } mem::take(&mut point_and_spot_light_view_entities.1); continue; @@ -1751,12 +1748,9 @@ pub fn prepare_lights( point_and_spot_light_view_entities.1.iter_mut() { commands - .entity(*view_entity) - .remove::(); - despawn_entities( - &mut commands, - mem::take(&mut light_view_entities), - ); + .entity(*view_entity) + .remove::(); + despawn_entities(&mut commands, mem::take(&mut light_view_entities)); } mem::take(&mut point_and_spot_light_view_entities.1); continue; From 89ced9c1cbb36b20b8cbbbeb477e75b5d91e8614 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 11:57:52 -0400 Subject: [PATCH 19/29] lint --- crates/bevy_camera/src/camera.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index f96f868305044..7a31a67646c16 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -416,7 +416,7 @@ pub struct Camera { /// The shadow maps will be generated using the same render layers and HLOD configuration as this camera. /// The light's render layers **must** be a superset of this camera's render layers in order for this to work properly. /// - /// Enabling this setting can have a negative impact on performance. PointLights in particular + /// Enabling this setting can have a negative impact on performance. Point lights in particular /// will generate 6 additional shadow maps per camera that opts to have its own shadow maps. /// /// Defaults to `false`. From 6c2da6bde0fdcebbbf66744cf989884269ef73b0 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 12:09:35 -0400 Subject: [PATCH 20/29] clippy --- crates/bevy_pbr/src/render/light.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index f342295151be9..054da01db620d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1537,13 +1537,13 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); - for (view_entity, mut light_view_entities) in + for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter_mut() { commands .entity(*view_entity) .remove::(); - despawn_entities(&mut commands, mem::take(&mut light_view_entities)); + despawn_entities(&mut commands, mem::take(light_view_entities)); } mem::take(&mut point_and_spot_light_view_entities.1); continue; @@ -1744,13 +1744,13 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); - for (view_entity, mut light_view_entities) in + for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter_mut() { commands .entity(*view_entity) .remove::(); - despawn_entities(&mut commands, mem::take(&mut light_view_entities)); + despawn_entities(&mut commands, mem::take(light_view_entities)); } mem::take(&mut point_and_spot_light_view_entities.1); continue; @@ -2328,13 +2328,13 @@ pub fn prepare_lights( .retain(|entity, _| live_shadow_mapping_lights.contains(entity)); } -/// Creates six point shadow map for retained_view_entities identified by the light_main_entity, -/// the auxiliary_entity (the main_entity part), and the six cube face rotation indices. +/// Creates six point shadow map for a `RetainedViewEntity` identified by the `light_main_entity`, +/// the `auxiliary_entity` (the `main_entity` part), and the six cube face rotation indices. /// /// Six new depth attachments are created per unique auxiliary entity, identified by its /// its `aux_entity_index`. /// -/// An auxiliary_entity of `None` creates a shadow map that will be shared across cameras. +/// An `auxiliary_entity` of `None` creates a shadow map that will be shared across cameras. /// If a camera requests for its own shadow maps, the auxiliary entity is the requesting camera. fn create_point_shadow_maps( commands: &mut Commands, @@ -2466,7 +2466,7 @@ fn create_point_shadow_maps( if let Some((entity, _)) = auxiliary_entity { point_and_spot_light_view_entities .1 - .insert(*entity, light_view_entities.iter().copied().collect()); + .insert(*entity, light_view_entities.to_vec()); // Ensure these view-specific shadow maps are rendered in `per_view_shadow_pass`. // This is placed on the auxiliary view entity. @@ -2480,10 +2480,10 @@ fn create_point_shadow_maps( } } -/// Creates the spot shadow map for retained_view_entities identified by the light_main_entity and -/// the auxiliary_entity (the main_entity part). +/// Creates the spot shadow map for a `RetainedViewEntity` identified by the `light_main_entity` and +/// the `auxiliary_entity` (the `main_entity` part). /// -/// An auxiliary_entity of `None` creates a shadow map that will be shared across cameras. +/// An `auxiliary_entity` of `None` creates a shadow map that will be shared across cameras. /// If a camera requests for its own shadow maps, the auxiliary entity is the requesting camera. fn create_spot_shadow_map( commands: &mut Commands, From e9f8e8779d9efb2b5e3cfdd1c08b294c5e1ac1f2 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 13:29:34 -0400 Subject: [PATCH 21/29] fix volumetric fog --- crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 984f11f69303b..00bc09e478b75 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -129,7 +129,7 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the view. let exposure = view.exposure; - let point_spot_shadow_map_offset = view.point_spot_shadow_map_offset; + let point_spot_shadow_map_offset = view.point_spot_shadow_map_index_offset; // Sample the depth to put an upper bound on the length of the ray (as we // shouldn't trace through solid objects). If this is multisample, just use From c84d702bc02e89ce5fe01cdd0c9f9d9d22dd97f0 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 18:01:07 -0400 Subject: [PATCH 22/29] wip: fix wgsl indexing of point and spot shadow maps --- crates/bevy_pbr/src/render/pbr_functions.wgsl | 27 +++++++++--- crates/bevy_pbr/src/render/shadows.wgsl | 11 +++-- .../src/volumetric_fog/volumetric_fog.wgsl | 41 +++++++++++++++---- crates/bevy_render/src/view/mod.rs | 23 ++++++++++- crates/bevy_render/src/view/view.wgsl | 7 +++- 5 files changed, 89 insertions(+), 20 deletions(-) diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 76a20fb3db844..5ba0407e512e7 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -465,7 +465,8 @@ fn apply_pbr_lighting( view_bindings::view.view_from_world[2].z, view_bindings::view.view_from_world[3].z ), in.world_position); - let point_spot_shadow_map_offset = view_bindings::view.point_spot_shadow_map_index_offset; + let point_spot_shadow_map_mult_offset = view_bindings::view.point_spot_shadow_map_index_mult_offset; + let point_spot_shadow_map_add_offset = view_bindings::view.point_spot_shadow_map_index_add_offset; let cluster_index = clustering::view_fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); var clusterable_object_index_ranges = clustering::unpack_clusterable_object_index_ranges(cluster_index); @@ -494,7 +495,14 @@ fn apply_pbr_lighting( var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = shadows::fetch_point_shadow(light_id, point_spot_shadow_map_offset, in.world_position, in.world_normal, in.frag_coord.xy); + shadow = shadows::fetch_point_shadow( + light_id, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, + in.world_position, + in.world_normal, + in.frag_coord.xy + ); } #ifdef CONTACT_SHADOWS @@ -524,7 +532,14 @@ fn apply_pbr_lighting( var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - transmitted_shadow = shadows::fetch_point_shadow(light_id, point_spot_shadow_map_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, in.frag_coord.xy); + transmitted_shadow = shadows::fetch_point_shadow( + light_id, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + in.frag_coord.xy + ); } let transmitted_light_contrib = @@ -555,7 +570,8 @@ fn apply_pbr_lighting( mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = shadows::fetch_spot_shadow( light_id, - point_spot_shadow_map_offset, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, in.world_position, in.world_normal, view_bindings::clustered_lights.data[light_id].shadow_map_near_z, @@ -592,7 +608,8 @@ fn apply_pbr_lighting( && (view_bindings::clustered_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { transmitted_shadow = shadows::fetch_spot_shadow( light_id, - point_spot_shadow_map_offset, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, diffuse_transmissive_lobe_world_position, -in.world_normal, view_bindings::clustered_lights.data[light_id].shadow_map_near_z, diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 91a2e9aa19b48..4203dfc386ecf 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -18,7 +18,8 @@ const flip_z: vec3 = vec3(1.0, 1.0, -1.0); fn fetch_point_shadow( light_id: u32, - point_shadow_map_offset: u32, + point_shadow_map_mult_offset: u32, + point_shadow_map_add_offset: u32, frag_position: vec4, surface_normal: vec3, frag_coord_xy: vec2, @@ -53,7 +54,7 @@ fn fetch_point_shadow( // If soft shadows are enabled, use the PCSS path. Cubemaps assume a // left-handed coordinate space, so we have to flip the z-axis when // sampling. - let array_index = light_id + point_shadow_map_offset; + let array_index = light_id * point_shadow_map_mult_offset + point_shadow_map_add_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_cubemap_pcss( frag_ls * flip_z, @@ -72,7 +73,8 @@ fn fetch_point_shadow( fn fetch_spot_shadow( light_id: u32, - view_specific_spot_shadow_map_offset: u32, + view_specific_spot_shadow_map_mult_offset: u32, + view_specific_spot_shadow_map_add_offset: u32, frag_position: vec4, surface_normal: vec3, near_z: f32, @@ -115,7 +117,8 @@ fn fetch_spot_shadow( let depth = near_z / -projected_position.z; // If soft shadows are enabled, use the PCSS path. - let array_index = i32(light_id) + i32(view_specific_spot_shadow_map_offset) + view_bindings::lights.spot_light_shadowmap_offset; + let array_index = i32(light_id * view_specific_spot_shadow_map_mult_offset) + i32(view_specific_spot_shadow_map_add_offset) + + view_bindings::lights.spot_light_shadowmap_offset; if ((*light).soft_shadow_size > 0.0) { return sample_shadow_map_pcss( shadow_uv, diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 00bc09e478b75..928daf15d07b4 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -129,7 +129,8 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the view. let exposure = view.exposure; - let point_spot_shadow_map_offset = view.point_spot_shadow_map_index_offset; + let point_spot_shadow_map_mult_offset = view.point_spot_shadow_map_index_mult_offset; + let point_spot_shadow_map_add_offset = view.point_spot_shadow_map_index_add_offset; // Sample the depth to put an upper bound on the length of the ray (as we // shouldn't trace through solid objects). If this is multisample, just use @@ -393,7 +394,12 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_point_shadow_without_normal(light_id, point_spot_shadow_map_offset, vec4(P_world, 1.0), position.xy); + shadow = fetch_point_shadow_without_normal(light_id, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, + vec4(P_world, 1.0), + position.xy + ); } local_light_attenuation *= shadow; } else { @@ -414,7 +420,12 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { var shadow: f32 = 1.0; if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_spot_shadow_without_normal(light_id, point_spot_shadow_map_offset, vec4(P_world, 1.0), position.xy); + shadow = fetch_spot_shadow_without_normal(light_id, + point_spot_shadow_map_mult_offset, + point_spot_shadow_map_add_offset, + vec4(P_world, 1.0), + position.xy + ); } local_light_attenuation *= spot_attenuation * shadow; } @@ -445,7 +456,14 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { return vec4(accumulated_color, 1.0 - background_alpha); } -fn fetch_point_shadow_without_normal(light_id: u32, point_shadow_map_offset: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { +fn fetch_point_shadow_without_normal( + light_id: u32, + point_shadow_map_mult_offset: u32, + point_shadow_map_add_offset: u32, + frag_position: vec4, + frag_coord_xy: vec2 +) -> f32 { + let light = &clustered_lights.data[light_id]; // because the shadow maps align with the axes and the frustum planes are at 45 degrees @@ -475,11 +493,17 @@ fn fetch_point_shadow_without_normal(light_id: u32, point_shadow_map_offset: u32 // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, // so we have to flip the z-axis when sampling. let flip_z = vec3(1.0, 1.0, -1.0); - let array_index = light_id + point_shadow_map_offset; + let array_index = light_id * point_shadow_map_mult_offset + point_shadow_map_add_offset; return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, array_index, frag_coord_xy); } -fn fetch_spot_shadow_without_normal(light_id: u32, view_specific_spot_shadow_map_offset: u32, frag_position: vec4, frag_coord_xy: vec2) -> f32 { +fn fetch_spot_shadow_without_normal( + light_id: u32, + view_specific_spot_shadow_map_mult_offset: u32, + view_specific_spot_shadow_map_add_offset: u32, + frag_position: vec4, + frag_coord_xy: vec2 +) -> f32 { let light = &clustered_lights.data[light_id]; let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; @@ -514,11 +538,12 @@ fn fetch_spot_shadow_without_normal(light_id: u32, view_specific_spot_shadow_map // 0.1 must match POINT_LIGHT_NEAR_Z let depth = 0.1 / -projected_position.z; - + let array_index = i32(light_id * view_specific_spot_shadow_map_mult_offset) + i32(view_specific_spot_shadow_map_add_offset) + + lights.spot_light_shadowmap_offset; return sample_shadow_map( shadow_uv, depth, - i32(light_id) + i32(view_specific_spot_shadow_map_offset) + lights.spot_light_shadowmap_offset, + array_index, frag_coord_xy, SPOT_SHADOW_TEXEL_SIZE ); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 9816708bb7449..5ef630cd53132 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -673,7 +673,15 @@ pub struct ViewUniform { pub frame_count: u32, /// This offset is used to fetch the correct point or spot shadow map to use for this view. /// This is used to accommodates views that may be configured to have their own point or spot shadow maps. - pub point_spot_shadow_map_index_offset: u32, + /// This represents the total number of shadow maps (not counting shadow maps of different face indices) + /// that have been generated to be utilized by all possible views. + /// This must be multiplied to the initial index. + pub point_spot_shadow_map_index_mult_offset: u32, + /// This offset is used to fetch the correct point or spot shadow map to use for this view. + /// This is used to accommodates views that may be configured to have their own point or spot shadow maps. + /// This represents the additive index of the point/spot shadow map that this view should use. + /// This must be added to the index after the multiplicative offset has been applied. + pub point_spot_shadow_map_index_add_offset: u32, } #[derive(Resource)] @@ -1038,6 +1046,14 @@ pub fn prepare_view_uniforms( .enumerate() .map(|(index, entity)| (entity, index)) .collect(); + let num_view_agnostic_shadow_map = if views + .iter() + .any(|(_, camera, _, _, _, _, _)| camera.is_some_and(|camera| !camera.has_own_shadow_maps)) + { + 1 + } else { + 0 + }; let Some(mut writer) = view_uniforms .uniforms @@ -1143,7 +1159,10 @@ pub fn prepare_view_uniforms( color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, - point_spot_shadow_map_index_offset: *own_shadow_map_view_to_index + point_spot_shadow_map_index_mult_offset: (own_shadow_map_view_to_index.len() + + num_view_agnostic_shadow_map) + as u32, + point_spot_shadow_map_index_add_offset: *own_shadow_map_view_to_index .get(&entity) // Refer to the view agnostic point/spot shadow map, // which is after all of the view-specific ones. diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 2c966e79aa9eb..f2f07fb863f80 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -72,7 +72,12 @@ struct View { frame_count: u32, // This offset is used to fetch the correct point shadow map to use for this view. // This is used to accommodates views that may be configured to have their own point shadow maps. - point_spot_shadow_map_index_offset: u32, + // This offset must be applied to the index first. + point_spot_shadow_map_index_mult_offset: u32, + // This offset is used to fetch the correct point shadow map to use for this view. + // This is used to accommodates views that may be configured to have their own point shadow maps. + // This offset must be added after the index has been multiplied by the multiplicative offset. + point_spot_shadow_map_index_add_offset: u32, }; /// World space: From 40c1ba52e727827955004aaf2c455738dd20a054 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 19:53:55 -0400 Subject: [PATCH 23/29] fix removal bug in light.rs --- crates/bevy_pbr/src/render/light.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 054da01db620d..038face5fb8e8 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1611,7 +1611,7 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && views_count - auxiliary_entities.len() == 0 + && auxiliary_entities[auxiliary_entities.len() - 1] != None { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); @@ -1640,7 +1640,6 @@ pub fn prepare_lights( view_translation, cube_face_projection, ), - // The non view specific shadow map is at the end of the auxiliary_entities vec. (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), gpu_preprocessing_support.max_supported_mode, ); @@ -1817,7 +1816,7 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && views_count - auxiliary_entities.len() == 0 + && auxiliary_entities[auxiliary_entities.len() - 1] != None { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); From ad3d74bddb0b3a1bef371b1b43c1bbc7481e36a4 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 20:43:02 -0400 Subject: [PATCH 24/29] ensure limits to shadow maps are respected --- crates/bevy_pbr/src/render/light.rs | 83 +++++++++++++++++++---------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 038face5fb8e8..b50e2b39b001c 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1089,7 +1089,7 @@ pub fn prepare_lights( ) { let views_iter = views.iter(); let views_count = views_iter.len(); - let mut auxiliary_entities: Vec> = sorted_cameras + let mut point_spot_shadow_aux_entities: Vec> = sorted_cameras .0 .iter() .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) @@ -1100,10 +1100,10 @@ pub fn prepare_lights( Some((entity, MainEntity::from(main_entity))) }) .collect(); - if views_count - auxiliary_entities.len() > 0 { + if views_count - point_spot_shadow_aux_entities.len() > 0 { // There exist views that necessitate creating the shared shadow map. // The shared shadow map has an auxiliary entity of None. - auxiliary_entities.push(None); + point_spot_shadow_aux_entities.push(None); } let Some(mut view_gpu_lights_writer) = @@ -1195,11 +1195,12 @@ pub fn prepare_lights( .count() .min(max_texture_cubes); - let point_light_shadow_maps_count = point_lights + let point_light_shadow_maps_count = (point_lights .iter() .filter(|light| light.2.shadow_maps_enabled && light.2.spot_light_angles.is_none()) .count() - .min(max_texture_cubes); + * point_spot_shadow_aux_entities.len()) + .min(max_texture_cubes); let directional_volumetric_enabled_count = directional_lights .iter() @@ -1227,13 +1228,14 @@ pub fn prepare_lights( .count() .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); - let spot_light_shadow_maps_count = point_lights + let spot_light_shadow_maps_count = (point_lights .iter() .filter(|(_, _, light, _, _)| { light.shadow_maps_enabled && light.spot_light_angles.is_some() }) .count() - .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + * point_spot_shadow_aux_entities.len()) + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); // Sort lights by // - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader, @@ -1275,9 +1277,12 @@ pub fn prepare_lights( // Lights are sorted, shadow enabled lights are first if light.shadow_maps_enabled - && (index < point_light_shadow_maps_count + // Ensure that there are enough shadow maps available to accommodate all of the potential shadow maps + // that need to be created for a given index. + // The check is for "index + 1" since enumerate() is 0-indexed + && ((index + 1) * point_spot_shadow_aux_entities.len() <= point_light_shadow_maps_count || (light.spot_light_angles.is_some() - && index - point_light_count < spot_light_shadow_maps_count)) + && (index + 1 - point_light_count) * point_spot_shadow_aux_entities.len() <= spot_light_shadow_maps_count)) { flags |= PointLightFlags::SHADOW_MAPS_ENABLED; } @@ -1428,7 +1433,7 @@ pub fn prepare_lights( width: point_light_shadow_map.size as u32, height: point_light_shadow_map.size as u32, depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 - * auxiliary_entities.len() as u32 + * point_spot_shadow_aux_entities.len() as u32 * 6, }, mip_level_count: 1, @@ -1480,7 +1485,7 @@ pub fn prepare_lights( height: (directional_light_shadow_map.size as u32) .min(render_device.limits().max_texture_dimension_2d), depth_or_array_layers: (num_directional_cascades_enabled - + spot_light_shadow_maps_count * auxiliary_entities.len()) + + spot_light_shadow_maps_count * point_spot_shadow_aux_entities.len()) .max(1) as u32, }, mip_level_count: 1, @@ -1567,7 +1572,9 @@ pub fn prepare_lights( let init = point_and_spot_light_view_entities.0.is_empty() && point_and_spot_light_view_entities.1.is_empty(); if init { - for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { create_point_shadow_maps( &mut commands, &mut point_and_spot_light_view_entities, @@ -1581,7 +1588,11 @@ pub fn prepare_lights( view_translation, cube_face_projection, ), - (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), gpu_preprocessing_support.max_supported_mode, ); } @@ -1591,7 +1602,7 @@ pub fn prepare_lights( for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter_mut() { - if !auxiliary_entities + if !point_spot_shadow_aux_entities .iter() .any(|aux_entity| aux_entity.is_some_and(|(entity, _)| entity == *view_entity)) { @@ -1611,7 +1622,7 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && auxiliary_entities[auxiliary_entities.len() - 1] != None + && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] != None { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); @@ -1620,7 +1631,9 @@ pub fn prepare_lights( } // Add any shadow maps that are missing - for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { let insert_shadow_map = match auxiliary_entity { Some((entity, _)) => point_and_spot_light_view_entities.1.get(entity).is_none(), None => point_and_spot_light_view_entities.0.is_empty(), @@ -1640,7 +1653,11 @@ pub fn prepare_lights( view_translation, cube_face_projection, ), - (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), gpu_preprocessing_support.max_supported_mode, ); } @@ -1655,7 +1672,7 @@ pub fn prepare_lights( 1.0, light.shadow_map_near_z, ); - for auxiliary_entity in auxiliary_entities.iter() { + for auxiliary_entity in point_spot_shadow_aux_entities.iter() { let light_view_entities = if let Some((entity, _)) = auxiliary_entity { let entry = point_and_spot_light_view_entities.1.get(entity); if let Some(entities) = entry { @@ -1707,7 +1724,7 @@ pub fn prepare_lights( // Initialize the shadow render phases. We have to do this even if we've // already created the views in order to clear out old data. - for auxiliary_entity in auxiliary_entities.iter() { + for auxiliary_entity in point_spot_shadow_aux_entities.iter() { for face_index in 0..6 { let retained_view_entity = RetainedViewEntity::new( *light_main_entity, @@ -1771,7 +1788,9 @@ pub fn prepare_lights( && point_and_spot_light_view_entities.1.is_empty(); if init { - for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { create_spot_shadow_map( &mut commands, &mut point_and_spot_light_view_entities, @@ -1786,7 +1805,11 @@ pub fn prepare_lights( spot_projection, spot_light_frustum, ), - (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), gpu_preprocessing_support.max_supported_mode, ); } @@ -1796,7 +1819,7 @@ pub fn prepare_lights( for (view_entity, light_view_entities) in point_and_spot_light_view_entities.1.iter_mut() { - if !auxiliary_entities + if !point_spot_shadow_aux_entities .iter() .any(|aux_entity| aux_entity.is_some_and(|(entity, _)| entity == *view_entity)) { @@ -1816,7 +1839,7 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && auxiliary_entities[auxiliary_entities.len() - 1] != None + && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] != None { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); @@ -1825,7 +1848,9 @@ pub fn prepare_lights( } // Add any shadow maps that are missing - for (aux_entity_index, auxiliary_entity) in auxiliary_entities.iter().enumerate() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { let insert_shadow_map = match auxiliary_entity { Some((entity, _)) => point_and_spot_light_view_entities.1.get(entity).is_none(), None => point_and_spot_light_view_entities.0.is_empty(), @@ -1846,7 +1871,11 @@ pub fn prepare_lights( spot_projection, spot_light_frustum, ), - (auxiliary_entities.len(), auxiliary_entity, aux_entity_index), + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), gpu_preprocessing_support.max_supported_mode, ); } @@ -1862,7 +1891,7 @@ pub fn prepare_lights( [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); - for auxiliary_entity in auxiliary_entities.iter() { + for auxiliary_entity in point_spot_shadow_aux_entities.iter() { // There should be only one `view_light_entity` for spotlights. let view_light_entity = if let Some((_, main_entity)) = auxiliary_entity { let entry = point_and_spot_light_view_entities @@ -1907,7 +1936,7 @@ pub fn prepare_lights( } } - for auxiliary_entity in auxiliary_entities.iter() { + for auxiliary_entity in point_spot_shadow_aux_entities.iter() { let retained_view_entity = RetainedViewEntity::new( *light_main_entity, auxiliary_entity.map(|(_, main_entity)| main_entity), From 57090ff2580a9a1c81dfdbe3f74479f3b0c11e20 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 20:45:25 -0400 Subject: [PATCH 25/29] clippy --- crates/bevy_pbr/src/render/light.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b50e2b39b001c..b30815163e922 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1622,7 +1622,8 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] != None + && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] + .is_some() { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); @@ -1839,7 +1840,8 @@ pub fn prepare_lights( // Remove the non view specific shadow map if it is no longer needed if !point_and_spot_light_view_entities.0.is_empty() - && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] != None + && point_spot_shadow_aux_entities[point_spot_shadow_aux_entities.len() - 1] + .is_some() { for view_light_entity in point_and_spot_light_view_entities.0.iter() { commands.entity(*view_light_entity).despawn(); From 793914f9263e818a3e62b5e7330ef2f65667a10c Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 14 Jun 2026 22:00:26 -0400 Subject: [PATCH 26/29] readjust depth_or_array_layers --- crates/bevy_pbr/src/render/light.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b30815163e922..4f573040a8cfd 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1432,9 +1432,7 @@ pub fn prepare_lights( size: Extent3d { width: point_light_shadow_map.size as u32, height: point_light_shadow_map.size as u32, - depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 - * point_spot_shadow_aux_entities.len() as u32 - * 6, + depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, }, mip_level_count: 1, sample_count: 1, @@ -1485,8 +1483,8 @@ pub fn prepare_lights( height: (directional_light_shadow_map.size as u32) .min(render_device.limits().max_texture_dimension_2d), depth_or_array_layers: (num_directional_cascades_enabled - + spot_light_shadow_maps_count * point_spot_shadow_aux_entities.len()) - .max(1) as u32, + + spot_light_shadow_maps_count) + .max(1) as u32, }, mip_level_count: 1, sample_count: 1, From cf1ab1196d3620906223bb38b3432c120be78a00 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Mon, 15 Jun 2026 16:25:52 -0400 Subject: [PATCH 27/29] fix usage of depth attachment hash maps --- crates/bevy_pbr/src/render/light.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 4f573040a8cfd..4c29ce93ba4ed 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1421,10 +1421,8 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); - let mut point_light_depth_attachments = - HashMap::<(u32, Option), DepthAttachment>::default(); - let mut directional_light_depth_attachments = - HashMap::<(u32, Option), DepthAttachment>::default(); + let mut point_light_depth_attachments = HashMap::::default(); + let mut directional_light_depth_attachments = HashMap::::default(); let point_light_depth_texture = texture_cache.get( &render_device, @@ -2367,7 +2365,7 @@ pub fn prepare_lights( fn create_point_shadow_maps( commands: &mut Commands, point_and_spot_light_view_entities: &mut Mut, - point_light_depth_attachments: &mut HashMap<(u32, Option), DepthAttachment>, + point_light_depth_attachments: &mut HashMap, views: Query< ( Entity, @@ -2411,12 +2409,8 @@ fn create_point_shadow_maps( let base_array_layer = (light_index * auxiliary_entities_size * 6 + aux_entity_index * 6 + face_index) as u32; - let depth_attachment_key = ( - (light_index + face_index) as u32, - auxiliary_entity.map(|(entity, _)| entity), - ); let depth_attachment = point_light_depth_attachments - .entry(depth_attachment_key) + .entry(base_array_layer) .or_insert_with(|| { let depth_texture_view = point_light_depth_texture @@ -2516,7 +2510,7 @@ fn create_point_shadow_maps( fn create_spot_shadow_map( commands: &mut Commands, point_and_spot_light_view_entities: &mut Mut<'_, PointAndSpotLightViewEntities>, - directional_light_depth_attachments: &mut HashMap<(u32, Option), DepthAttachment>, + directional_light_depth_attachments: &mut HashMap, views: Query< ( Entity, @@ -2546,12 +2540,8 @@ fn create_spot_shadow_map( let base_array_layer = (num_directional_cascades_enabled + light_index * auxiliary_entities_size + aux_entity_index) as u32; - let depth_attachment_key = ( - (num_directional_cascades_enabled + light_index) as u32, - auxiliary_entity.map(|(entity, _)| entity), - ); let depth_attachment = directional_light_depth_attachments - .entry(depth_attachment_key) + .entry(base_array_layer) .or_insert_with(|| { let depth_texture_view = directional_light_depth_texture From 881db76ef5a60cb281f38f4a841ff5cb540ea062 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Wed, 17 Jun 2026 10:02:14 -0400 Subject: [PATCH 28/29] fix clippy --- crates/bevy_pbr/src/render/light.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 71c3a9b26519d..eb2d77dc90c60 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1675,7 +1675,7 @@ pub fn prepare_lights( continue; } } else { - (&point_and_spot_light_view_entities.0).to_vec() + point_and_spot_light_view_entities.0.to_vec() }; create_point_shadow_maps( &mut commands, From 97030562def0bed275d65438b5c564ae5bc0db34 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Wed, 17 Jun 2026 10:59:29 -0400 Subject: [PATCH 29/29] remove unnecessary newline --- crates/bevy_pbr/src/render/light.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index eb2d77dc90c60..3b47ccfc6cdcb 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -2835,7 +2835,6 @@ pub(crate) fn specialize_shadows( { continue; } - let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else { continue; };