Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_cubes.rs
6592 views
1
//! Simple benchmark to test per-entity draw overhead.
2
//!
3
//! To measure performance realistically, be sure to run this in release mode.
4
//! `cargo run --example many_cubes --release`
5
//!
6
//! By default, this arranges the meshes in a spherical pattern that
7
//! distributes the meshes evenly.
8
//!
9
//! See `cargo run --example many_cubes --release -- --help` for more options.
10
11
use std::{f64::consts::PI, str::FromStr};
12
13
use argh::FromArgs;
14
use bevy::{
15
asset::RenderAssetUsages,
16
camera::visibility::{NoCpuCulling, NoFrustumCulling},
17
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
18
light::NotShadowCaster,
19
math::{DVec2, DVec3},
20
prelude::*,
21
render::{
22
batching::NoAutomaticBatching,
23
render_resource::{Extent3d, TextureDimension, TextureFormat},
24
view::NoIndirectDrawing,
25
},
26
window::{PresentMode, WindowResolution},
27
winit::{UpdateMode, WinitSettings},
28
};
29
use rand::{seq::IndexedRandom, Rng, SeedableRng};
30
use rand_chacha::ChaCha8Rng;
31
32
#[derive(FromArgs, Resource)]
33
/// `many_cubes` stress test
34
struct Args {
35
/// how the cube instances should be positioned.
36
#[argh(option, default = "Layout::Sphere")]
37
layout: Layout,
38
39
/// whether to step the camera animation by a fixed amount such that each frame is the same across runs.
40
#[argh(switch)]
41
benchmark: bool,
42
43
/// whether to vary the material data in each instance.
44
#[argh(switch)]
45
vary_material_data_per_instance: bool,
46
47
/// the number of different textures from which to randomly select the material base color. 0 means no textures.
48
#[argh(option, default = "0")]
49
material_texture_count: usize,
50
51
/// the number of different meshes from which to randomly select. Clamped to at least 1.
52
#[argh(option, default = "1")]
53
mesh_count: usize,
54
55
/// whether to disable all frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn.
56
#[argh(switch)]
57
no_frustum_culling: bool,
58
59
/// whether to disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding.
60
#[argh(switch)]
61
no_automatic_batching: bool,
62
63
/// whether to disable indirect drawing.
64
#[argh(switch)]
65
no_indirect_drawing: bool,
66
67
/// whether to disable CPU culling.
68
#[argh(switch)]
69
no_cpu_culling: bool,
70
71
/// whether to enable directional light cascaded shadow mapping.
72
#[argh(switch)]
73
shadows: bool,
74
75
/// animate the cube materials by updating the material from the cpu each frame
76
#[argh(switch)]
77
animate_materials: bool,
78
}
79
80
#[derive(Default, Clone)]
81
enum Layout {
82
Cube,
83
#[default]
84
Sphere,
85
}
86
87
impl FromStr for Layout {
88
type Err = String;
89
90
fn from_str(s: &str) -> Result<Self, Self::Err> {
91
match s {
92
"cube" => Ok(Self::Cube),
93
"sphere" => Ok(Self::Sphere),
94
_ => Err(format!(
95
"Unknown layout value: '{s}', valid options: 'cube', 'sphere'"
96
)),
97
}
98
}
99
}
100
101
fn main() {
102
// `from_env` panics on the web
103
#[cfg(not(target_arch = "wasm32"))]
104
let args: Args = argh::from_env();
105
#[cfg(target_arch = "wasm32")]
106
let args = Args::from_args(&[], &[]).unwrap();
107
108
let mut app = App::new();
109
app.add_plugins((
110
DefaultPlugins.set(WindowPlugin {
111
primary_window: Some(Window {
112
present_mode: PresentMode::AutoNoVsync,
113
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
114
..default()
115
}),
116
..default()
117
}),
118
FrameTimeDiagnosticsPlugin::default(),
119
LogDiagnosticsPlugin::default(),
120
))
121
.insert_resource(WinitSettings {
122
focused_mode: UpdateMode::Continuous,
123
unfocused_mode: UpdateMode::Continuous,
124
})
125
.add_systems(Startup, setup)
126
.add_systems(Update, (move_camera, print_mesh_count));
127
128
if args.animate_materials {
129
app.add_systems(Update, update_materials);
130
}
131
132
app.insert_resource(args).run();
133
}
134
135
const WIDTH: usize = 200;
136
const HEIGHT: usize = 200;
137
138
fn setup(
139
mut commands: Commands,
140
args: Res<Args>,
141
mesh_assets: ResMut<Assets<Mesh>>,
142
material_assets: ResMut<Assets<StandardMaterial>>,
143
images: ResMut<Assets<Image>>,
144
) {
145
warn!(include_str!("warning_string.txt"));
146
147
let args = args.into_inner();
148
let images = images.into_inner();
149
let material_assets = material_assets.into_inner();
150
let mesh_assets = mesh_assets.into_inner();
151
152
let meshes = init_meshes(args, mesh_assets);
153
154
let material_textures = init_textures(args, images);
155
let materials = init_materials(args, &material_textures, material_assets);
156
157
// We're seeding the PRNG here to make this example deterministic for testing purposes.
158
// This isn't strictly required in practical use unless you need your app to be deterministic.
159
let mut material_rng = ChaCha8Rng::seed_from_u64(42);
160
match args.layout {
161
Layout::Sphere => {
162
// NOTE: This pattern is good for testing performance of culling as it provides roughly
163
// the same number of visible meshes regardless of the viewing angle.
164
const N_POINTS: usize = WIDTH * HEIGHT * 4;
165
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
166
let radius = WIDTH as f64 * 2.5;
167
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
168
for i in 0..N_POINTS {
169
let spherical_polar_theta_phi =
170
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
171
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
172
let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
173
commands
174
.spawn((
175
Mesh3d(mesh.clone()),
176
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
177
Transform::from_translation((radius * unit_sphere_p).as_vec3())
178
.looking_at(Vec3::ZERO, Vec3::Y)
179
.mul_transform(*transform),
180
))
181
.insert_if(NoFrustumCulling, || args.no_frustum_culling)
182
.insert_if(NoAutomaticBatching, || args.no_automatic_batching);
183
}
184
185
// camera
186
let mut camera = commands.spawn(Camera3d::default());
187
if args.no_indirect_drawing {
188
camera.insert(NoIndirectDrawing);
189
}
190
if args.no_cpu_culling {
191
camera.insert(NoCpuCulling);
192
}
193
194
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
195
commands.spawn((
196
Mesh3d(mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2)))),
197
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
198
Transform::from_scale(-Vec3::ONE),
199
NotShadowCaster,
200
));
201
}
202
_ => {
203
// NOTE: This pattern is good for demonstrating that frustum culling is working correctly
204
// as the number of visible meshes rises and falls depending on the viewing angle.
205
let scale = 2.5;
206
for x in 0..WIDTH {
207
for y in 0..HEIGHT {
208
// introduce spaces to break any kind of moiré pattern
209
if x % 10 == 0 || y % 10 == 0 {
210
continue;
211
}
212
// cube
213
commands.spawn((
214
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
215
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
216
Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
217
));
218
commands.spawn((
219
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
220
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
221
Transform::from_xyz(
222
(x as f32) * scale,
223
HEIGHT as f32 * scale,
224
(y as f32) * scale,
225
),
226
));
227
commands.spawn((
228
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
229
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
230
Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
231
));
232
commands.spawn((
233
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
234
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
235
Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
236
));
237
}
238
}
239
// camera
240
let center = 0.5 * scale * Vec3::new(WIDTH as f32, HEIGHT as f32, WIDTH as f32);
241
commands.spawn((Camera3d::default(), Transform::from_translation(center)));
242
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
243
commands.spawn((
244
Mesh3d(mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center))),
245
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
246
Transform::from_scale(-Vec3::ONE).with_translation(center),
247
NotShadowCaster,
248
));
249
}
250
}
251
252
commands.spawn((
253
DirectionalLight {
254
shadows_enabled: args.shadows,
255
..default()
256
},
257
Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
258
));
259
}
260
261
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
262
// We're seeding the PRNG here to make this example deterministic for testing purposes.
263
// This isn't strictly required in practical use unless you need your app to be deterministic.
264
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
265
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
266
.map(|i| {
267
if (i % 4) == 3 {
268
255
269
} else {
270
color_rng.random()
271
}
272
})
273
.collect();
274
color_bytes
275
.chunks(4)
276
.map(|pixel| {
277
images.add(Image::new_fill(
278
Extent3d::default(),
279
TextureDimension::D2,
280
pixel,
281
TextureFormat::Rgba8UnormSrgb,
282
RenderAssetUsages::RENDER_WORLD,
283
))
284
})
285
.collect()
286
}
287
288
fn init_materials(
289
args: &Args,
290
textures: &[Handle<Image>],
291
assets: &mut Assets<StandardMaterial>,
292
) -> Vec<Handle<StandardMaterial>> {
293
let capacity = if args.vary_material_data_per_instance {
294
match args.layout {
295
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
296
Layout::Sphere => WIDTH * HEIGHT * 4,
297
}
298
} else {
299
args.material_texture_count
300
}
301
.max(1);
302
303
let mut materials = Vec::with_capacity(capacity);
304
materials.push(assets.add(StandardMaterial {
305
base_color: Color::WHITE,
306
base_color_texture: textures.first().cloned(),
307
..default()
308
}));
309
310
// We're seeding the PRNG here to make this example deterministic for testing purposes.
311
// This isn't strictly required in practical use unless you need your app to be deterministic.
312
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
313
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
314
materials.extend(
315
std::iter::repeat_with(|| {
316
assets.add(StandardMaterial {
317
base_color: Color::srgb_u8(
318
color_rng.random(),
319
color_rng.random(),
320
color_rng.random(),
321
),
322
base_color_texture: textures.choose(&mut texture_rng).cloned(),
323
..default()
324
})
325
})
326
.take(capacity - materials.len()),
327
);
328
329
materials
330
}
331
332
fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
333
let capacity = args.mesh_count.max(1);
334
335
// We're seeding the PRNG here to make this example deterministic for testing purposes.
336
// This isn't strictly required in practical use unless you need your app to be deterministic.
337
let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
338
let mut variant = 0;
339
std::iter::repeat_with(|| {
340
let radius = radius_rng.random_range(0.25f32..=0.75f32);
341
let (handle, transform) = match variant % 15 {
342
0 => (
343
assets.add(Cuboid {
344
half_size: Vec3::splat(radius),
345
}),
346
Transform::IDENTITY,
347
),
348
1 => (
349
assets.add(Capsule3d {
350
radius,
351
half_length: radius,
352
}),
353
Transform::IDENTITY,
354
),
355
2 => (
356
assets.add(Circle { radius }),
357
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
358
),
359
3 => {
360
let mut vertices = [Vec2::ZERO; 3];
361
let dtheta = std::f32::consts::TAU / 3.0;
362
for (i, vertex) in vertices.iter_mut().enumerate() {
363
let (s, c) = ops::sin_cos(i as f32 * dtheta);
364
*vertex = Vec2::new(c, s) * radius;
365
}
366
(
367
assets.add(Triangle2d { vertices }),
368
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
369
)
370
}
371
4 => (
372
assets.add(Rectangle {
373
half_size: Vec2::splat(radius),
374
}),
375
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
376
),
377
v if (5..=8).contains(&v) => (
378
assets.add(RegularPolygon {
379
circumcircle: Circle { radius },
380
sides: v,
381
}),
382
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
383
),
384
9 => (
385
assets.add(Cylinder {
386
radius,
387
half_height: radius,
388
}),
389
Transform::IDENTITY,
390
),
391
10 => (
392
assets.add(Ellipse {
393
half_size: Vec2::new(radius, 0.5 * radius),
394
}),
395
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
396
),
397
11 => (
398
assets.add(
399
Plane3d {
400
normal: Dir3::NEG_Z,
401
half_size: Vec2::splat(0.5),
402
}
403
.mesh()
404
.size(radius, radius),
405
),
406
Transform::IDENTITY,
407
),
408
12 => (assets.add(Sphere { radius }), Transform::IDENTITY),
409
13 => (
410
assets.add(Torus {
411
minor_radius: 0.5 * radius,
412
major_radius: radius,
413
}),
414
Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
415
),
416
14 => (
417
assets.add(Capsule2d {
418
radius,
419
half_length: radius,
420
}),
421
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
422
),
423
_ => unreachable!(),
424
};
425
variant += 1;
426
(handle, transform)
427
})
428
.take(capacity)
429
.collect()
430
}
431
432
// NOTE: This epsilon value is apparently optimal for optimizing for the average
433
// nearest-neighbor distance. See:
434
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
435
// for details.
436
const EPSILON: f64 = 0.36;
437
438
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
439
DVec2::new(
440
PI * 2. * (i as f64 / golden_ratio),
441
f64::acos(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)),
442
)
443
}
444
445
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
446
let (sin_theta, cos_theta) = p.x.sin_cos();
447
let (sin_phi, cos_phi) = p.y.sin_cos();
448
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
449
}
450
451
// System for rotating the camera
452
fn move_camera(
453
time: Res<Time>,
454
args: Res<Args>,
455
mut camera_transform: Single<&mut Transform, With<Camera>>,
456
) {
457
let delta = 0.15
458
* if args.benchmark {
459
1.0 / 60.0
460
} else {
461
time.delta_secs()
462
};
463
camera_transform.rotate_z(delta);
464
camera_transform.rotate_x(delta);
465
}
466
467
// System for printing the number of meshes on every tick of the timer
468
fn print_mesh_count(
469
time: Res<Time>,
470
mut timer: Local<PrintingTimer>,
471
sprites: Query<(&Mesh3d, &ViewVisibility)>,
472
) {
473
timer.tick(time.delta());
474
475
if timer.just_finished() {
476
info!(
477
"Meshes: {} - Visible Meshes {}",
478
sprites.iter().len(),
479
sprites.iter().filter(|(_, vis)| vis.get()).count(),
480
);
481
}
482
}
483
484
#[derive(Deref, DerefMut)]
485
struct PrintingTimer(Timer);
486
487
impl Default for PrintingTimer {
488
fn default() -> Self {
489
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
490
}
491
}
492
493
fn update_materials(mut materials: ResMut<Assets<StandardMaterial>>, time: Res<Time>) {
494
let elapsed = time.elapsed_secs();
495
for (i, (_, material)) in materials.iter_mut().enumerate() {
496
let hue = (elapsed + i as f32 * 0.005).rem_euclid(1.0);
497
// This is much faster than using base_color.set_hue(hue), and in a tight loop it shows.
498
let color = fast_hue_to_rgb(hue);
499
material.base_color = Color::linear_rgb(color.x, color.y, color.z);
500
}
501
}
502
503
#[inline]
504
fn fast_hue_to_rgb(hue: f32) -> Vec3 {
505
(hue * 6.0 - vec3(3.0, 2.0, 4.0)).abs() * vec3(1.0, -1.0, -1.0) + vec3(-1.0, 2.0, 2.0)
506
}
507
508