diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 38ecb39a016cb..7a31a67646c16 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 in order for this to work properly. + /// + /// 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`. + 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_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 33617abac86dc..3b2ed8ca32633 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, SpotLightShadowViewEntity, +}; use super::{ShadowView, ViewLightEntities}; @@ -524,7 +527,14 @@ 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>, + Option<&SpotLightShadowViewEntity>, + ), + Without, + >, view_query: Query<&ExtractedView, Without>, light_query: Query<&LightEntity>, batched_instance_buffers: Res>, @@ -546,9 +556,15 @@ 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, 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, + ); // 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 +616,14 @@ pub fn unpack_bins( } pub fn early_gpu_preprocess( - current_view: ViewQuery, Without>, + current_view: ViewQuery< + ( + Option<&ViewLightEntities>, + Option<&PointLightShadowViewEntities>, + Option<&SpotLightShadowViewEntity>, + ), + Without, + >, view_query: Query< ( &ExtractedView, @@ -630,9 +653,15 @@ 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, 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, + ); // Run the compute passes. for view_entity in all_views { @@ -795,6 +824,8 @@ 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>, + spot_light_shadow_view: Option<&SpotLightShadowViewEntity>, light_query: &Query<&LightEntity>, ) -> SmallVec<[Entity; 8]> { let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); @@ -812,6 +843,26 @@ 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(), + ); + } + 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 } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 0188614a17623..3b47ccfc6cdcb 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,26 @@ pub struct ViewLightEntities { pub lights: Vec, } +/// A component that holds the shadow views generated by a pointlight +/// 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 + /// 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. This is placed on the +/// camera entity. +#[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 +1031,7 @@ pub fn prepare_lights( Option<&RenderLayers>, Has, Option<&AmbientLight>, + Option<&ExtractedCamera>, ), With, >, @@ -1060,6 +1089,23 @@ pub fn prepare_lights( ) { let views_iter = views.iter(); let views_count = views_iter.len(); + let mut point_spot_shadow_aux_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 - 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. + point_spot_shadow_aux_entities.push(None); + } + let Some(mut view_gpu_lights_writer) = light_meta .view_gpu_lights @@ -1149,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() @@ -1181,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, @@ -1229,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; } @@ -1339,6 +1390,7 @@ pub fn prepare_lights( maybe_layers, _no_indirect_drawing, _maybe_ambient_override, + _extracted_camera, ) in sorted_cameras .0 .iter() @@ -1466,6 +1518,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() @@ -1485,57 +1538,184 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); + 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(light_view_entities)); + } + mem::take(&mut point_and_spot_light_view_entities.1); continue; } - if point_and_spot_light_view_entities.0.is_empty() { - // 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 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 + point_spot_shadow_aux_entities.iter().enumerate() + { + // For each face of a cube we spawn a light entity + let light_view_entities: Vec<_> = + (0..6).map(|_| commands.spawn_empty().id()).collect(); + + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + &global_clusterable_object_meta, + views, + ( + &cube_face_rotations, + point_light_frusta, + light_view_entities, + ), + &point_light_depth_texture, + (light_entity, light_main_entity, light), + point_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } + } 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 !point_spot_shadow_aux_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::(); + light_view_entities.clear(); + to_remove.push(*view_entity); + } + } + to_remove.iter().for_each(|entity| { + point_and_spot_light_view_entities.1.remove(entity); + }); - create_point_shadow_maps( - &mut commands, - &mut point_light_depth_attachments, - &global_clusterable_object_meta, - ( - &cube_face_rotations, - point_light_frusta, - &light_view_entities, - ), - &point_light_depth_texture, - (light_entity, light_main_entity, light), - point_light_shadow_map.size as u32, - gpu_preprocessing_support.max_supported_mode, - ); + // 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] + .is_some() + { + 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(); + } - point_and_spot_light_view_entities.0 = light_view_entities; - } else if changed_point_lights.get(*light_entity).is_ok() { - // Update the point shadow maps with the changes. - create_point_shadow_maps( - &mut commands, - &mut point_light_depth_attachments, - &global_clusterable_object_meta, - ( - &cube_face_rotations, - point_light_frusta, - &point_and_spot_light_view_entities.0, - ), - &point_light_depth_texture, - (light_entity, light_main_entity, light), - point_light_shadow_map.size as u32, - gpu_preprocessing_support.max_supported_mode, - ); + // Add any shadow maps that are missing + 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(), + }; + + if insert_shadow_map { + let light_view_entities: Vec<_> = + (0..6).map(|_| commands.spawn_empty().id()).collect(); + + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + &global_clusterable_object_meta, + views, + ( + &cube_face_rotations, + point_light_frusta, + light_view_entities, + ), + &point_light_depth_texture, + (light_entity, light_main_entity, light), + point_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + 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() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { + let light_view_entities = if let Some((_, main_entity)) = auxiliary_entity { + let entry = point_and_spot_light_view_entities + .1 + .get(&main_entity.entity()); + if let Some(view_light_entities) = entry { + view_light_entities.to_vec() + } 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 already inserted with the updated data anyway. + continue; + } + } else { + point_and_spot_light_view_entities.0.to_vec() + }; + create_point_shadow_maps( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut point_light_depth_attachments, + &global_clusterable_object_meta, + views, + ( + &cube_face_rotations, + point_light_frusta, + light_view_entities, + ), + &point_light_depth_texture, + (light_entity, light_main_entity, light), + point_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } } // 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 point_spot_shadow_aux_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); + } } } @@ -1559,49 +1739,170 @@ pub fn prepare_lights( &mut commands, mem::take(&mut point_and_spot_light_view_entities.0), ); + 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(light_view_entities)); + } + mem::take(&mut point_and_spot_light_view_entities.1); 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 init = point_and_spot_light_view_entities.0.is_empty() + && point_and_spot_light_view_entities.1.is_empty(); - if point_and_spot_light_view_entities.0.is_empty() { - let view_light_entity = commands.spawn_empty().id(); + if init { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { + let view_light_entity = commands.spawn_empty().id(); + + create_spot_shadow_map( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut directional_light_depth_attachments, + (num_directional_cascades_enabled, light_index), + views, + &directional_light_depth_texture, + view_light_entity, + (light_entity, light_main_entity, light), + spot_light_frustum, + directional_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } + } 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 !point_spot_shadow_aux_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::(); + light_view_entities.clear(); + to_remove.push(*view_entity); + } + } + to_remove.iter().for_each(|entity| { + point_and_spot_light_view_entities.1.remove(entity); + }); - create_spot_shadow_map( - &mut commands, - &mut directional_light_depth_attachments, - (num_directional_cascades_enabled, light_index), - &directional_light_depth_texture, - view_light_entity, - (light_entity, light_main_entity, light), - spot_light_frustum, - directional_light_shadow_map.size as u32, - gpu_preprocessing_support.max_supported_mode, - ); + // 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] + .is_some() + { + 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(); + } - point_and_spot_light_view_entities.0 = vec![view_light_entity]; - } else if changed_point_lights.get(*light_entity).is_ok() { - create_spot_shadow_map( - &mut commands, - &mut directional_light_depth_attachments, - (num_directional_cascades_enabled, light_index), - &directional_light_depth_texture, - // There should only be one view light entity for spotlights - point_and_spot_light_view_entities.0[0], - (light_entity, light_main_entity, light), - spot_light_frustum, - directional_light_shadow_map.size as u32, + // Add any shadow maps that are missing + 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(), + }; + + if insert_shadow_map { + let view_light_entity = commands.spawn_empty().id(); + + create_spot_shadow_map( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut directional_light_depth_attachments, + (num_directional_cascades_enabled, light_index), + views, + &directional_light_depth_texture, + view_light_entity, + (light_entity, light_main_entity, light), + spot_light_frustum, + directional_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } + } + } + + if !init && changed_point_lights.get(*light_entity).is_ok() { + for (aux_entity_index, auxiliary_entity) in + point_spot_shadow_aux_entities.iter().enumerate() + { + // 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 { + // 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 { + point_and_spot_light_view_entities.0[0] + }; + create_spot_shadow_map( + &mut commands, + &mut point_and_spot_light_view_entities, + &mut directional_light_depth_attachments, + (num_directional_cascades_enabled, light_index), + views, + &directional_light_depth_texture, + view_light_entity, + (light_entity, light_main_entity, light), + spot_light_frustum, + directional_light_shadow_map.size as u32, + ( + point_spot_shadow_aux_entities.len(), + auxiliary_entity, + aux_entity_index, + ), + gpu_preprocessing_support.max_supported_mode, + ); + } + } + + 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), + 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); } - - 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 @@ -1613,6 +1914,7 @@ pub fn prepare_lights( maybe_layers, no_indirect_drawing, maybe_ambient_override, + _extracted_camera, ) in sorted_cameras .0 .iter() @@ -2009,21 +2311,45 @@ pub fn prepare_lights( .retain(|entity, _| live_shadow_mapping_lights.contains(entity)); } -/// Creates six point shadow maps for a `RetainedViewEntity` identified by the `light_main_entity` -/// and the six cube face rotation indices. These shadow maps are shared across -/// all cameras. +/// 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. +/// 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, global_clusterable_object_meta: &ResMut, + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + Option<&ExtractedCamera>, + ), + With, + >, (cube_face_rotations, point_light_frusta, light_view_entities): ( &Vec, Option<&CubemapFrusta>, - &Vec, + Vec, ), point_light_depth_texture: &CachedTexture, (light_entity, light_main_entity, light): (&Entity, &MainEntity, &ExtractedPointLight), point_light_shadow_map_size: u32, + (auxiliary_entities_size, auxiliary_entity, aux_entity_index): ( + usize, + &Option<(Entity, MainEntity)>, + usize, + ), gpu_preprocessing_support_max_supported_mode: GpuPreprocessingMode, ) { let light_index = *global_clusterable_object_meta @@ -2040,15 +2366,20 @@ fn create_point_shadow_maps( 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; + 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 = point_light_depth_attachments .entry(base_array_layer) .or_insert_with(|| { @@ -2066,23 +2397,18 @@ fn create_point_shadow_maps( base_array_layer, array_layer_count: Some(1u32), }); - DepthAttachment::new(depth_texture_view, Some(0.0)) }) .clone(); - // 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_{}_{}", + "shadow_point_light_{}_{}_aux_{:?}", light_index, - face_index_to_name(face_index) + face_index_to_name(face_index), + auxiliary_entity.map(|(_, main_entity)| main_entity), ), }, ExtractedView { @@ -2105,9 +2431,23 @@ fn create_point_shadow_maps( light_entity: *light_entity, face_index, }, - RootNonCameraView(Core3d.intern()), )); + 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 @@ -2115,19 +2455,57 @@ fn create_point_shadow_maps( commands.entity(view_light_entity).insert(NoIndirectDrawing); } } + + if let Some((entity, _)) = auxiliary_entity { + point_and_spot_light_view_entities + .1 + .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. + commands + .entity(*entity) + .insert(PointLightShadowViewEntities { + shadow_view_entities: light_view_entities, + }); + } else { + point_and_spot_light_view_entities.0 = light_view_entities; + } } -/// Creates the spot shadow map for a `RetainedViewEntity` identified by the `light_main_entity`. -/// This shadow map is shared across all cameras. +/// 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. +/// 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, directional_light_depth_attachments: &mut HashMap, (num_directional_cascades_enabled, light_index): (usize, usize), + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + Option<&ExtractedCamera>, + ), + With, + >, directional_light_depth_texture: &CachedTexture, view_light_entity: Entity, (light_entity, light_main_entity, light): (&Entity, &MainEntity, &ExtractedPointLight), spot_light_frustum: Option<&Frustum>, directional_light_shadow_map_size: u32, + (auxiliary_entities_size, auxiliary_entity, aux_entity_index): ( + usize, + &Option<(Entity, MainEntity)>, + usize, + ), gpu_preprocessing_support_max_supported_mode: GpuPreprocessingMode, ) { let spot_world_from_view = spot_light_world_from_view(&light.transform); @@ -2137,8 +2515,9 @@ fn create_spot_shadow_map( [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; - + 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 .entry(base_array_layer) .or_insert_with(|| { @@ -2161,11 +2540,20 @@ fn create_spot_shadow_map( }) .clone(); - let retained_view_entity = RetainedViewEntity::new(*light_main_entity, None, 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_{light_index}"), + pass_name: format!( + "shadow_spot_light_{}_aux_{:?}", + light_index, + auxiliary_entity.map(|(_, main_entity)| main_entity), + ), }, ExtractedView { retained_view_entity, @@ -2186,8 +2574,21 @@ fn create_spot_shadow_map( LightEntity::Spot { light_entity: *light_entity, }, - RootNonCameraView(Core3d.intern()), )); + 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, @@ -2195,6 +2596,20 @@ fn create_spot_shadow_map( ) { 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) { @@ -2795,31 +3210,71 @@ 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. +/// 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>, + view: ViewQuery<( + Option<&ViewLightEntities>, + Option<&PointLightShadowViewEntities>, + Option<&SpotLightShadowViewEntity>, + )>, 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, 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, + ); + } + } + } + + 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. diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index ae7703f883914..5ba0407e512e7 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -465,6 +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_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); @@ -493,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, 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 @@ -523,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, 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 = @@ -554,6 +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_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, @@ -590,6 +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_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/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index d4f0751e17e68..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_id: 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_id: view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_comparison_sampler, light_local, - i32(light_id), + i32(array_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, + 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_id), + i32(array_index), ); #endif @@ -386,12 +386,12 @@ fn sample_shadow_cubemap_at_offset( y_basis: vec3, light_local: vec3, depth: f32, - light_id: u32, + array_index: u32, ) -> f32 { return sample_shadow_cubemap_hardware( light_local + position.x * x_basis + position.y * y_basis, depth, - light_id + 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_id: 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_id + array_index ); } @@ -425,7 +425,7 @@ fn sample_shadow_cubemap_gaussian( depth: f32, scale: f32, distance_to_light: f32, - light_id: 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id: 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id: 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_id); + 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_id, 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_id); + 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_id: 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id); + 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_id: 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_id); + 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_id, 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_id, 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 d10a960c221b1..4203dfc386ecf 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -18,6 +18,8 @@ const flip_z: vec3 = vec3(1.0, 1.0, -1.0); fn fetch_point_shadow( light_id: u32, + point_shadow_map_mult_offset: u32, + point_shadow_map_add_offset: u32, frag_position: vec4, surface_normal: vec3, frag_coord_xy: vec2, @@ -52,12 +54,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 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, distance_to_light, depth, - light_id, + array_index, (*light).soft_shadow_size, frag_coord_xy, ); @@ -65,11 +68,13 @@ 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, array_index, frag_coord_xy); } fn fetch_spot_shadow( light_id: 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, @@ -112,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) + 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 a64f85d434682..928daf15d07b4 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -129,6 +129,8 @@ fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the view. let exposure = view.exposure; + 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 @@ -392,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, 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 { @@ -413,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, 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; } @@ -444,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, 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 @@ -474,10 +493,17 @@ 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 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, 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; @@ -512,11 +538,12 @@ fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4, fra // 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) + lights.spot_light_shadowmap_offset, + array_index, frag_coord_xy, SPOT_SHADOW_TEXEL_SIZE ); 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), diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index ddabb60e7d408..5ef630cd53132 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}; @@ -278,12 +280,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, @@ -666,6 +671,17 @@ pub struct ViewUniform { pub color_grading: ColorGradingUniform, pub mip_bias: f32, 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. + /// 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)] @@ -1011,9 +1027,33 @@ 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 + // 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() + .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 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 @@ -1119,6 +1159,15 @@ 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_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. + .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..f2f07fb863f80 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -70,6 +70,14 @@ 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. + // 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: 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),