1use std::{f64::consts::PI, str::FromStr};
12
13use argh::FromArgs;
14use bevy::{
15 asset::RenderAssetUsages,
16 camera::visibility::{NoCpuCulling, NoFrustumCulling},
17 diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
18 light::NotShadowCaster,
19 math::{
20 ops::{cbrt, sqrt},
21 DVec2, DVec3,
22 },
23 mesh::MeshCompressionArgs,
24 post_process::motion_blur::MotionBlur,
25 prelude::*,
26 render::{
27 batching::NoAutomaticBatching,
28 render_resource::{Extent3d, TextureDimension, TextureFormat},
29 view::NoIndirectDrawing,
30 },
31 window::{PresentMode, WindowResolution},
32 winit::WinitSettings,
33};
34use chacha20::ChaCha8Rng;
35use rand::{seq::IndexedRandom, RngExt, SeedableRng};
36
37#[derive(FromArgs, Resource)]
38struct Args {
40 #[argh(option, default = "Layout::Sphere")]
42 layout: Layout,
43
44 #[argh(switch)]
46 benchmark: bool,
47
48 #[argh(switch)]
50 vary_material_data_per_instance: bool,
51
52 #[argh(option, default = "0")]
54 material_texture_count: usize,
55
56 #[argh(option, default = "1")]
58 mesh_count: usize,
59
60 #[argh(option, default = "1600000")]
62 instance_count: usize,
63
64 #[argh(switch)]
66 no_frustum_culling: bool,
67
68 #[argh(switch)]
70 no_automatic_batching: bool,
71
72 #[argh(switch)]
74 no_indirect_drawing: bool,
75
76 #[argh(switch)]
78 no_cpu_culling: bool,
79
80 #[argh(switch)]
82 shadows: bool,
83
84 #[argh(switch)]
86 rotate_cubes: bool,
87
88 #[argh(switch)]
90 animate_materials: bool,
91
92 #[argh(switch)]
94 motion_blur: bool,
95
96 #[argh(switch)]
98 mesh_compression: bool,
99}
100
101#[derive(Default, Clone, PartialEq)]
102enum Layout {
103 Cube,
104 #[default]
105 Sphere,
106 Dense,
107}
108
109impl FromStr for Layout {
110 type Err = String;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 match s {
114 "cube" => Ok(Self::Cube),
115 "sphere" => Ok(Self::Sphere),
116 "dense" => Ok(Self::Dense),
117 _ => Err(format!(
118 "Unknown layout value: '{s}', valid options: 'cube', 'sphere', 'dense'"
119 )),
120 }
121 }
122}
123
124fn main() {
125 #[cfg(not(target_arch = "wasm32"))]
127 let args: Args = argh::from_env();
128 #[cfg(target_arch = "wasm32")]
129 let args = Args::from_args(&[], &[]).unwrap();
130
131 let mut app = App::new();
132 app.add_plugins((
133 DefaultPlugins.set(WindowPlugin {
134 primary_window: Some(Window {
135 present_mode: PresentMode::AutoNoVsync,
136 resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
137 ..default()
138 }),
139 ..default()
140 }),
141 FrameTimeDiagnosticsPlugin::default(),
142 LogDiagnosticsPlugin::default(),
143 ))
144 .insert_resource(WinitSettings::continuous())
145 .add_systems(Startup, setup)
146 .add_systems(Update, print_mesh_count);
147
148 if args.layout != Layout::Dense {
149 app.add_systems(Update, move_camera);
150 }
151
152 if args.rotate_cubes {
153 app.add_systems(Update, rotate_cubes);
154 }
155
156 if args.animate_materials {
157 app.add_systems(Update, update_materials);
158 }
159
160 app.insert_resource(args).run();
161}
162
163const WIDTH: usize = 200;
164const HEIGHT: usize = 200;
165
166fn setup(
167 mut commands: Commands,
168 args: Res<Args>,
169 mesh_assets: ResMut<Assets<Mesh>>,
170 material_assets: ResMut<Assets<StandardMaterial>>,
171 images: ResMut<Assets<Image>>,
172) {
173 warn!(include_str!("warning_string.txt"));
174
175 let args = args.into_inner();
176 let images = images.into_inner();
177 let material_assets = material_assets.into_inner();
178 let mesh_assets = mesh_assets.into_inner();
179
180 let meshes = init_meshes(args, mesh_assets);
181
182 let material_textures = init_textures(args, images);
183 let materials = init_materials(args, &material_textures, material_assets);
184
185 let mut material_rng = ChaCha8Rng::seed_from_u64(42);
188 match args.layout {
189 Layout::Sphere => {
190 let n_points: usize = args.instance_count;
193 let radius = WIDTH as f64 * 2.5;
195 let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
196 for i in 0..n_points {
197 let spherical_polar_theta_phi =
198 fibonacci_spiral_on_sphere(golden_ratio, i, n_points);
199 let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
200 let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
201 commands
202 .spawn((
203 Mesh3d(mesh.clone()),
204 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
205 Transform::from_translation((radius * unit_sphere_p).as_vec3())
206 .looking_at(Vec3::ZERO, Vec3::Y)
207 .mul_transform(*transform),
208 ))
209 .insert_if(NoFrustumCulling, || args.no_frustum_culling)
210 .insert_if(NoAutomaticBatching, || args.no_automatic_batching)
211 .insert_if(NoCpuCulling, || args.no_cpu_culling);
212 }
213
214 let mut camera = commands.spawn(Camera3d::default());
216 if args.no_indirect_drawing {
217 camera.insert(NoIndirectDrawing);
218 }
219 if args.no_cpu_culling {
220 camera.insert(NoCpuCulling);
221 }
222 if args.motion_blur {
223 camera.insert((
224 MotionBlur {
225 shutter_angle: 3.0,
227 ..Default::default()
228 },
229 #[cfg(all(
231 feature = "webgl2",
232 target_arch = "wasm32",
233 not(feature = "webgpu")
234 ))]
235 Msaa::Off,
236 ));
237 }
238
239 commands.spawn((
241 Mesh3d(mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2)))),
242 MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
243 Transform::from_scale(-Vec3::ONE),
244 NotShadowCaster,
245 ));
246 }
247 Layout::Cube => {
248 let scale = 2.5;
251
252 let factor = (5.0 / 9.0) * sqrt(args.instance_count as f32)
262 / (sqrt(HEIGHT as f32) * sqrt(WIDTH as f32));
263 let dimensions = (vec2(WIDTH as f32, HEIGHT as f32) * factor)
264 .ceil()
265 .as_uvec2();
266
267 for x in 0..dimensions.x {
268 for y in 0..dimensions.y {
269 if x % 10 == 0 || y % 10 == 0 {
271 continue;
272 }
273 commands
275 .spawn((
276 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
277 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
278 Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
279 ))
280 .insert_if(NoCpuCulling, || args.no_cpu_culling);
281 commands
282 .spawn((
283 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
284 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
285 Transform::from_xyz(
286 (x as f32) * scale,
287 dimensions.y as f32 * scale,
288 (y as f32) * scale,
289 ),
290 ))
291 .insert_if(NoCpuCulling, || args.no_cpu_culling);
292 commands
293 .spawn((
294 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
295 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
296 Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
297 ))
298 .insert_if(NoCpuCulling, || args.no_cpu_culling);
299 commands
300 .spawn((
301 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
302 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
303 Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
304 ))
305 .insert_if(NoCpuCulling, || args.no_cpu_culling);
306 }
307 }
308 let center = 0.5
310 * scale
311 * Vec3::new(
312 dimensions.x as f32,
313 dimensions.y as f32,
314 dimensions.x as f32,
315 );
316 commands.spawn((Camera3d::default(), Transform::from_translation(center)));
317 commands.spawn((
319 Mesh3d(mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center))),
320 MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
321 Transform::from_scale(-Vec3::ONE).with_translation(center),
322 NotShadowCaster,
323 ));
324 }
325 Layout::Dense => {
326 let count = args.instance_count;
329 let size = cbrt(count as f32).round();
330 let gap = 1.25;
331
332 for i in 0..count {
333 let x = i as f32 % size;
334 let y = (i as f32 / size) % size;
335 let z = i as f32 / (size * size);
336 let pos = Vec3::new(x * gap, y * gap, z * gap);
337 commands
338 .spawn((
339 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
340 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
341 Transform::from_translation(pos),
342 ))
343 .insert_if(NoCpuCulling, || args.no_cpu_culling);
344 }
345
346 commands.spawn((
348 Camera3d::default(),
349 Transform::from_xyz(100.0, 90.0, 100.0)
350 .looking_at(Vec3::new(0.0, -10.0, 0.0), Vec3::Y),
351 ));
352 }
353 }
354
355 commands.spawn((
356 DirectionalLight {
357 shadow_maps_enabled: args.shadows,
358 ..default()
359 },
360 Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
361 ));
362}
363
364fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
365 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
368 let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
369 .map(|i| {
370 if (i % 4) == 3 {
371 255
372 } else {
373 color_rng.random()
374 }
375 })
376 .collect();
377 color_bytes
378 .chunks(4)
379 .map(|pixel| {
380 images.add(Image::new_fill(
381 Extent3d::default(),
382 TextureDimension::D2,
383 pixel,
384 TextureFormat::Rgba8UnormSrgb,
385 RenderAssetUsages::RENDER_WORLD,
386 ))
387 })
388 .collect()
389}
390
391fn init_materials(
392 args: &Args,
393 textures: &[Handle<Image>],
394 assets: &mut Assets<StandardMaterial>,
395) -> Vec<Handle<StandardMaterial>> {
396 let capacity = if args.vary_material_data_per_instance {
397 args.instance_count
398 } else {
399 args.material_texture_count
400 }
401 .max(1);
402
403 let mut materials = Vec::with_capacity(capacity);
404 materials.push(assets.add(StandardMaterial {
405 base_color: Color::WHITE,
406 base_color_texture: textures.first().cloned(),
407 ..default()
408 }));
409
410 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
413 let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
414 materials.extend(
415 std::iter::repeat_with(|| {
416 assets.add(StandardMaterial {
417 base_color: Color::srgb_u8(
418 color_rng.random(),
419 color_rng.random(),
420 color_rng.random(),
421 ),
422 base_color_texture: textures.choose(&mut texture_rng).cloned(),
423 ..default()
424 })
425 })
426 .take(capacity - materials.len()),
427 );
428
429 materials
430}
431
432fn compress_mesh(args: &Args, mesh: impl Into<Mesh>) -> Mesh {
433 if args.mesh_compression {
434 mesh.into()
435 .compressed_mesh(&MeshCompressionArgs::regular())
436 .unwrap()
437 } else {
438 mesh.into()
439 }
440}
441
442fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
443 let capacity = args.mesh_count.max(1);
444
445 let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
448 let mut variant = 0;
449 std::iter::repeat_with(|| {
450 let radius = radius_rng.random_range(0.25f32..=0.75f32);
451 let (handle, transform) = match variant % 15 {
452 0 => (
453 assets.add(compress_mesh(
454 args,
455 Cuboid {
456 half_size: Vec3::splat(radius),
457 },
458 )),
459 Transform::IDENTITY,
460 ),
461 1 => (
462 assets.add(compress_mesh(
463 args,
464 Capsule3d {
465 radius,
466 half_length: radius,
467 },
468 )),
469 Transform::IDENTITY,
470 ),
471 2 => (
472 assets.add(compress_mesh(args, Circle { radius })),
473 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
474 ),
475 3 => {
476 let mut vertices = [Vec2::ZERO; 3];
477 let dtheta = std::f32::consts::TAU / 3.0;
478 for (i, vertex) in vertices.iter_mut().enumerate() {
479 let (s, c) = ops::sin_cos(i as f32 * dtheta);
480 *vertex = Vec2::new(c, s) * radius;
481 }
482 (
483 assets.add(compress_mesh(args, Triangle2d { vertices })),
484 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
485 )
486 }
487 4 => (
488 assets.add(compress_mesh(
489 args,
490 Rectangle {
491 half_size: Vec2::splat(radius),
492 },
493 )),
494 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
495 ),
496 v if (5..=8).contains(&v) => (
497 assets.add(compress_mesh(
498 args,
499 RegularPolygon {
500 circumcircle: Circle { radius },
501 sides: v,
502 },
503 )),
504 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
505 ),
506 9 => (
507 assets.add(compress_mesh(
508 args,
509 Cylinder {
510 radius,
511 half_height: radius,
512 },
513 )),
514 Transform::IDENTITY,
515 ),
516 10 => (
517 assets.add(compress_mesh(
518 args,
519 Ellipse {
520 half_size: Vec2::new(radius, 0.5 * radius),
521 },
522 )),
523 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
524 ),
525 11 => (
526 assets.add(compress_mesh(
527 args,
528 Plane3d {
529 normal: Dir3::NEG_Z,
530 half_size: Vec2::splat(radius / 2.0),
531 },
532 )),
533 Transform::IDENTITY,
534 ),
535 12 => (
536 assets.add(compress_mesh(args, Sphere { radius })),
537 Transform::IDENTITY,
538 ),
539 13 => (
540 assets.add(compress_mesh(
541 args,
542 Torus {
543 minor_radius: 0.5 * radius,
544 major_radius: radius,
545 },
546 )),
547 Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
548 ),
549 14 => (
550 assets.add(compress_mesh(
551 args,
552 Capsule2d {
553 radius,
554 half_length: radius,
555 },
556 )),
557 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
558 ),
559 _ => unreachable!(),
560 };
561 variant += 1;
562 (handle, transform)
563 })
564 .take(capacity)
565 .collect()
566}
567
568const EPSILON: f64 = 0.36;
573
574fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
575 DVec2::new(
576 PI * 2. * (i as f64 / golden_ratio),
577 f64::acos(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)),
578 )
579}
580
581fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
582 let (sin_theta, cos_theta) = p.x.sin_cos();
583 let (sin_phi, cos_phi) = p.y.sin_cos();
584 DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
585}
586
587fn move_camera(
589 time: Res<Time>,
590 args: Res<Args>,
591 mut camera_transform: Single<&mut Transform, With<Camera>>,
592) {
593 let delta = 0.15
594 * if args.benchmark {
595 1.0 / 60.0
596 } else {
597 time.delta_secs()
598 };
599 camera_transform.rotate_z(delta);
600 camera_transform.rotate_x(delta);
601}
602
603fn print_mesh_count(
605 time: Res<Time>,
606 mut timer: Local<PrintingTimer>,
607 sprites: Query<(&Mesh3d, &ViewVisibility)>,
608) {
609 timer.tick(time.delta());
610
611 if timer.just_finished() {
612 info!(
613 "Meshes: {} - Visible Meshes {}",
614 sprites.iter().len(),
615 sprites.iter().filter(|(_, vis)| vis.get()).count(),
616 );
617 }
618}
619
620#[derive(Deref, DerefMut)]
621struct PrintingTimer(Timer);
622
623impl Default for PrintingTimer {
624 fn default() -> Self {
625 Self(Timer::from_seconds(1.0, TimerMode::Repeating))
626 }
627}
628
629fn update_materials(mut materials: ResMut<Assets<StandardMaterial>>, time: Res<Time>) {
630 let elapsed = time.elapsed_secs();
631 for (i, (_, material)) in materials.iter_mut().enumerate() {
632 let hue = (elapsed + i as f32 * 0.005).rem_euclid(1.0);
633 let color = fast_hue_to_rgb(hue);
635 material.base_color = Color::linear_rgb(color.x, color.y, color.z);
636 }
637}
638
639fn rotate_cubes(
640 mut query: Query<&mut Transform, (With<Mesh3d>, Without<NotShadowCaster>)>,
641 time: Res<Time>,
642) {
643 query.par_iter_mut().for_each(|mut transform| {
644 transform.rotate_y(10.0 * time.delta_secs());
645 });
646}
647
648#[inline]
649fn fast_hue_to_rgb(hue: f32) -> Vec3 {
650 (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)
651}