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