Skip to content

Commit a188bab

Browse files
committed
many_cubes: Add a cube pattern suitable for benchmarking culling changes (#4126)
# Objective - Add a cube pattern to `many_cubes` suitable for benchmarking culling changes ## Solution - Use a 'golden spiral' mapped to a sphere with the strategy of optimising for average nearest neighbour distance, as per: http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
1 parent 4add96b commit a188bab

File tree

1 file changed

+87
-43
lines changed

1 file changed

+87
-43
lines changed

examples/3d/many_cubes.rs

+87-43
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use bevy::{
22
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
3+
math::{DVec2, DVec3},
34
prelude::*,
45
};
5-
66
fn main() {
77
App::new()
88
.add_plugins(DefaultPlugins)
@@ -26,41 +26,75 @@ fn setup(
2626
base_color: Color::PINK,
2727
..default()
2828
});
29-
for x in 0..WIDTH {
30-
for y in 0..HEIGHT {
31-
// introduce spaces to break any kind of moiré pattern
32-
if x % 10 == 0 || y % 10 == 0 {
33-
continue;
29+
30+
match std::env::args().nth(1).as_deref() {
31+
Some("sphere") => {
32+
// NOTE: This pattern is good for testing performance of culling as it provides roughly
33+
// the same number of visible meshes regardless of the viewing angle.
34+
const N_POINTS: usize = WIDTH * HEIGHT * 4;
35+
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
36+
let radius = WIDTH as f64 * 2.5;
37+
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
38+
for i in 0..N_POINTS {
39+
let spherical_polar_theta_phi =
40+
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
41+
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
42+
commands.spawn_bundle(PbrBundle {
43+
mesh: mesh.clone_weak(),
44+
material: material.clone_weak(),
45+
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()),
46+
..default()
47+
});
48+
}
49+
50+
// camera
51+
commands.spawn_bundle(PerspectiveCameraBundle::default());
52+
}
53+
_ => {
54+
// NOTE: This pattern is good for demonstrating that frustum culling is working correctly
55+
// as the number of visible meshes rises and falls depending on the viewing angle.
56+
for x in 0..WIDTH {
57+
for y in 0..HEIGHT {
58+
// introduce spaces to break any kind of moiré pattern
59+
if x % 10 == 0 || y % 10 == 0 {
60+
continue;
61+
}
62+
// cube
63+
commands.spawn_bundle(PbrBundle {
64+
mesh: mesh.clone_weak(),
65+
material: material.clone_weak(),
66+
transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0),
67+
..default()
68+
});
69+
commands.spawn_bundle(PbrBundle {
70+
mesh: mesh.clone_weak(),
71+
material: material.clone_weak(),
72+
transform: Transform::from_xyz(
73+
(x as f32) * 2.5,
74+
HEIGHT as f32 * 2.5,
75+
(y as f32) * 2.5,
76+
),
77+
..default()
78+
});
79+
commands.spawn_bundle(PbrBundle {
80+
mesh: mesh.clone_weak(),
81+
material: material.clone_weak(),
82+
transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5),
83+
..default()
84+
});
85+
commands.spawn_bundle(PbrBundle {
86+
mesh: mesh.clone_weak(),
87+
material: material.clone_weak(),
88+
transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5),
89+
..default()
90+
});
91+
}
3492
}
35-
// cube
36-
commands.spawn_bundle(PbrBundle {
37-
mesh: mesh.clone_weak(),
38-
material: material.clone_weak(),
39-
transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0),
93+
// camera
94+
commands.spawn_bundle(PerspectiveCameraBundle {
95+
transform: Transform::from_xyz(WIDTH as f32, HEIGHT as f32, WIDTH as f32),
4096
..default()
4197
});
42-
commands.spawn_bundle(PbrBundle {
43-
mesh: mesh.clone_weak(),
44-
material: material.clone_weak(),
45-
transform: Transform::from_xyz(
46-
(x as f32) * 2.5,
47-
HEIGHT as f32 * 2.5,
48-
(y as f32) * 2.5,
49-
),
50-
..Default::default()
51-
});
52-
commands.spawn_bundle(PbrBundle {
53-
mesh: mesh.clone_weak(),
54-
material: material.clone_weak(),
55-
transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5),
56-
..Default::default()
57-
});
58-
commands.spawn_bundle(PbrBundle {
59-
mesh: mesh.clone_weak(),
60-
material: material.clone_weak(),
61-
transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5),
62-
..Default::default()
63-
});
6498
}
6599
}
66100

@@ -72,20 +106,30 @@ fn setup(
72106
transform: Transform {
73107
translation: Vec3::new(0.0, HEIGHT as f32 * 2.5, 0.0),
74108
scale: Vec3::splat(5.0),
75-
..Default::default()
109+
..default()
76110
},
77-
..Default::default()
78-
});
79-
80-
// camera
81-
commands.spawn_bundle(PerspectiveCameraBundle {
82-
transform: Transform::from_xyz(WIDTH as f32, HEIGHT as f32, WIDTH as f32),
83111
..default()
84112
});
85113

86-
commands.spawn_bundle(DirectionalLightBundle {
87-
..Default::default()
88-
});
114+
commands.spawn_bundle(DirectionalLightBundle { ..default() });
115+
}
116+
117+
// NOTE: This epsilon value is apparently optimal for optimizing for the average
118+
// nearest-neighbor distance. See:
119+
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
120+
// for details.
121+
const EPSILON: f64 = 0.36;
122+
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
123+
DVec2::new(
124+
2.0 * std::f64::consts::PI * (i as f64 / golden_ratio),
125+
(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)).acos(),
126+
)
127+
}
128+
129+
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
130+
let (sin_theta, cos_theta) = p.x.sin_cos();
131+
let (sin_phi, cos_phi) = p.y.sin_cos();
132+
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
89133
}
90134

91135
// System for rotating the camera

0 commit comments

Comments
 (0)