Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_cubes.rs
9349 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::{ops::cbrt, 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::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
/// whether to continuously rotate individual cubes.
76
#[argh(switch)]
77
rotate_cubes: bool,
78
79
/// animate the cube materials by updating the material from the cpu each frame
80
#[argh(switch)]
81
animate_materials: bool,
82
}
83
84
#[derive(Default, Clone, PartialEq)]
85
enum Layout {
86
Cube,
87
#[default]
88
Sphere,
89
Dense,
90
}
91
92
impl FromStr for Layout {
93
type Err = String;
94
95
fn from_str(s: &str) -> Result<Self, Self::Err> {
96
match s {
97
"cube" => Ok(Self::Cube),
98
"sphere" => Ok(Self::Sphere),
99
"dense" => Ok(Self::Dense),
100
_ => Err(format!(
101
"Unknown layout value: '{s}', valid options: 'cube', 'sphere', 'dense'"
102
)),
103
}
104
}
105
}
106
107
fn main() {
108
// `from_env` panics on the web
109
#[cfg(not(target_arch = "wasm32"))]
110
let args: Args = argh::from_env();
111
#[cfg(target_arch = "wasm32")]
112
let args = Args::from_args(&[], &[]).unwrap();
113
114
let mut app = App::new();
115
app.add_plugins((
116
DefaultPlugins.set(WindowPlugin {
117
primary_window: Some(Window {
118
present_mode: PresentMode::AutoNoVsync,
119
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
120
..default()
121
}),
122
..default()
123
}),
124
FrameTimeDiagnosticsPlugin::default(),
125
LogDiagnosticsPlugin::default(),
126
))
127
.insert_resource(WinitSettings::continuous())
128
.add_systems(Startup, setup)
129
.add_systems(Update, print_mesh_count);
130
131
if args.layout != Layout::Dense {
132
app.add_systems(Update, move_camera);
133
}
134
135
if args.rotate_cubes {
136
app.add_systems(Update, rotate_cubes);
137
}
138
139
if args.animate_materials {
140
app.add_systems(Update, update_materials);
141
}
142
143
app.insert_resource(args).run();
144
}
145
146
const WIDTH: usize = 200;
147
const HEIGHT: usize = 200;
148
149
fn setup(
150
mut commands: Commands,
151
args: Res<Args>,
152
mesh_assets: ResMut<Assets<Mesh>>,
153
material_assets: ResMut<Assets<StandardMaterial>>,
154
images: ResMut<Assets<Image>>,
155
) {
156
warn!(include_str!("warning_string.txt"));
157
158
let args = args.into_inner();
159
let images = images.into_inner();
160
let material_assets = material_assets.into_inner();
161
let mesh_assets = mesh_assets.into_inner();
162
163
let meshes = init_meshes(args, mesh_assets);
164
165
let material_textures = init_textures(args, images);
166
let materials = init_materials(args, &material_textures, material_assets);
167
168
// We're seeding the PRNG here to make this example deterministic for testing purposes.
169
// This isn't strictly required in practical use unless you need your app to be deterministic.
170
let mut material_rng = ChaCha8Rng::seed_from_u64(42);
171
match args.layout {
172
Layout::Sphere => {
173
// NOTE: This pattern is good for testing performance of culling as it provides roughly
174
// the same number of visible meshes regardless of the viewing angle.
175
const N_POINTS: usize = WIDTH * HEIGHT * 4;
176
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
177
let radius = WIDTH as f64 * 2.5;
178
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
179
for i in 0..N_POINTS {
180
let spherical_polar_theta_phi =
181
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
182
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
183
let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
184
commands
185
.spawn((
186
Mesh3d(mesh.clone()),
187
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
188
Transform::from_translation((radius * unit_sphere_p).as_vec3())
189
.looking_at(Vec3::ZERO, Vec3::Y)
190
.mul_transform(*transform),
191
))
192
.insert_if(NoFrustumCulling, || args.no_frustum_culling)
193
.insert_if(NoAutomaticBatching, || args.no_automatic_batching);
194
}
195
196
// camera
197
let mut camera = commands.spawn(Camera3d::default());
198
if args.no_indirect_drawing {
199
camera.insert(NoIndirectDrawing);
200
}
201
if args.no_cpu_culling {
202
camera.insert(NoCpuCulling);
203
}
204
205
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
206
commands.spawn((
207
Mesh3d(mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2)))),
208
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
209
Transform::from_scale(-Vec3::ONE),
210
NotShadowCaster,
211
));
212
}
213
Layout::Cube => {
214
// NOTE: This pattern is good for demonstrating that frustum culling is working correctly
215
// as the number of visible meshes rises and falls depending on the viewing angle.
216
let scale = 2.5;
217
for x in 0..WIDTH {
218
for y in 0..HEIGHT {
219
// introduce spaces to break any kind of moiré pattern
220
if x % 10 == 0 || y % 10 == 0 {
221
continue;
222
}
223
// cube
224
commands.spawn((
225
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
226
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
227
Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
228
));
229
commands.spawn((
230
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
231
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
232
Transform::from_xyz(
233
(x as f32) * scale,
234
HEIGHT as f32 * scale,
235
(y as f32) * scale,
236
),
237
));
238
commands.spawn((
239
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
240
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
241
Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
242
));
243
commands.spawn((
244
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
245
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
246
Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
247
));
248
}
249
}
250
// camera
251
let center = 0.5 * scale * Vec3::new(WIDTH as f32, HEIGHT as f32, WIDTH as f32);
252
commands.spawn((Camera3d::default(), Transform::from_translation(center)));
253
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
254
commands.spawn((
255
Mesh3d(mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center))),
256
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
257
Transform::from_scale(-Vec3::ONE).with_translation(center),
258
NotShadowCaster,
259
));
260
}
261
Layout::Dense => {
262
// NOTE: This pattern is good for demonstrating a dense configuration of cubes
263
// overlapping each other, all within the camera frustum.
264
let count = WIDTH * HEIGHT * 2;
265
let size = cbrt(count as f32).round();
266
let gap = 1.25;
267
268
let cubes = (0..count).map(move |i| {
269
let x = i as f32 % size;
270
let y = (i as f32 / size) % size;
271
let z = i as f32 / (size * size);
272
let pos = Vec3::new(x * gap, y * gap, z * gap);
273
(
274
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
275
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
276
Transform::from_translation(pos),
277
)
278
});
279
280
// camera
281
commands.spawn((
282
Camera3d::default(),
283
Transform::from_xyz(100.0, 90.0, 100.0)
284
.looking_at(Vec3::new(0.0, -10.0, 0.0), Vec3::Y),
285
));
286
287
commands.spawn_batch(cubes);
288
}
289
}
290
291
commands.spawn((
292
DirectionalLight {
293
shadow_maps_enabled: args.shadows,
294
..default()
295
},
296
Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
297
));
298
}
299
300
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
301
// We're seeding the PRNG here to make this example deterministic for testing purposes.
302
// This isn't strictly required in practical use unless you need your app to be deterministic.
303
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
304
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
305
.map(|i| {
306
if (i % 4) == 3 {
307
255
308
} else {
309
color_rng.random()
310
}
311
})
312
.collect();
313
color_bytes
314
.chunks(4)
315
.map(|pixel| {
316
images.add(Image::new_fill(
317
Extent3d::default(),
318
TextureDimension::D2,
319
pixel,
320
TextureFormat::Rgba8UnormSrgb,
321
RenderAssetUsages::RENDER_WORLD,
322
))
323
})
324
.collect()
325
}
326
327
fn init_materials(
328
args: &Args,
329
textures: &[Handle<Image>],
330
assets: &mut Assets<StandardMaterial>,
331
) -> Vec<Handle<StandardMaterial>> {
332
let capacity = if args.vary_material_data_per_instance {
333
match args.layout {
334
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
335
Layout::Sphere => WIDTH * HEIGHT * 4,
336
Layout::Dense => WIDTH * HEIGHT * 2,
337
}
338
} else {
339
args.material_texture_count
340
}
341
.max(1);
342
343
let mut materials = Vec::with_capacity(capacity);
344
materials.push(assets.add(StandardMaterial {
345
base_color: Color::WHITE,
346
base_color_texture: textures.first().cloned(),
347
..default()
348
}));
349
350
// We're seeding the PRNG here to make this example deterministic for testing purposes.
351
// This isn't strictly required in practical use unless you need your app to be deterministic.
352
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
353
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
354
materials.extend(
355
std::iter::repeat_with(|| {
356
assets.add(StandardMaterial {
357
base_color: Color::srgb_u8(
358
color_rng.random(),
359
color_rng.random(),
360
color_rng.random(),
361
),
362
base_color_texture: textures.choose(&mut texture_rng).cloned(),
363
..default()
364
})
365
})
366
.take(capacity - materials.len()),
367
);
368
369
materials
370
}
371
372
fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
373
let capacity = args.mesh_count.max(1);
374
375
// We're seeding the PRNG here to make this example deterministic for testing purposes.
376
// This isn't strictly required in practical use unless you need your app to be deterministic.
377
let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
378
let mut variant = 0;
379
std::iter::repeat_with(|| {
380
let radius = radius_rng.random_range(0.25f32..=0.75f32);
381
let (handle, transform) = match variant % 15 {
382
0 => (
383
assets.add(Cuboid {
384
half_size: Vec3::splat(radius),
385
}),
386
Transform::IDENTITY,
387
),
388
1 => (
389
assets.add(Capsule3d {
390
radius,
391
half_length: radius,
392
}),
393
Transform::IDENTITY,
394
),
395
2 => (
396
assets.add(Circle { radius }),
397
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
398
),
399
3 => {
400
let mut vertices = [Vec2::ZERO; 3];
401
let dtheta = std::f32::consts::TAU / 3.0;
402
for (i, vertex) in vertices.iter_mut().enumerate() {
403
let (s, c) = ops::sin_cos(i as f32 * dtheta);
404
*vertex = Vec2::new(c, s) * radius;
405
}
406
(
407
assets.add(Triangle2d { vertices }),
408
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
409
)
410
}
411
4 => (
412
assets.add(Rectangle {
413
half_size: Vec2::splat(radius),
414
}),
415
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
416
),
417
v if (5..=8).contains(&v) => (
418
assets.add(RegularPolygon {
419
circumcircle: Circle { radius },
420
sides: v,
421
}),
422
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
423
),
424
9 => (
425
assets.add(Cylinder {
426
radius,
427
half_height: radius,
428
}),
429
Transform::IDENTITY,
430
),
431
10 => (
432
assets.add(Ellipse {
433
half_size: Vec2::new(radius, 0.5 * radius),
434
}),
435
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
436
),
437
11 => (
438
assets.add(
439
Plane3d {
440
normal: Dir3::NEG_Z,
441
half_size: Vec2::splat(0.5),
442
}
443
.mesh()
444
.size(radius, radius),
445
),
446
Transform::IDENTITY,
447
),
448
12 => (assets.add(Sphere { radius }), Transform::IDENTITY),
449
13 => (
450
assets.add(Torus {
451
minor_radius: 0.5 * radius,
452
major_radius: radius,
453
}),
454
Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
455
),
456
14 => (
457
assets.add(Capsule2d {
458
radius,
459
half_length: radius,
460
}),
461
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
462
),
463
_ => unreachable!(),
464
};
465
variant += 1;
466
(handle, transform)
467
})
468
.take(capacity)
469
.collect()
470
}
471
472
// NOTE: This epsilon value is apparently optimal for optimizing for the average
473
// nearest-neighbor distance. See:
474
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
475
// for details.
476
const EPSILON: f64 = 0.36;
477
478
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
479
DVec2::new(
480
PI * 2. * (i as f64 / golden_ratio),
481
f64::acos(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)),
482
)
483
}
484
485
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
486
let (sin_theta, cos_theta) = p.x.sin_cos();
487
let (sin_phi, cos_phi) = p.y.sin_cos();
488
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
489
}
490
491
// System for rotating the camera
492
fn move_camera(
493
time: Res<Time>,
494
args: Res<Args>,
495
mut camera_transform: Single<&mut Transform, With<Camera>>,
496
) {
497
let delta = 0.15
498
* if args.benchmark {
499
1.0 / 60.0
500
} else {
501
time.delta_secs()
502
};
503
camera_transform.rotate_z(delta);
504
camera_transform.rotate_x(delta);
505
}
506
507
// System for printing the number of meshes on every tick of the timer
508
fn print_mesh_count(
509
time: Res<Time>,
510
mut timer: Local<PrintingTimer>,
511
sprites: Query<(&Mesh3d, &ViewVisibility)>,
512
) {
513
timer.tick(time.delta());
514
515
if timer.just_finished() {
516
info!(
517
"Meshes: {} - Visible Meshes {}",
518
sprites.iter().len(),
519
sprites.iter().filter(|(_, vis)| vis.get()).count(),
520
);
521
}
522
}
523
524
#[derive(Deref, DerefMut)]
525
struct PrintingTimer(Timer);
526
527
impl Default for PrintingTimer {
528
fn default() -> Self {
529
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
530
}
531
}
532
533
fn update_materials(mut materials: ResMut<Assets<StandardMaterial>>, time: Res<Time>) {
534
let elapsed = time.elapsed_secs();
535
for (i, (_, material)) in materials.iter_mut().enumerate() {
536
let hue = (elapsed + i as f32 * 0.005).rem_euclid(1.0);
537
// This is much faster than using base_color.set_hue(hue), and in a tight loop it shows.
538
let color = fast_hue_to_rgb(hue);
539
material.base_color = Color::linear_rgb(color.x, color.y, color.z);
540
}
541
}
542
543
fn rotate_cubes(
544
mut query: Query<&mut Transform, (With<Mesh3d>, Without<NotShadowCaster>)>,
545
time: Res<Time>,
546
) {
547
query.par_iter_mut().for_each(|mut transform| {
548
transform.rotate_y(10.0 * time.delta_secs());
549
});
550
}
551
552
#[inline]
553
fn fast_hue_to_rgb(hue: f32) -> Vec3 {
554
(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)
555
}
556
557