many_lights/
many_lights.rs

1//! Simple benchmark to test rendering many point lights.
2//! Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights.
3
4use std::f64::consts::PI;
5
6use bevy::{
7    camera::ScalingMode,
8    color::palettes::css::DEEP_PINK,
9    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
10    math::{DVec2, DVec3},
11    pbr::{ExtractedPointLight, GlobalClusterableObjectMeta},
12    prelude::*,
13    render::{Render, RenderApp, RenderSystems},
14    window::{PresentMode, WindowResolution},
15    winit::WinitSettings,
16};
17use rand::{rng, Rng};
18
19fn main() {
20    App::new()
21        .add_plugins((
22            DefaultPlugins.set(WindowPlugin {
23                primary_window: Some(Window {
24                    resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
25                    title: "many_lights".into(),
26                    present_mode: PresentMode::AutoNoVsync,
27                    ..default()
28                }),
29                ..default()
30            }),
31            FrameTimeDiagnosticsPlugin::default(),
32            LogDiagnosticsPlugin::default(),
33            LogVisibleLights,
34        ))
35        .insert_resource(WinitSettings::continuous())
36        .add_systems(Startup, setup)
37        .add_systems(Update, (move_camera, print_light_count))
38        .run();
39}
40
41fn setup(
42    mut commands: Commands,
43    mut meshes: ResMut<Assets<Mesh>>,
44    mut materials: ResMut<Assets<StandardMaterial>>,
45) {
46    warn!(include_str!("warning_string.txt"));
47
48    const LIGHT_RADIUS: f32 = 0.3;
49    const LIGHT_INTENSITY: f32 = 1000.0;
50    const RADIUS: f32 = 50.0;
51    const N_LIGHTS: usize = 100_000;
52
53    commands.spawn((
54        Mesh3d(meshes.add(Sphere::new(RADIUS).mesh().ico(9).unwrap())),
55        MeshMaterial3d(materials.add(Color::WHITE)),
56        Transform::from_scale(Vec3::NEG_ONE),
57    ));
58
59    let mesh = meshes.add(Cuboid::default());
60    let material = materials.add(StandardMaterial {
61        base_color: DEEP_PINK.into(),
62        ..default()
63    });
64
65    // NOTE: This pattern is good for testing performance of culling as it provides roughly
66    // the same number of visible meshes regardless of the viewing angle.
67    // NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
68    let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
69
70    // Spawn N_LIGHTS many lights
71    commands.spawn_batch((0..N_LIGHTS).map(move |i| {
72        let mut rng = rng();
73
74        let spherical_polar_theta_phi = fibonacci_spiral_on_sphere(golden_ratio, i, N_LIGHTS);
75        let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
76
77        (
78            PointLight {
79                range: LIGHT_RADIUS,
80                intensity: LIGHT_INTENSITY,
81                color: Color::hsl(rng.random_range(0.0..360.0), 1.0, 0.5),
82                ..default()
83            },
84            Transform::from_translation((RADIUS as f64 * unit_sphere_p).as_vec3()),
85        )
86    }));
87
88    // camera
89    match std::env::args().nth(1).as_deref() {
90        Some("orthographic") => commands.spawn((
91            Camera3d::default(),
92            Projection::from(OrthographicProjection {
93                scaling_mode: ScalingMode::FixedHorizontal {
94                    viewport_width: 20.0,
95                },
96                ..OrthographicProjection::default_3d()
97            }),
98        )),
99        _ => commands.spawn(Camera3d::default()),
100    };
101
102    // add one cube, the only one with strong handles
103    // also serves as a reference point during rotation
104    commands.spawn((
105        Mesh3d(mesh),
106        MeshMaterial3d(material),
107        Transform {
108            translation: Vec3::new(0.0, RADIUS, 0.0),
109            scale: Vec3::splat(5.0),
110            ..default()
111        },
112    ));
113}
114
115// NOTE: This epsilon value is apparently optimal for optimizing for the average
116// nearest-neighbor distance. See:
117// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
118// for details.
119const EPSILON: f64 = 0.36;
120fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
121    DVec2::new(
122        PI * 2. * (i as f64 / golden_ratio),
123        ops::acos((1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)) as f32)
124            as f64,
125    )
126}
127
128fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
129    let (sin_theta, cos_theta) = p.x.sin_cos();
130    let (sin_phi, cos_phi) = p.y.sin_cos();
131    DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
132}
133
134// System for rotating the camera
135fn move_camera(time: Res<Time>, mut camera_transform: Single<&mut Transform, With<Camera>>) {
136    let delta = time.delta_secs() * 0.15;
137    camera_transform.rotate_z(delta);
138    camera_transform.rotate_x(delta);
139}
140
141// System for printing the number of meshes on every tick of the timer
142fn print_light_count(time: Res<Time>, mut timer: Local<PrintingTimer>, lights: Query<&PointLight>) {
143    timer.0.tick(time.delta());
144
145    if timer.0.just_finished() {
146        info!("Lights: {}", lights.iter().len());
147    }
148}
149
150struct LogVisibleLights;
151
152impl Plugin for LogVisibleLights {
153    fn build(&self, app: &mut App) {
154        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
155            return;
156        };
157
158        render_app.add_systems(
159            Render,
160            print_visible_light_count.in_set(RenderSystems::Prepare),
161        );
162    }
163}
164
165// System for printing the number of meshes on every tick of the timer
166fn print_visible_light_count(
167    time: Res<Time>,
168    mut timer: Local<PrintingTimer>,
169    visible: Query<&ExtractedPointLight>,
170    global_light_meta: Res<GlobalClusterableObjectMeta>,
171) {
172    timer.0.tick(time.delta());
173
174    if timer.0.just_finished() {
175        info!(
176            "Visible Lights: {}, Rendered Lights: {}",
177            visible.iter().len(),
178            global_light_meta.entity_to_index.len()
179        );
180    }
181}
182
183struct PrintingTimer(Timer);
184
185impl Default for PrintingTimer {
186    fn default() -> Self {
187        Self(Timer::from_seconds(1.0, TimerMode::Repeating))
188    }
189}