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 warn!(include_str!("warning_string.txt"));
193
194 let args = args.into_inner();
195 let images = images.into_inner();
196
197 let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
198 if args.material_texture_count > 0 {
199 textures.push(asset_server.load("branding/icon.png"));
200 }
201 init_textures(&mut textures, args, images);
202
203 let material_assets = material_assets.into_inner();
204 let materials = init_materials(args, &textures, material_assets);
205
206 let mut cube_resources = CubeResources {
207 _textures: textures,
208 materials,
209 cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
210 color_rng: ChaCha8Rng::seed_from_u64(42),
211 material_rng: ChaCha8Rng::seed_from_u64(12),
212 velocity_rng: ChaCha8Rng::seed_from_u64(97),
213 transform_rng: ChaCha8Rng::seed_from_u64(26),
214 };
215
216 let font = TextFont {
217 font_size: FontSize::Px(40.0),
218 ..Default::default()
219 };
220
221 commands.spawn((
222 Camera3d::default(),
223 Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
224 ));
225
226 commands.spawn((
227 DirectionalLight {
228 illuminance: 10000.0,
229 shadow_maps_enabled: false,
230 ..default()
231 },
232 Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
233 ));
234
235 commands.spawn((
236 Node {
237 position_type: PositionType::Absolute,
238 padding: UiRect::all(px(5)),
239 ..default()
240 },
241 BackgroundColor(Color::BLACK.with_alpha(0.75)),
242 GlobalZIndex(i32::MAX),
243 children![(
244 Text::default(),
245 StatsText,
246 children![
247 (
248 TextSpan::new("Cube Count: "),
249 font.clone(),
250 TextColor(LIME.into()),
251 ),
252 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
253 (
254 TextSpan::new("\nFPS (raw): "),
255 font.clone(),
256 TextColor(LIME.into()),
257 ),
258 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
259 (
260 TextSpan::new("\nFPS (SMA): "),
261 font.clone(),
262 TextColor(LIME.into()),
263 ),
264 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
265 (
266 TextSpan::new("\nFPS (EMA): "),
267 font.clone(),
268 TextColor(LIME.into()),
269 ),
270 (TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
271 ]
272 )],
273 ));
274
275 let mut scheduled = CubeScheduled {
276 per_wave: args.per_wave,
277 waves: args.waves,
278 };
279
280 if args.benchmark {
281 let counter = counter.into_inner();
282 for wave in (0..scheduled.waves).rev() {
283 spawn_cubes(
284 &mut commands,
285 args,
286 counter,
287 scheduled.per_wave,
288 &mut cube_resources,
289 Some(wave),
290 wave,
291 );
292 }
293 scheduled.waves = 0;
294 }
295 commands.insert_resource(cube_resources);
296 commands.insert_resource(scheduled);
297}
298
299fn mouse_handler(
300 mut commands: Commands,
301 args: Res<Args>,
302 time: Res<Time>,
303 mouse_button_input: Res<ButtonInput<MouseButton>>,
304 cube_resources: ResMut<CubeResources>,
305 mut counter: ResMut<BevyCounter>,
306 mut rng: Local<Option<ChaCha8Rng>>,
307 mut wave: Local<usize>,
308) {
309 if rng.is_none() {
310 *rng = Some(ChaCha8Rng::seed_from_u64(42));
311 }
312 let rng = rng.as_mut().unwrap();
313
314 if mouse_button_input.just_released(MouseButton::Left) {
315 counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
316 }
317
318 if mouse_button_input.pressed(MouseButton::Left) {
319 let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
320 spawn_cubes(
321 &mut commands,
322 args.into_inner(),
323 &mut counter,
324 spawn_count,
325 cube_resources.into_inner(),
326 None,
327 *wave,
328 );
329 *wave += 1;
330 }
331}
332
333fn cube_velocity_transform(
334 mut translation: Vec3,
335 velocity_rng: &mut ChaCha8Rng,
336 waves: Option<usize>,
337 dt: f32,
338) -> (Transform, Vec3) {
339 let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
340
341 if let Some(waves) = waves {
342 for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
343 step_movement(&mut translation, &mut velocity, dt);
344 handle_collision(&translation, &mut velocity);
345 }
346 }
347 (Transform::from_translation(translation), velocity)
348}
349
350const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
351
352fn spawn_cubes(
353 commands: &mut Commands,
354 args: &Args,
355 counter: &mut BevyCounter,
356 spawn_count: usize,
357 cube_resources: &mut CubeResources,
358 waves_to_simulate: Option<usize>,
359 wave: usize,
360) {
361 let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
362
363 let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
364 let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
365
366 let batch = (0..spawn_count)
367 .map(|_| {
368 let spawn_pos = Vec3::new(
369 (cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
370 spawn_y,
371 spawn_z,
372 );
373
374 let (transform, velocity) = cube_velocity_transform(
375 spawn_pos,
376 &mut cube_resources.velocity_rng,
377 waves_to_simulate,
378 FIXED_DELTA_TIME,
379 );
380
381 let material = if args.vary_per_instance {
382 cube_resources
383 .materials
384 .choose(&mut cube_resources.material_rng)
385 .unwrap()
386 .clone()
387 } else {
388 batch_material.clone()
389 };
390
391 (
392 Mesh3d(cube_resources.cube_mesh.clone()),
393 MeshMaterial3d(material),
394 transform,
395 Cube { velocity },
396 )
397 })
398 .collect::<Vec<_>>();
399 commands.spawn_batch(batch);
400
401 counter.count += spawn_count;
402 counter.color = Color::linear_rgb(
403 cube_resources.color_rng.random(),
404 cube_resources.color_rng.random(),
405 cube_resources.color_rng.random(),
406 );
407}
408
409fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
410 translation.x += velocity.x * dt;
411 translation.y += velocity.y * dt;
412 translation.z += velocity.z * dt;
413 velocity.y += GRAVITY * dt;
414}
415
416fn movement_system(
417 args: Res<Args>,
418 time: Res<Time>,
419 mut cube_query: Query<(&mut Cube, &mut Transform)>,
420) {
421 let dt = if args.benchmark {
422 FIXED_DELTA_TIME
423 } else {
424 time.delta_secs()
425 };
426 for (mut cube, mut transform) in &mut cube_query {
427 step_movement(&mut transform.translation, &mut cube.velocity, dt);
428 }
429}
430
431fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
432 if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
433 || (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
434 {
435 velocity.x = -velocity.x;
436 }
437 if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
438 || (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
439 {
440 velocity.z = -velocity.z;
441 }
442
443 let velocity_y = velocity.y;
444 if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
445 velocity.y = -velocity_y;
446 }
447 if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
448 velocity.y = 0.0;
449 }
450}
451
452fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
453 cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
454 handle_collision(&transform.translation, &mut cube.velocity);
455 });
456}
457
458fn counter_system(
459 diagnostics: Res<DiagnosticsStore>,
460 counter: Res<BevyCounter>,
461 query: Single<Entity, With<StatsText>>,
462 mut writer: TextUiWriter,
463) {
464 let text = *query;
465
466 if counter.is_changed() {
467 *writer.text(text, 2) = counter.count.to_string();
468 }
469
470 if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
471 if let Some(raw) = fps.value() {
472 *writer.text(text, 4) = format!("{raw:.2}");
473 }
474 if let Some(sma) = fps.average() {
475 *writer.text(text, 6) = format!("{sma:.2}");
476 }
477 if let Some(ema) = fps.smoothed() {
478 *writer.text(text, 8) = format!("{ema:.2}");
479 }
480 };
481}
482
483fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
484 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
485 while textures.len() < args.material_texture_count {
486 let pixel = [
487 color_rng.random(),
488 color_rng.random(),
489 color_rng.random(),
490 255,
491 ];
492 textures.push(images.add(Image::new_fill(
493 Extent3d {
494 width: CUBE_TEXTURE_SIZE as u32,
495 height: CUBE_TEXTURE_SIZE as u32,
496 depth_or_array_layers: 1,
497 },
498 TextureDimension::D2,
499 &pixel,
500 TextureFormat::Rgba8UnormSrgb,
501 RenderAssetUsages::RENDER_WORLD,
502 )));
503 }
504}
505
506fn init_materials(
507 args: &Args,
508 textures: &[Handle<Image>],
509 assets: &mut Assets<StandardMaterial>,
510) -> Vec<Handle<StandardMaterial>> {
511 let mut capacity = if args.vary_per_instance {
512 args.per_wave * args.waves
513 } else {
514 args.material_texture_count.max(args.waves)
515 };
516 if !args.benchmark {
517 capacity = capacity.max(256);
518 }
519 capacity = capacity.max(1);
520
521 let alpha_mode = match args.alpha_mode {
522 AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
523 AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
524 AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
525 };
526
527 let mut materials = Vec::with_capacity(capacity);
528 materials.push(assets.add(StandardMaterial {
529 base_color: Color::WHITE,
530 base_color_texture: textures.first().cloned(),
531 alpha_mode,
532 ..default()
533 }));
534
535 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
536 let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
537 materials.extend(
538 std::iter::repeat_with(|| {
539 assets.add(StandardMaterial {
540 base_color: Color::linear_rgb(
541 color_rng.random(),
542 color_rng.random(),
543 color_rng.random(),
544 ),
545 base_color_texture: textures.choose(&mut texture_rng).cloned(),
546 alpha_mode,
547 ..default()
548 })
549 })
550 .take(capacity - materials.len()),
551 );
552
553 materials
554}