Do not re-check visibility or re-render shadow maps for point and spot lights for each view (#15156)

# Objective

_If I understand it correctly_, we were checking mesh visibility, as
well as re-rendering point and spot light shadow maps for each view.
This makes it so that M views and N lights produce M x N complexity.
This PR aims to fix that, as well as introduce a stress test for this
specific scenario.

## Solution

- Keep track of what lights have already had mesh visibility calculated
and do not calculate it again;
- Reuse shadow depth textures and attachments across all views, and only
render shadow maps for the _first_ time a light is encountered on a
view;
- Directional lights remain unaltered, since their shadow map cascades
are view-dependent;
- Add a new `many_cameras_lights` stress test example to verify the
solution

## Showcase

110% speed up on the stress test
83% reduction of memory usage in stress test

### Before (5.35 FPS on stress test)
<img width="1392" alt="Screenshot 2024-09-11 at 12 25 57"
src="https://github.com/user-attachments/assets/136b0785-e9a4-44df-9a22-f99cc465e126">

### After (11.34 FPS on stress test)
<img width="1392" alt="Screenshot 2024-09-11 at 12 24 35"
src="https://github.com/user-attachments/assets/b8dd858f-5e19-467f-8344-2b46ca039630">


## Testing

- Did you test these changes? If so, how? 
- On my game project where I have two cameras, and many shadow casting
lights I managed to get pretty much double the FPS.
  - Also included a stress test, see the comparison above
- Are there any parts that need more testing?
- Yes, I would like help verifying that this fix is indeed correct, and
that we were really re-rendering the shadow maps by mistake and it's
indeed okay to not do that
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
  - Run the `many_cameras_lights` example
- On the `main` branch, cherry pick the commit with the example (`git
cherry-pick --no-commit 1ed4ace01`) and run it
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
  - macOS

---------

Co-authored-by: François Mockers <francois.mockers@vleue.com>
This commit is contained in:
Marco Buono 2024-11-11 15:49:09 -03:00 committed by François Mockers
parent 7284d14adc
commit 623e7a63a5
6 changed files with 297 additions and 131 deletions

View file

@ -2708,6 +2708,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"

View file

@ -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<Res<VisibleEntityRanges>>,
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
mut checked_lights: Local<EntityHashSet>,
) {
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,

View file

@ -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};
@ -686,10 +688,11 @@ pub fn prepare_lights(
point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
(mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): (
Local<bool>,
Local<bool>,
),
(
mut max_directional_lights_warning_emitted,
mut max_cascades_per_light_warning_emitted,
mut live_shadow_mapping_lights,
): (Local<bool>, Local<bool>, Local<EntityHashSet>),
point_lights: Query<(
Entity,
&ExtractedPointLight,
@ -697,7 +700,7 @@ pub fn prepare_lights(
)>,
directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
mut light_view_entities: Query<&mut LightViewEntities>,
mut live_shadow_mapping_lights: Local<EntityHashSet>,
sorted_cameras: Res<SortedCameras>,
) {
let views_iter = views.iter();
let views_count = views_iter.len();
@ -993,11 +996,8 @@ pub fn prepare_lights(
live_shadow_mapping_lights.clear();
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() {
live_views.insert(entity);
let mut point_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
let mut directional_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
let point_light_depth_texture = texture_cache.get(
&render_device,
@ -1016,6 +1016,36 @@ pub fn prepare_lights(
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 {
@ -1037,6 +1067,37 @@ pub fn prepare_lights(
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 sorted_cameras
.0
.iter()
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
{
live_views.insert(entity);
let mut view_lights = Vec::new();
let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0;
@ -1111,6 +1172,14 @@ pub fn prepare_lights(
.zip(light_view_entities.iter().copied())
.enumerate()
{
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
@ -1121,13 +1190,17 @@ pub fn prepare_lights(
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: (light_index * 6 + face_index) as u32,
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,
@ -1156,10 +1229,13 @@ pub fn prepare_lights(
view_lights.push(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);
}
}
}
// spot lights
for (light_index, &(light_entity, light, (_, spot_light_frustum))) in point_lights
@ -1186,19 +1262,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 {
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: (num_directional_cascades_enabled + light_index) as u32,
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)
@ -1208,7 +1295,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 {
@ -1230,9 +1317,12 @@ pub fn prepare_lights(
view_lights.push(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
let mut directional_depth_texture_array_index = 0u32;
@ -1316,6 +1406,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;
@ -1325,7 +1421,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}"
),
@ -1351,65 +1447,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,

View file

@ -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<f32>,

View file

@ -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

View file

@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
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<Time>, mut query: Query<&mut Transform, With<Camera>>) {
for mut transform in query.iter_mut() {
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_secs()));
}
}