diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 315563752a..9cc3fec765 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -4,7 +4,7 @@ use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::{EntityHashMap, EntityHashSet}, + entity::{EntityHash, EntityHashMap, EntityHashSet}, prelude::*, system::lifetimeless::Read, }; @@ -459,7 +459,9 @@ fn create_render_visible_mesh_entities( } #[derive(Component, Default, Deref, DerefMut)] -pub struct LightViewEntities(Vec); +/// Component automatically attached to a light entity to track light-view entities +/// for each view. +pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( @@ -477,9 +479,11 @@ pub(crate) fn remove_light_view_entities( mut commands: Commands, ) { if let Ok(entities) = query.get(trigger.entity()) { - for e in entities.0.iter().copied() { - if let Some(mut v) = commands.get_entity(e) { - v.despawn(); + for v in entities.0.values() { + for e in v.iter().copied() { + if let Some(mut v) = commands.get_entity(e) { + v.despawn(); + } } } } @@ -731,7 +735,8 @@ pub fn prepare_lights( let point_light_count = point_lights .iter() .filter(|light| light.1.spot_light_angles.is_none()) - .count(); + .count() + .min(max_texture_cubes); let point_light_volumetric_enabled_count = point_lights .iter() @@ -759,6 +764,12 @@ pub fn prepare_lights( .count() .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + let spot_light_count = point_lights + .iter() + .filter(|(_, light, _)| light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + let spot_light_volumetric_enabled_count = point_lights .iter() .filter(|(_, light, _)| light.volumetric && light.spot_light_angles.is_some()) @@ -956,9 +967,12 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); - let mut dir_light_view_offset = 0; + let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash); + // set up light data for each view - for (offset, (entity, extracted_view, clusters, maybe_layers)) in views.iter().enumerate() { + for (entity, extracted_view, clusters, maybe_layers) in views.iter() { + live_views.insert(entity); + let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -1032,9 +1046,19 @@ pub fn prepare_lights( for &(light_entity, light, (point_light_frusta, _)) in point_lights .iter() // Lights are sorted, shadow enabled lights are first - .take(point_light_shadow_maps_count) - .filter(|(_, light, _)| light.shadows_enabled) + .take(point_light_count) { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + let light_index = *global_light_meta .entity_to_index .get(&light_entity) @@ -1044,14 +1068,10 @@ 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()); - let Ok(mut light_entities) = light_view_entities.get_mut(light_entity) else { - continue; - }; - // for each face of a cube and each view we spawn a light entity - while light_entities.len() < 6 * (offset + 1) { - light_entities.push(commands.spawn_empty().id()); - } + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| (0..6).map(|_| commands.spawn_empty().id()).collect()); let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, @@ -1062,7 +1082,7 @@ pub fn prepare_lights( for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations .iter() .zip(&point_light_frusta.unwrap().frusta) - .zip(light_entities.iter().skip(6 * offset).copied()) + .zip(light_view_entities.iter().copied()) .enumerate() { let depth_texture_view = @@ -1119,16 +1139,23 @@ pub fn prepare_lights( for (light_index, &(light_entity, light, (_, spot_light_frustum))) in point_lights .iter() .skip(point_light_count) - .take(spot_light_shadow_maps_count) + .take(spot_light_count) .enumerate() { - let spot_world_from_view = spot_light_world_from_view(&light.transform); - let spot_world_from_view = spot_world_from_view.into(); - let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { continue; }; + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + 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); @@ -1147,11 +1174,11 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }); - while light_view_entities.len() < offset + 1 { - light_view_entities.push(commands.spawn_empty().id()); - } + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| vec![commands.spawn_empty().id()]); - let view_light_entity = light_view_entities[offset]; + let view_light_entity = light_view_entities[0]; commands.entity(view_light_entity).insert(( ShadowView { @@ -1194,14 +1221,21 @@ pub fn prepare_lights( let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { continue; }; + // Check if the light intersects with the view. if !view_layers.intersects(&light.render_layers) { gpu_light.skip = 1u32; + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } continue; } // Only deal with cascades when shadows are enabled. if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } continue; } @@ -1222,18 +1256,19 @@ pub fn prepare_lights( .zip(frusta) .zip(&light.cascade_shadow_config.bounds); - while light_view_entities.len() < dir_light_view_offset + iter.len() { - light_view_entities.push(commands.spawn_empty().id()); + let light_view_entities = light_view_entities.entry(entity).or_insert_with(|| { + (0..iter.len()) + .map(|_| commands.spawn_empty().id()) + .collect() + }); + if light_view_entities.len() != iter.len() { + let entities = core::mem::take(light_view_entities); + despawn_entities(&mut commands, entities); + light_view_entities.extend((0..iter.len()).map(|_| commands.spawn_empty().id())); } - for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in iter - .zip( - light_view_entities - .iter() - .skip(dir_light_view_offset) - .copied(), - ) - .enumerate() + for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in + iter.zip(light_view_entities.iter().copied()).enumerate() { gpu_lights.directional_lights[light_index].cascades[cascade_index] = GpuDirectionalCascade { @@ -1292,7 +1327,6 @@ pub fn prepare_lights( shadow_render_phases.insert_or_clear(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity); - dir_light_view_offset += 1; } } @@ -1360,9 +1394,29 @@ pub fn prepare_lights( )); } + // Despawn light-view entities for views that no longer exist + for mut entities in &mut light_view_entities { + for (_, light_view_entities) in + entities.extract_if(|entity, _| !live_views.contains(entity)) + { + despawn_entities(&mut commands, light_view_entities); + } + } + shadow_render_phases.retain(|entity, _| live_shadow_mapping_lights.contains(entity)); } +fn despawn_entities(commands: &mut Commands, entities: Vec) { + if entities.is_empty() { + return; + } + commands.queue(move |world: &mut World| { + for entity in entities { + world.despawn(entity); + } + }); +} + /// For each shadow cascade, iterates over all the meshes "visible" from it and /// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as /// appropriate.