diff --git a/Cargo.toml b/Cargo.toml index a371f316cb..307558e0bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2706,6 +2706,17 @@ description = "Test rendering of many UI elements" category = "Stress Tests" wasm = true +[[example]] +name = "many_cameras_lights" +path = "examples/stress_tests/many_cameras_lights.rs" +doc-scrape-examples = true + +[package.metadata.example.many_cameras_lights] +name = "Many Cameras & Lights" +description = "Test rendering of many cameras and lights" +category = "Stress Tests" +wasm = true + [[example]] name = "many_cubes" path = "examples/stress_tests/many_cubes.rs" diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 65d27d51ce..0b93c93c67 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1,6 +1,9 @@ use core::ops::DerefMut; -use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_ecs::{ + entity::{EntityHashMap, EntityHashSet}, + prelude::*, +}; use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ @@ -836,6 +839,7 @@ pub fn check_dir_light_mesh_visibility( }); } +#[allow(clippy::too_many_arguments)] pub fn check_point_light_mesh_visibility( visible_point_lights: Query<&VisibleClusterableObjects>, mut point_lights: Query<( @@ -872,10 +876,17 @@ pub fn check_point_light_mesh_visibility( visible_entity_ranges: Option>, mut cubemap_visible_entities_queue: Local; 6]>>, mut spot_visible_entities_queue: Local>>, + mut checked_lights: Local, ) { + checked_lights.clear(); + let visible_entity_ranges = visible_entity_ranges.as_deref(); for visible_lights in &visible_point_lights { for light_entity in visible_lights.entities.iter().copied() { + if !checked_lights.insert(light_entity) { + continue; + } + // Point lights if let Ok(( point_light, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index bd0248789d..c6ca3d0fb7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -9,6 +9,7 @@ use bevy_ecs::{ system::lifetimeless::Read, }; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_render::camera::SortedCameras; use bevy_render::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity}; use bevy_render::{ diagnostic::RecordDiagnostics, @@ -29,6 +30,7 @@ use bevy_utils::tracing::info_span; use bevy_utils::{ default, tracing::{error, warn}, + HashMap, }; use core::{hash::Hash, ops::Range}; @@ -677,10 +679,11 @@ pub fn prepare_lights( point_light_shadow_map: Res, directional_light_shadow_map: Res, mut shadow_render_phases: ResMut>, - (mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( - Local, - Local, - ), + ( + mut max_directional_lights_warning_emitted, + mut max_cascades_per_light_warning_emitted, + mut live_shadow_mapping_lights, + ): (Local, Local, Local), point_lights: Query<( Entity, &ExtractedPointLight, @@ -688,7 +691,7 @@ pub fn prepare_lights( )>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, mut light_view_entities: Query<&mut LightViewEntities>, - mut live_shadow_mapping_lights: Local, + sorted_cameras: Res, ) { let views_iter = views.iter(); let views_count = views_iter.len(); @@ -984,50 +987,108 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); + 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, + TextureDescriptor { + 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, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("point_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let point_light_depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_array_texture_view"), + format: None, + // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. + // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. + #[cfg(all( + not(feature = "ios_simulator"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + dimension: Some(TextureViewDimension::CubeArray), + #[cfg(any( + feature = "ios_simulator", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + dimension: Some(TextureViewDimension::Cube), + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let directional_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + 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, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("directional_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let directional_light_depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + dimension: Some(TextureViewDimension::D2Array), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash); // set up light data for each view - for (entity, extracted_view, clusters, maybe_layers) in views.iter() { + for (entity, extracted_view, clusters, maybe_layers) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { live_views.insert(entity); - - let point_light_depth_texture = texture_cache.get( - &render_device, - TextureDescriptor { - 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, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: CORE_3D_DEPTH_FORMAT, - label: Some("point_light_shadow_map_texture"), - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); - let directional_light_depth_texture = texture_cache.get( - &render_device, - TextureDescriptor { - size: Extent3d { - width: (directional_light_shadow_map.size as u32) - .min(render_device.limits().max_texture_dimension_2d), - 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, - }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: CORE_3D_DEPTH_FORMAT, - label: Some("directional_light_shadow_map_texture"), - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ); let mut view_lights = Vec::new(); let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; @@ -1102,23 +1163,35 @@ pub fn prepare_lights( .zip(light_view_entities.iter().copied()) .enumerate() { - 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), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: (light_index * 6 + face_index) as u32, - array_layer_count: Some(1u32), - }); + let mut first = false; + 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(|| { + first = true; + + 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), + 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: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!( "shadow pass point light {} {}", light_index, @@ -1147,8 +1220,11 @@ pub fn prepare_lights( view_lights.push(view_light_entity); - shadow_render_phases.insert_or_clear(view_light_entity); - live_shadow_mapping_lights.insert(view_light_entity); + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases.insert_or_clear(view_light_entity); + live_shadow_mapping_lights.insert(view_light_entity); + } } } @@ -1177,19 +1253,30 @@ 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 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), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: (num_directional_cascades_enabled + light_index) as u32, - array_layer_count: Some(1u32), - }); + let mut first = false; + 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(|| { + first = true; + + 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), + 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 light_view_entities = light_view_entities .entry(entity) @@ -1199,7 +1286,7 @@ pub fn prepare_lights( commands.entity(view_light_entity).insert(( ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!("shadow pass spot light {light_index}"), }, ExtractedView { @@ -1221,8 +1308,11 @@ pub fn prepare_lights( view_lights.push(view_light_entity); - shadow_render_phases.insert_or_clear(view_light_entity); - live_shadow_mapping_lights.insert(view_light_entity); + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases.insert_or_clear(view_light_entity); + live_shadow_mapping_lights.insert(view_light_entity); + } } // directional lights @@ -1307,6 +1397,12 @@ pub fn prepare_lights( base_array_layer: directional_depth_texture_array_index, array_layer_count: Some(1u32), }); + + // NOTE: For point and spotlights, we reuse the same depth attachment for all views. + // However, for directional lights, we want a new depth attachment for each view, + // so that the view is cleared for each view. + let depth_attachment = DepthAttachment::new(depth_texture_view, Some(0.0)); + directional_depth_texture_array_index += 1; let mut frustum = *frustum; @@ -1316,7 +1412,7 @@ pub fn prepare_lights( commands.entity(view_light_entity).insert(( ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + depth_attachment, pass_name: format!( "shadow pass directional light {light_index} cascade {cascade_index}" ), @@ -1342,65 +1438,19 @@ pub fn prepare_lights( )); view_lights.push(view_light_entity); + // Subsequent views with the same light entity will **NOT** reuse the same shadow map + // (Because the cascades are unique to each view) shadow_render_phases.insert_or_clear(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity); } } - let point_light_depth_texture_view = - point_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("point_light_shadow_map_array_texture_view"), - format: None, - // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. - // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. - #[cfg(all( - not(feature = "ios_simulator"), - any( - not(feature = "webgl"), - not(target_arch = "wasm32"), - feature = "webgpu" - ) - ))] - dimension: Some(TextureViewDimension::CubeArray), - #[cfg(any( - feature = "ios_simulator", - all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) - ))] - dimension: Some(TextureViewDimension::Cube), - aspect: TextureAspect::DepthOnly, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: None, - }); - let directional_light_depth_texture_view = directional_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("directional_light_shadow_map_array_texture_view"), - format: None, - #[cfg(any( - not(feature = "webgl"), - not(target_arch = "wasm32"), - feature = "webgpu" - ))] - dimension: Some(TextureViewDimension::D2Array), - #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::DepthOnly, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: None, - }); - commands.entity(entity).insert(( ViewShadowBindings { - point_light_depth_texture: point_light_depth_texture.texture, - point_light_depth_texture_view, - directional_light_depth_texture: directional_light_depth_texture.texture, - directional_light_depth_texture_view, + point_light_depth_texture: point_light_depth_texture.texture.clone(), + point_light_depth_texture_view: point_light_depth_texture_view.clone(), + directional_light_depth_texture: directional_light_depth_texture.texture.clone(), + directional_light_depth_texture_view: directional_light_depth_texture_view.clone(), }, ViewLightEntities { lights: view_lights, diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index e0947e9972..ac3854227f 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -80,6 +80,7 @@ impl ColorAttachment { } /// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. +#[derive(Clone)] pub struct DepthAttachment { pub view: TextureView, clear_value: Option, diff --git a/examples/README.md b/examples/README.md index 903cb82746..92bf3f188b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -459,6 +459,7 @@ Example | Description [Bevymark](../examples/stress_tests/bevymark.rs) | A heavy sprite rendering workload to benchmark your system with Bevy [Many Animated Sprites](../examples/stress_tests/many_animated_sprites.rs) | Displays many animated sprites in a grid arrangement with slight offsets to their animation timers. Used for performance testing. [Many Buttons](../examples/stress_tests/many_buttons.rs) | Test rendering of many UI elements +[Many Cameras & Lights](../examples/stress_tests/many_cameras_lights.rs) | Test rendering of many cameras and lights [Many Cubes](../examples/stress_tests/many_cubes.rs) | Simple benchmark to test per-entity draw overhead. Run with the `sphere` argument to test frustum culling [Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000 [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos diff --git a/examples/stress_tests/many_cameras_lights.rs b/examples/stress_tests/many_cameras_lights.rs new file mode 100644 index 0000000000..4d527faec3 --- /dev/null +++ b/examples/stress_tests/many_cameras_lights.rs @@ -0,0 +1,92 @@ +//! Test rendering of many cameras and lights + +use std::f32::consts::PI; + +use bevy::{ + math::ops::{cos, sin}, + prelude::*, + render::camera::Viewport, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, rotate_cameras) + .run(); +} + +const CAMERA_ROWS: usize = 4; +const CAMERA_COLS: usize = 4; +const NUM_LIGHTS: usize = 5; + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + window: Query<&Window>, +) { + // circular base + commands.spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_xyz(0.0, 0.5, 0.0), + )); + + // lights + for i in 0..NUM_LIGHTS { + let angle = (i as f32) / (NUM_LIGHTS as f32) * PI * 2.0; + commands.spawn(( + PointLight { + color: Color::hsv(angle.to_degrees(), 1.0, 1.0), + intensity: 2_000_000.0 / NUM_LIGHTS as f32, + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(sin(angle) * 4.0, 2.0, cos(angle) * 4.0), + )); + } + + // cameras + let window = window.single(); + let width = window.resolution.width() / CAMERA_COLS as f32 * window.resolution.scale_factor(); + let height = window.resolution.height() / CAMERA_ROWS as f32 * window.resolution.scale_factor(); + let mut i = 0; + for y in 0..CAMERA_COLS { + for x in 0..CAMERA_ROWS { + let angle = i as f32 / (CAMERA_ROWS * CAMERA_COLS) as f32 * PI * 2.0; + commands.spawn(( + Camera3d::default(), + Camera { + viewport: Some(Viewport { + physical_position: UVec2::new( + (x as f32 * width) as u32, + (y as f32 * height) as u32, + ), + physical_size: UVec2::new(width as u32, height as u32), + ..default() + }), + order: i, + ..default() + }, + Transform::from_xyz(sin(angle) * 4.0, 2.5, cos(angle) * 4.0) + .looking_at(Vec3::ZERO, Vec3::Y), + )); + i += 1; + } + } +} + +fn rotate_cameras(time: Res