Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/bevymark_3d.rs
9356 views
1
//! This example provides a 3D benchmark.
2
//!
3
//! Usage: spawn more entities by clicking with the left mouse button.
4
5
use core::time::Duration;
6
use std::str::FromStr;
7
8
use argh::FromArgs;
9
use bevy::{
10
asset::RenderAssetUsages,
11
color::palettes::basic::*,
12
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
13
prelude::*,
14
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
15
window::{PresentMode, WindowResolution},
16
winit::WinitSettings,
17
};
18
use rand::{seq::IndexedRandom, Rng, SeedableRng};
19
use rand_chacha::ChaCha8Rng;
20
21
const CUBES_PER_SECOND: u32 = 10000;
22
const GRAVITY: f32 = -9.8;
23
const MAX_VELOCITY: f32 = 10.;
24
const CUBE_SCALE: f32 = 1.0;
25
const CUBE_TEXTURE_SIZE: usize = 256;
26
const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
27
const VOLUME_WIDTH: usize = 50;
28
const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
29
30
#[derive(Resource)]
31
struct BevyCounter {
32
pub count: usize,
33
pub color: Color,
34
}
35
36
#[derive(Component)]
37
struct Cube {
38
velocity: Vec3,
39
}
40
41
#[derive(FromArgs, Resource)]
42
/// `bevymark_3d` cube stress test
43
struct Args {
44
/// whether to step animations by a fixed amount such that each frame is the same across runs.
45
/// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
46
/// load.
47
#[argh(switch)]
48
benchmark: bool,
49
50
/// how many cubes to spawn per wave.
51
#[argh(option, default = "0")]
52
per_wave: usize,
53
54
/// the number of waves to spawn.
55
#[argh(option, default = "0")]
56
waves: usize,
57
58
/// whether to vary the material data in each instance.
59
#[argh(switch)]
60
vary_per_instance: bool,
61
62
/// the number of different textures from which to randomly select the material color. 0 means no textures.
63
#[argh(option, default = "1")]
64
material_texture_count: usize,
65
66
/// the alpha mode used to spawn the cubes
67
#[argh(option, default = "AlphaMode::Opaque")]
68
alpha_mode: AlphaMode,
69
}
70
71
#[derive(Default, Clone)]
72
enum AlphaMode {
73
#[default]
74
Opaque,
75
Blend,
76
AlphaMask,
77
}
78
79
impl FromStr for AlphaMode {
80
type Err = String;
81
82
fn from_str(s: &str) -> Result<Self, Self::Err> {
83
match s {
84
"opaque" => Ok(Self::Opaque),
85
"blend" => Ok(Self::Blend),
86
"alpha_mask" => Ok(Self::AlphaMask),
87
_ => Err(format!(
88
"Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
89
)),
90
}
91
}
92
}
93
94
const FIXED_TIMESTEP: f32 = 0.2;
95
96
fn main() {
97
// `from_env` panics on the web
98
#[cfg(not(target_arch = "wasm32"))]
99
let args: Args = argh::from_env();
100
#[cfg(target_arch = "wasm32")]
101
let args = Args::from_args(&[], &[]).unwrap();
102
103
App::new()
104
.add_plugins((
105
DefaultPlugins.set(WindowPlugin {
106
primary_window: Some(Window {
107
title: "BevyMark 3D".into(),
108
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
109
present_mode: PresentMode::AutoNoVsync,
110
..default()
111
}),
112
..default()
113
}),
114
FrameTimeDiagnosticsPlugin::default(),
115
LogDiagnosticsPlugin::default(),
116
))
117
.insert_resource(WinitSettings::continuous())
118
.insert_resource(args)
119
.insert_resource(BevyCounter {
120
count: 0,
121
color: Color::WHITE,
122
})
123
.add_systems(Startup, setup)
124
.add_systems(FixedUpdate, scheduled_spawner)
125
.add_systems(
126
Update,
127
(
128
mouse_handler,
129
movement_system,
130
collision_system,
131
counter_system,
132
),
133
)
134
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
135
FIXED_TIMESTEP,
136
)))
137
.run();
138
}
139
140
#[derive(Resource)]
141
struct CubeScheduled {
142
waves: usize,
143
per_wave: usize,
144
}
145
146
fn scheduled_spawner(
147
mut commands: Commands,
148
args: Res<Args>,
149
mut scheduled: ResMut<CubeScheduled>,
150
mut counter: ResMut<BevyCounter>,
151
cube_resources: ResMut<CubeResources>,
152
) {
153
if scheduled.waves > 0 {
154
let cube_resources = cube_resources.into_inner();
155
spawn_cubes(
156
&mut commands,
157
args.into_inner(),
158
&mut counter,
159
scheduled.per_wave,
160
cube_resources,
161
None,
162
scheduled.waves - 1,
163
);
164
165
scheduled.waves -= 1;
166
}
167
}
168
169
#[derive(Resource)]
170
struct CubeResources {
171
_textures: Vec<Handle<Image>>,
172
materials: Vec<Handle<StandardMaterial>>,
173
cube_mesh: Handle<Mesh>,
174
color_rng: ChaCha8Rng,
175
material_rng: ChaCha8Rng,
176
velocity_rng: ChaCha8Rng,
177
transform_rng: ChaCha8Rng,
178
}
179
180
#[derive(Component)]
181
struct StatsText;
182
183
fn setup(
184
mut commands: Commands,
185
args: Res<Args>,
186
asset_server: Res<AssetServer>,
187
mut meshes: ResMut<Assets<Mesh>>,
188
material_assets: ResMut<Assets<StandardMaterial>>,
189
images: ResMut<Assets<Image>>,
190
counter: ResMut<BevyCounter>,
191
) {
192
let args = args.into_inner();
193
let images = images.into_inner();
194
195
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
196
if args.material_texture_count > 0 {
197
textures.push(asset_server.load("branding/icon.png"));
198
}
199
init_textures(&mut textures, args, images);
200
201
let material_assets = material_assets.into_inner();
202
let materials = init_materials(args, &textures, material_assets);
203
204
let mut cube_resources = CubeResources {
205
_textures: textures,
206
materials,
207
cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
208
color_rng: ChaCha8Rng::seed_from_u64(42),
209
material_rng: ChaCha8Rng::seed_from_u64(12),
210
velocity_rng: ChaCha8Rng::seed_from_u64(97),
211
transform_rng: ChaCha8Rng::seed_from_u64(26),
212
};
213
214
let font = TextFont {
215
font_size: FontSize::Px(40.0),
216
..Default::default()
217
};
218
219
commands.spawn((
220
Camera3d::default(),
221
Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
222
));
223
224
commands.spawn((
225
DirectionalLight {
226
illuminance: 10000.0,
227
shadow_maps_enabled: false,
228
..default()
229
},
230
Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
231
));
232
233
commands.spawn((
234
Node {
235
position_type: PositionType::Absolute,
236
padding: UiRect::all(px(5)),
237
..default()
238
},
239
BackgroundColor(Color::BLACK.with_alpha(0.75)),
240
GlobalZIndex(i32::MAX),
241
children![(
242
Text::default(),
243
StatsText,
244
children![
245
(
246
TextSpan::new("Cube Count: "),
247
font.clone(),
248
TextColor(LIME.into()),
249
),
250
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
251
(
252
TextSpan::new("\nFPS (raw): "),
253
font.clone(),
254
TextColor(LIME.into()),
255
),
256
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
257
(
258
TextSpan::new("\nFPS (SMA): "),
259
font.clone(),
260
TextColor(LIME.into()),
261
),
262
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
263
(
264
TextSpan::new("\nFPS (EMA): "),
265
font.clone(),
266
TextColor(LIME.into()),
267
),
268
(TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
269
]
270
)],
271
));
272
273
let mut scheduled = CubeScheduled {
274
per_wave: args.per_wave,
275
waves: args.waves,
276
};
277
278
if args.benchmark {
279
let counter = counter.into_inner();
280
for wave in (0..scheduled.waves).rev() {
281
spawn_cubes(
282
&mut commands,
283
args,
284
counter,
285
scheduled.per_wave,
286
&mut cube_resources,
287
Some(wave),
288
wave,
289
);
290
}
291
scheduled.waves = 0;
292
}
293
commands.insert_resource(cube_resources);
294
commands.insert_resource(scheduled);
295
}
296
297
fn mouse_handler(
298
mut commands: Commands,
299
args: Res<Args>,
300
time: Res<Time>,
301
mouse_button_input: Res<ButtonInput<MouseButton>>,
302
cube_resources: ResMut<CubeResources>,
303
mut counter: ResMut<BevyCounter>,
304
mut rng: Local<Option<ChaCha8Rng>>,
305
mut wave: Local<usize>,
306
) {
307
if rng.is_none() {
308
*rng = Some(ChaCha8Rng::seed_from_u64(42));
309
}
310
let rng = rng.as_mut().unwrap();
311
312
if mouse_button_input.just_released(MouseButton::Left) {
313
counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
314
}
315
316
if mouse_button_input.pressed(MouseButton::Left) {
317
let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
318
spawn_cubes(
319
&mut commands,
320
args.into_inner(),
321
&mut counter,
322
spawn_count,
323
cube_resources.into_inner(),
324
None,
325
*wave,
326
);
327
*wave += 1;
328
}
329
}
330
331
fn cube_velocity_transform(
332
mut translation: Vec3,
333
velocity_rng: &mut ChaCha8Rng,
334
waves: Option<usize>,
335
dt: f32,
336
) -> (Transform, Vec3) {
337
let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
338
339
if let Some(waves) = waves {
340
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
341
step_movement(&mut translation, &mut velocity, dt);
342
handle_collision(&translation, &mut velocity);
343
}
344
}
345
(Transform::from_translation(translation), velocity)
346
}
347
348
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
349
350
fn spawn_cubes(
351
commands: &mut Commands,
352
args: &Args,
353
counter: &mut BevyCounter,
354
spawn_count: usize,
355
cube_resources: &mut CubeResources,
356
waves_to_simulate: Option<usize>,
357
wave: usize,
358
) {
359
let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
360
361
let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
362
let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
363
364
let batch = (0..spawn_count)
365
.map(|_| {
366
let spawn_pos = Vec3::new(
367
(cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
368
spawn_y,
369
spawn_z,
370
);
371
372
let (transform, velocity) = cube_velocity_transform(
373
spawn_pos,
374
&mut cube_resources.velocity_rng,
375
waves_to_simulate,
376
FIXED_DELTA_TIME,
377
);
378
379
let material = if args.vary_per_instance {
380
cube_resources
381
.materials
382
.choose(&mut cube_resources.material_rng)
383
.unwrap()
384
.clone()
385
} else {
386
batch_material.clone()
387
};
388
389
(
390
Mesh3d(cube_resources.cube_mesh.clone()),
391
MeshMaterial3d(material),
392
transform,
393
Cube { velocity },
394
)
395
})
396
.collect::<Vec<_>>();
397
commands.spawn_batch(batch);
398
399
counter.count += spawn_count;
400
counter.color = Color::linear_rgb(
401
cube_resources.color_rng.random(),
402
cube_resources.color_rng.random(),
403
cube_resources.color_rng.random(),
404
);
405
}
406
407
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
408
translation.x += velocity.x * dt;
409
translation.y += velocity.y * dt;
410
translation.z += velocity.z * dt;
411
velocity.y += GRAVITY * dt;
412
}
413
414
fn movement_system(
415
args: Res<Args>,
416
time: Res<Time>,
417
mut cube_query: Query<(&mut Cube, &mut Transform)>,
418
) {
419
let dt = if args.benchmark {
420
FIXED_DELTA_TIME
421
} else {
422
time.delta_secs()
423
};
424
for (mut cube, mut transform) in &mut cube_query {
425
step_movement(&mut transform.translation, &mut cube.velocity, dt);
426
}
427
}
428
429
fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
430
if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
431
|| (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
432
{
433
velocity.x = -velocity.x;
434
}
435
if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
436
|| (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
437
{
438
velocity.z = -velocity.z;
439
}
440
441
let velocity_y = velocity.y;
442
if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
443
velocity.y = -velocity_y;
444
}
445
if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
446
velocity.y = 0.0;
447
}
448
}
449
450
fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
451
cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
452
handle_collision(&transform.translation, &mut cube.velocity);
453
});
454
}
455
456
fn counter_system(
457
diagnostics: Res<DiagnosticsStore>,
458
counter: Res<BevyCounter>,
459
query: Single<Entity, With<StatsText>>,
460
mut writer: TextUiWriter,
461
) {
462
let text = *query;
463
464
if counter.is_changed() {
465
*writer.text(text, 2) = counter.count.to_string();
466
}
467
468
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
469
if let Some(raw) = fps.value() {
470
*writer.text(text, 4) = format!("{raw:.2}");
471
}
472
if let Some(sma) = fps.average() {
473
*writer.text(text, 6) = format!("{sma:.2}");
474
}
475
if let Some(ema) = fps.smoothed() {
476
*writer.text(text, 8) = format!("{ema:.2}");
477
}
478
};
479
}
480
481
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
482
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
483
while textures.len() < args.material_texture_count {
484
let pixel = [
485
color_rng.random(),
486
color_rng.random(),
487
color_rng.random(),
488
255,
489
];
490
textures.push(images.add(Image::new_fill(
491
Extent3d {
492
width: CUBE_TEXTURE_SIZE as u32,
493
height: CUBE_TEXTURE_SIZE as u32,
494
depth_or_array_layers: 1,
495
},
496
TextureDimension::D2,
497
&pixel,
498
TextureFormat::Rgba8UnormSrgb,
499
RenderAssetUsages::RENDER_WORLD,
500
)));
501
}
502
}
503
504
fn init_materials(
505
args: &Args,
506
textures: &[Handle<Image>],
507
assets: &mut Assets<StandardMaterial>,
508
) -> Vec<Handle<StandardMaterial>> {
509
let mut capacity = if args.vary_per_instance {
510
args.per_wave * args.waves
511
} else {
512
args.material_texture_count.max(args.waves)
513
};
514
if !args.benchmark {
515
capacity = capacity.max(256);
516
}
517
capacity = capacity.max(1);
518
519
let alpha_mode = match args.alpha_mode {
520
AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
521
AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
522
AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
523
};
524
525
let mut materials = Vec::with_capacity(capacity);
526
materials.push(assets.add(StandardMaterial {
527
base_color: Color::WHITE,
528
base_color_texture: textures.first().cloned(),
529
alpha_mode,
530
..default()
531
}));
532
533
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
534
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
535
materials.extend(
536
std::iter::repeat_with(|| {
537
assets.add(StandardMaterial {
538
base_color: Color::linear_rgb(
539
color_rng.random(),
540
color_rng.random(),
541
color_rng.random(),
542
),
543
base_color_texture: textures.choose(&mut texture_rng).cloned(),
544
alpha_mode,
545
..default()
546
})
547
})
548
.take(capacity - materials.len()),
549
);
550
551
materials
552
}
553
554