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 GitHub
parent aad1fc6eba
commit ef23f465ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 297 additions and 131 deletions

View file

@ -2706,6 +2706,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests" category = "Stress Tests"
wasm = true 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]] [[example]]
name = "many_cubes" name = "many_cubes"
path = "examples/stress_tests/many_cubes.rs" path = "examples/stress_tests/many_cubes.rs"

View file

@ -1,6 +1,9 @@
use core::ops::DerefMut; 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_math::{ops, Mat4, Vec3A, Vec4};
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::{ 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( pub fn check_point_light_mesh_visibility(
visible_point_lights: Query<&VisibleClusterableObjects>, visible_point_lights: Query<&VisibleClusterableObjects>,
mut point_lights: Query<( mut point_lights: Query<(
@ -872,10 +876,17 @@ pub fn check_point_light_mesh_visibility(
visible_entity_ranges: Option<Res<VisibleEntityRanges>>, visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>, mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>, 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(); let visible_entity_ranges = visible_entity_ranges.as_deref();
for visible_lights in &visible_point_lights { for visible_lights in &visible_point_lights {
for light_entity in visible_lights.entities.iter().copied() { for light_entity in visible_lights.entities.iter().copied() {
if !checked_lights.insert(light_entity) {
continue;
}
// Point lights // Point lights
if let Ok(( if let Ok((
point_light, point_light,

View file

@ -9,6 +9,7 @@ use bevy_ecs::{
system::lifetimeless::Read, system::lifetimeless::Read,
}; };
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; 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::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity};
use bevy_render::{ use bevy_render::{
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
@ -29,6 +30,7 @@ use bevy_utils::tracing::info_span;
use bevy_utils::{ use bevy_utils::{
default, default,
tracing::{error, warn}, tracing::{error, warn},
HashMap,
}; };
use core::{hash::Hash, ops::Range}; use core::{hash::Hash, ops::Range};
@ -677,10 +679,11 @@ pub fn prepare_lights(
point_light_shadow_map: Res<PointLightShadowMap>, point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>, directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>, mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
(mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( (
Local<bool>, mut max_directional_lights_warning_emitted,
Local<bool>, mut max_cascades_per_light_warning_emitted,
), mut live_shadow_mapping_lights,
): (Local<bool>, Local<bool>, Local<EntityHashSet>),
point_lights: Query<( point_lights: Query<(
Entity, Entity,
&ExtractedPointLight, &ExtractedPointLight,
@ -688,7 +691,7 @@ pub fn prepare_lights(
)>, )>,
directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
mut light_view_entities: Query<&mut LightViewEntities>, 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_iter = views.iter();
let views_count = views_iter.len(); let views_count = views_iter.len();
@ -984,50 +987,108 @@ pub fn prepare_lights(
live_shadow_mapping_lights.clear(); live_shadow_mapping_lights.clear();
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,
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); let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash);
// set up light data for each view // 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); 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 mut view_lights = Vec::new();
let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; 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()) .zip(light_view_entities.iter().copied())
.enumerate() .enumerate()
{ {
let depth_texture_view = let mut first = false;
point_light_depth_texture let base_array_layer = (light_index * 6 + face_index) as u32;
.texture
.create_view(&TextureViewDescriptor { let depth_attachment = point_light_depth_attachments
label: Some("point_light_shadow_map_texture_view"), .entry(base_array_layer)
format: None, .or_insert_with(|| {
dimension: Some(TextureViewDimension::D2), first = true;
aspect: TextureAspect::All,
base_mip_level: 0, let depth_texture_view =
mip_level_count: None, point_light_depth_texture
base_array_layer: (light_index * 6 + face_index) as u32, .texture
array_layer_count: Some(1u32), .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(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!( pass_name: format!(
"shadow pass point light {} {}", "shadow pass point light {} {}",
light_index, light_index,
@ -1147,8 +1220,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity); view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity); if first {
live_shadow_mapping_lights.insert(view_light_entity); // 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; [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 spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z);
let depth_texture_view = let mut first = false;
directional_light_depth_texture let base_array_layer = (num_directional_cascades_enabled + light_index) as u32;
.texture
.create_view(&TextureViewDescriptor { let depth_attachment = directional_light_depth_attachments
label: Some("spot_light_shadow_map_texture_view"), .entry(base_array_layer)
format: None, .or_insert_with(|| {
dimension: Some(TextureViewDimension::D2), first = true;
aspect: TextureAspect::All,
base_mip_level: 0, let depth_texture_view = directional_light_depth_texture.texture.create_view(
mip_level_count: None, &TextureViewDescriptor {
base_array_layer: (num_directional_cascades_enabled + light_index) as u32, label: Some("spot_light_shadow_map_texture_view"),
array_layer_count: Some(1u32), 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 let light_view_entities = light_view_entities
.entry(entity) .entry(entity)
@ -1199,7 +1286,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!("shadow pass spot light {light_index}"), pass_name: format!("shadow pass spot light {light_index}"),
}, },
ExtractedView { ExtractedView {
@ -1221,8 +1308,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity); view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity); if first {
live_shadow_mapping_lights.insert(view_light_entity); // 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 // directional lights
@ -1307,6 +1397,12 @@ pub fn prepare_lights(
base_array_layer: directional_depth_texture_array_index, base_array_layer: directional_depth_texture_array_index,
array_layer_count: Some(1u32), 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; directional_depth_texture_array_index += 1;
let mut frustum = *frustum; let mut frustum = *frustum;
@ -1316,7 +1412,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!( pass_name: format!(
"shadow pass directional light {light_index} cascade {cascade_index}" "shadow pass directional light {light_index} cascade {cascade_index}"
), ),
@ -1342,65 +1438,19 @@ pub fn prepare_lights(
)); ));
view_lights.push(view_light_entity); 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); shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(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(( commands.entity(entity).insert((
ViewShadowBindings { ViewShadowBindings {
point_light_depth_texture: point_light_depth_texture.texture, point_light_depth_texture: point_light_depth_texture.texture.clone(),
point_light_depth_texture_view, point_light_depth_texture_view: point_light_depth_texture_view.clone(),
directional_light_depth_texture: directional_light_depth_texture.texture, directional_light_depth_texture: directional_light_depth_texture.texture.clone(),
directional_light_depth_texture_view, directional_light_depth_texture_view: directional_light_depth_texture_view.clone(),
}, },
ViewLightEntities { ViewLightEntities {
lights: view_lights, lights: view_lights,

View file

@ -80,6 +80,7 @@ impl ColorAttachment {
} }
/// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. /// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`].
#[derive(Clone)]
pub struct DepthAttachment { pub struct DepthAttachment {
pub view: TextureView, pub view: TextureView,
clear_value: Option<f32>, 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 [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 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 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 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 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 [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()));
}
}