1use core::time::Duration;
6use std::str::FromStr;
7
8use argh::FromArgs;
9use 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};
18use chacha20::ChaCha8Rng;
19use rand::{seq::IndexedRandom, RngExt, SeedableRng};
20
21const CUBES_PER_SECOND: u32 = 10000;
22const GRAVITY: f32 = -9.8;
23const MAX_VELOCITY: f32 = 10.;
24const CUBE_SCALE: f32 = 1.0;
25const CUBE_TEXTURE_SIZE: usize = 256;
26const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
27const VOLUME_WIDTH: usize = 50;
28const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
29
30#[derive(Resource)]
31struct BevyCounter {
32 pub count: usize,
33 pub color: Color,
34}
35
36#[derive(Component)]
37struct Cube {
38 velocity: Vec3,
39}
40
41#[derive(FromArgs, Resource)]
42struct Args {
44 #[argh(switch)]
48 benchmark: bool,
49
50 #[argh(option, default = "0")]
52 per_wave: usize,
53
54 #[argh(option, default = "0")]
56 waves: usize,
57
58 #[argh(switch)]
60 vary_per_instance: bool,
61
62 #[argh(option, default = "1")]
64 material_texture_count: usize,
65
66 #[argh(option, default = "AlphaMode::Opaque")]
68 alpha_mode: AlphaMode,
69}
70
71#[derive(Default, Clone)]
72enum AlphaMode {
73 #[default]
74 Opaque,
75 Blend,
76 AlphaMask,
77}
78
79impl 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
94const FIXED_TIMESTEP: f32 = 0.2;
95
96fn main() {
97 #[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)]
141struct CubeScheduled {
142 waves: usize,
143 per_wave: usize,
144}
145
146fn 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)]
170struct 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)]
181struct StatsText;
182
183fn 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
297fn 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
331fn 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
348const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
349
350fn 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
407fn 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
414fn 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
429fn 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
450fn 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
456fn 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
481fn 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
504fn 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}