|
| 1 | +use bevy::{ |
| 2 | + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, |
| 3 | + math::{DVec2, DVec3}, |
| 4 | + pbr::{ExtractedPointLight, GlobalLightMeta}, |
| 5 | + prelude::*, |
| 6 | + render::{RenderApp, RenderStage}, |
| 7 | +}; |
| 8 | + |
| 9 | +fn main() { |
| 10 | + App::new() |
| 11 | + .insert_resource(WindowDescriptor { |
| 12 | + width: 1024.0, |
| 13 | + height: 768.0, |
| 14 | + title: "many_lights".to_string(), |
| 15 | + present_mode: bevy::window::PresentMode::Immediate, |
| 16 | + ..default() |
| 17 | + }) |
| 18 | + .add_plugins(DefaultPlugins) |
| 19 | + .add_plugin(FrameTimeDiagnosticsPlugin::default()) |
| 20 | + .add_plugin(LogDiagnosticsPlugin::default()) |
| 21 | + .add_startup_system(setup) |
| 22 | + .add_system(move_camera) |
| 23 | + .add_system(print_light_count) |
| 24 | + .add_plugin(LogVisibleLights) |
| 25 | + .run(); |
| 26 | +} |
| 27 | + |
| 28 | +fn setup( |
| 29 | + mut commands: Commands, |
| 30 | + mut meshes: ResMut<Assets<Mesh>>, |
| 31 | + mut materials: ResMut<Assets<StandardMaterial>>, |
| 32 | +) { |
| 33 | + const LIGHT_RADIUS: f32 = 0.3; |
| 34 | + const LIGHT_INTENSITY: f32 = 5.0; |
| 35 | + const RADIUS: f32 = 50.0; |
| 36 | + const N_LIGHTS: usize = 100_000; |
| 37 | + |
| 38 | + commands.spawn_bundle(PbrBundle { |
| 39 | + mesh: meshes.add(Mesh::from(shape::Icosphere { |
| 40 | + radius: RADIUS, |
| 41 | + subdivisions: 9, |
| 42 | + })), |
| 43 | + material: materials.add(StandardMaterial::from(Color::WHITE)), |
| 44 | + transform: Transform::from_scale(Vec3::splat(-1.0)), |
| 45 | + ..default() |
| 46 | + }); |
| 47 | + |
| 48 | + let mesh = meshes.add(Mesh::from(shape::Cube { size: 1.0 })); |
| 49 | + let material = materials.add(StandardMaterial { |
| 50 | + base_color: Color::PINK, |
| 51 | + ..default() |
| 52 | + }); |
| 53 | + |
| 54 | + // NOTE: This pattern is good for testing performance of culling as it provides roughly |
| 55 | + // the same number of visible meshes regardless of the viewing angle. |
| 56 | + // NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution |
| 57 | + let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt()); |
| 58 | + for i in 0..N_LIGHTS { |
| 59 | + let spherical_polar_theta_phi = fibonacci_spiral_on_sphere(golden_ratio, i, N_LIGHTS); |
| 60 | + let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi); |
| 61 | + commands.spawn_bundle(PointLightBundle { |
| 62 | + point_light: PointLight { |
| 63 | + range: LIGHT_RADIUS, |
| 64 | + intensity: LIGHT_INTENSITY, |
| 65 | + ..default() |
| 66 | + }, |
| 67 | + transform: Transform::from_translation((RADIUS as f64 * unit_sphere_p).as_vec3()), |
| 68 | + ..default() |
| 69 | + }); |
| 70 | + } |
| 71 | + |
| 72 | + // camera |
| 73 | + commands.spawn_bundle(PerspectiveCameraBundle::default()); |
| 74 | + |
| 75 | + // add one cube, the only one with strong handles |
| 76 | + // also serves as a reference point during rotation |
| 77 | + commands.spawn_bundle(PbrBundle { |
| 78 | + mesh, |
| 79 | + material, |
| 80 | + transform: Transform { |
| 81 | + translation: Vec3::new(0.0, RADIUS as f32, 0.0), |
| 82 | + scale: Vec3::splat(5.0), |
| 83 | + ..default() |
| 84 | + }, |
| 85 | + ..default() |
| 86 | + }); |
| 87 | +} |
| 88 | + |
| 89 | +// NOTE: This epsilon value is apparently optimal for optimizing for the average |
| 90 | +// nearest-neighbor distance. See: |
| 91 | +// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/ |
| 92 | +// for details. |
| 93 | +const EPSILON: f64 = 0.36; |
| 94 | +fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 { |
| 95 | + DVec2::new( |
| 96 | + 2.0 * std::f64::consts::PI * (i as f64 / golden_ratio), |
| 97 | + (1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)).acos(), |
| 98 | + ) |
| 99 | +} |
| 100 | + |
| 101 | +fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 { |
| 102 | + let (sin_theta, cos_theta) = p.x.sin_cos(); |
| 103 | + let (sin_phi, cos_phi) = p.y.sin_cos(); |
| 104 | + DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi) |
| 105 | +} |
| 106 | + |
| 107 | +// System for rotating the camera |
| 108 | +fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) { |
| 109 | + let mut camera_transform = camera_query.single_mut(); |
| 110 | + camera_transform.rotate(Quat::from_rotation_z(time.delta_seconds() * 0.15)); |
| 111 | + camera_transform.rotate(Quat::from_rotation_x(time.delta_seconds() * 0.15)); |
| 112 | +} |
| 113 | + |
| 114 | +// System for printing the number of meshes on every tick of the timer |
| 115 | +fn print_light_count(time: Res<Time>, mut timer: Local<PrintingTimer>, lights: Query<&PointLight>) { |
| 116 | + timer.0.tick(time.delta()); |
| 117 | + |
| 118 | + if timer.0.just_finished() { |
| 119 | + info!("Lights: {}", lights.iter().len(),); |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +struct LogVisibleLights; |
| 124 | + |
| 125 | +impl Plugin for LogVisibleLights { |
| 126 | + fn build(&self, app: &mut App) { |
| 127 | + let render_app = match app.get_sub_app_mut(RenderApp) { |
| 128 | + Ok(render_app) => render_app, |
| 129 | + Err(_) => return, |
| 130 | + }; |
| 131 | + |
| 132 | + render_app |
| 133 | + .add_system_to_stage(RenderStage::Extract, extract_time) |
| 134 | + .add_system_to_stage(RenderStage::Prepare, print_visible_light_count); |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +// System for printing the number of meshes on every tick of the timer |
| 139 | +fn print_visible_light_count( |
| 140 | + time: Res<Time>, |
| 141 | + mut timer: Local<PrintingTimer>, |
| 142 | + visible: Query<&ExtractedPointLight>, |
| 143 | + global_light_meta: Res<GlobalLightMeta>, |
| 144 | +) { |
| 145 | + timer.0.tick(time.delta()); |
| 146 | + |
| 147 | + if timer.0.just_finished() { |
| 148 | + info!( |
| 149 | + "Visible Lights: {}, Rendered Lights: {}", |
| 150 | + visible.iter().len(), |
| 151 | + global_light_meta.entity_to_index.len() |
| 152 | + ); |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +fn extract_time(mut commands: Commands, time: Res<Time>) { |
| 157 | + commands.insert_resource(time.into_inner().clone()); |
| 158 | +} |
| 159 | + |
| 160 | +struct PrintingTimer(Timer); |
| 161 | + |
| 162 | +impl Default for PrintingTimer { |
| 163 | + fn default() -> Self { |
| 164 | + Self(Timer::from_seconds(1.0, true)) |
| 165 | + } |
| 166 | +} |
0 commit comments