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::{DVec2, DVec3},
20 prelude::*,
21 render::{
22 batching::NoAutomaticBatching,
23 render_resource::{Extent3d, TextureDimension, TextureFormat},
24 view::NoIndirectDrawing,
25 },
26 window::{PresentMode, WindowResolution},
27 winit::{UpdateMode, WinitSettings},
28};
29use rand::{seq::IndexedRandom, Rng, SeedableRng};
30use rand_chacha::ChaCha8Rng;
31
32#[derive(FromArgs, Resource)]
33struct Args {
35 #[argh(option, default = "Layout::Sphere")]
37 layout: Layout,
38
39 #[argh(switch)]
41 benchmark: bool,
42
43 #[argh(switch)]
45 vary_material_data_per_instance: bool,
46
47 #[argh(option, default = "0")]
49 material_texture_count: usize,
50
51 #[argh(option, default = "1")]
53 mesh_count: usize,
54
55 #[argh(switch)]
57 no_frustum_culling: bool,
58
59 #[argh(switch)]
61 no_automatic_batching: bool,
62
63 #[argh(switch)]
65 no_indirect_drawing: bool,
66
67 #[argh(switch)]
69 no_cpu_culling: bool,
70
71 #[argh(switch)]
73 shadows: bool,
74
75 #[argh(switch)]
77 animate_materials: bool,
78}
79
80#[derive(Default, Clone)]
81enum Layout {
82 Cube,
83 #[default]
84 Sphere,
85}
86
87impl FromStr for Layout {
88 type Err = String;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 match s {
92 "cube" => Ok(Self::Cube),
93 "sphere" => Ok(Self::Sphere),
94 _ => Err(format!(
95 "Unknown layout value: '{s}', valid options: 'cube', 'sphere'"
96 )),
97 }
98 }
99}
100
101fn main() {
102 #[cfg(not(target_arch = "wasm32"))]
104 let args: Args = argh::from_env();
105 #[cfg(target_arch = "wasm32")]
106 let args = Args::from_args(&[], &[]).unwrap();
107
108 let mut app = App::new();
109 app.add_plugins((
110 DefaultPlugins.set(WindowPlugin {
111 primary_window: Some(Window {
112 present_mode: PresentMode::AutoNoVsync,
113 resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
114 ..default()
115 }),
116 ..default()
117 }),
118 FrameTimeDiagnosticsPlugin::default(),
119 LogDiagnosticsPlugin::default(),
120 ))
121 .insert_resource(WinitSettings {
122 focused_mode: UpdateMode::Continuous,
123 unfocused_mode: UpdateMode::Continuous,
124 })
125 .add_systems(Startup, setup)
126 .add_systems(Update, (move_camera, print_mesh_count));
127
128 if args.animate_materials {
129 app.add_systems(Update, update_materials);
130 }
131
132 app.insert_resource(args).run();
133}
134
135const WIDTH: usize = 200;
136const HEIGHT: usize = 200;
137
138fn setup(
139 mut commands: Commands,
140 args: Res<Args>,
141 mesh_assets: ResMut<Assets<Mesh>>,
142 material_assets: ResMut<Assets<StandardMaterial>>,
143 images: ResMut<Assets<Image>>,
144) {
145 warn!(include_str!("warning_string.txt"));
146
147 let args = args.into_inner();
148 let images = images.into_inner();
149 let material_assets = material_assets.into_inner();
150 let mesh_assets = mesh_assets.into_inner();
151
152 let meshes = init_meshes(args, mesh_assets);
153
154 let material_textures = init_textures(args, images);
155 let materials = init_materials(args, &material_textures, material_assets);
156
157 let mut material_rng = ChaCha8Rng::seed_from_u64(42);
160 match args.layout {
161 Layout::Sphere => {
162 const N_POINTS: usize = WIDTH * HEIGHT * 4;
165 let radius = WIDTH as f64 * 2.5;
167 let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
168 for i in 0..N_POINTS {
169 let spherical_polar_theta_phi =
170 fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
171 let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
172 let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
173 commands
174 .spawn((
175 Mesh3d(mesh.clone()),
176 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
177 Transform::from_translation((radius * unit_sphere_p).as_vec3())
178 .looking_at(Vec3::ZERO, Vec3::Y)
179 .mul_transform(*transform),
180 ))
181 .insert_if(NoFrustumCulling, || args.no_frustum_culling)
182 .insert_if(NoAutomaticBatching, || args.no_automatic_batching);
183 }
184
185 let mut camera = commands.spawn(Camera3d::default());
187 if args.no_indirect_drawing {
188 camera.insert(NoIndirectDrawing);
189 }
190 if args.no_cpu_culling {
191 camera.insert(NoCpuCulling);
192 }
193
194 commands.spawn((
196 Mesh3d(mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2)))),
197 MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
198 Transform::from_scale(-Vec3::ONE),
199 NotShadowCaster,
200 ));
201 }
202 _ => {
203 let scale = 2.5;
206 for x in 0..WIDTH {
207 for y in 0..HEIGHT {
208 if x % 10 == 0 || y % 10 == 0 {
210 continue;
211 }
212 commands.spawn((
214 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
215 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
216 Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
217 ));
218 commands.spawn((
219 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
220 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
221 Transform::from_xyz(
222 (x as f32) * scale,
223 HEIGHT as f32 * scale,
224 (y as f32) * scale,
225 ),
226 ));
227 commands.spawn((
228 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
229 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
230 Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
231 ));
232 commands.spawn((
233 Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
234 MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
235 Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
236 ));
237 }
238 }
239 let center = 0.5 * scale * Vec3::new(WIDTH as f32, HEIGHT as f32, WIDTH as f32);
241 commands.spawn((Camera3d::default(), Transform::from_translation(center)));
242 commands.spawn((
244 Mesh3d(mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center))),
245 MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
246 Transform::from_scale(-Vec3::ONE).with_translation(center),
247 NotShadowCaster,
248 ));
249 }
250 }
251
252 commands.spawn((
253 DirectionalLight {
254 shadows_enabled: args.shadows,
255 ..default()
256 },
257 Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
258 ));
259}
260
261fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
262 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
265 let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
266 .map(|i| {
267 if (i % 4) == 3 {
268 255
269 } else {
270 color_rng.random()
271 }
272 })
273 .collect();
274 color_bytes
275 .chunks(4)
276 .map(|pixel| {
277 images.add(Image::new_fill(
278 Extent3d::default(),
279 TextureDimension::D2,
280 pixel,
281 TextureFormat::Rgba8UnormSrgb,
282 RenderAssetUsages::RENDER_WORLD,
283 ))
284 })
285 .collect()
286}
287
288fn init_materials(
289 args: &Args,
290 textures: &[Handle<Image>],
291 assets: &mut Assets<StandardMaterial>,
292) -> Vec<Handle<StandardMaterial>> {
293 let capacity = if args.vary_material_data_per_instance {
294 match args.layout {
295 Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
296 Layout::Sphere => WIDTH * HEIGHT * 4,
297 }
298 } else {
299 args.material_texture_count
300 }
301 .max(1);
302
303 let mut materials = Vec::with_capacity(capacity);
304 materials.push(assets.add(StandardMaterial {
305 base_color: Color::WHITE,
306 base_color_texture: textures.first().cloned(),
307 ..default()
308 }));
309
310 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
313 let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
314 materials.extend(
315 std::iter::repeat_with(|| {
316 assets.add(StandardMaterial {
317 base_color: Color::srgb_u8(
318 color_rng.random(),
319 color_rng.random(),
320 color_rng.random(),
321 ),
322 base_color_texture: textures.choose(&mut texture_rng).cloned(),
323 ..default()
324 })
325 })
326 .take(capacity - materials.len()),
327 );
328
329 materials
330}
331
332fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
333 let capacity = args.mesh_count.max(1);
334
335 let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
338 let mut variant = 0;
339 std::iter::repeat_with(|| {
340 let radius = radius_rng.random_range(0.25f32..=0.75f32);
341 let (handle, transform) = match variant % 15 {
342 0 => (
343 assets.add(Cuboid {
344 half_size: Vec3::splat(radius),
345 }),
346 Transform::IDENTITY,
347 ),
348 1 => (
349 assets.add(Capsule3d {
350 radius,
351 half_length: radius,
352 }),
353 Transform::IDENTITY,
354 ),
355 2 => (
356 assets.add(Circle { radius }),
357 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
358 ),
359 3 => {
360 let mut vertices = [Vec2::ZERO; 3];
361 let dtheta = std::f32::consts::TAU / 3.0;
362 for (i, vertex) in vertices.iter_mut().enumerate() {
363 let (s, c) = ops::sin_cos(i as f32 * dtheta);
364 *vertex = Vec2::new(c, s) * radius;
365 }
366 (
367 assets.add(Triangle2d { vertices }),
368 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
369 )
370 }
371 4 => (
372 assets.add(Rectangle {
373 half_size: Vec2::splat(radius),
374 }),
375 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
376 ),
377 v if (5..=8).contains(&v) => (
378 assets.add(RegularPolygon {
379 circumcircle: Circle { radius },
380 sides: v,
381 }),
382 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
383 ),
384 9 => (
385 assets.add(Cylinder {
386 radius,
387 half_height: radius,
388 }),
389 Transform::IDENTITY,
390 ),
391 10 => (
392 assets.add(Ellipse {
393 half_size: Vec2::new(radius, 0.5 * radius),
394 }),
395 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
396 ),
397 11 => (
398 assets.add(
399 Plane3d {
400 normal: Dir3::NEG_Z,
401 half_size: Vec2::splat(0.5),
402 }
403 .mesh()
404 .size(radius, radius),
405 ),
406 Transform::IDENTITY,
407 ),
408 12 => (assets.add(Sphere { radius }), Transform::IDENTITY),
409 13 => (
410 assets.add(Torus {
411 minor_radius: 0.5 * radius,
412 major_radius: radius,
413 }),
414 Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
415 ),
416 14 => (
417 assets.add(Capsule2d {
418 radius,
419 half_length: radius,
420 }),
421 Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
422 ),
423 _ => unreachable!(),
424 };
425 variant += 1;
426 (handle, transform)
427 })
428 .take(capacity)
429 .collect()
430}
431
432const EPSILON: f64 = 0.36;
437
438fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
439 DVec2::new(
440 PI * 2. * (i as f64 / golden_ratio),
441 f64::acos(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)),
442 )
443}
444
445fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
446 let (sin_theta, cos_theta) = p.x.sin_cos();
447 let (sin_phi, cos_phi) = p.y.sin_cos();
448 DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
449}
450
451fn move_camera(
453 time: Res<Time>,
454 args: Res<Args>,
455 mut camera_transform: Single<&mut Transform, With<Camera>>,
456) {
457 let delta = 0.15
458 * if args.benchmark {
459 1.0 / 60.0
460 } else {
461 time.delta_secs()
462 };
463 camera_transform.rotate_z(delta);
464 camera_transform.rotate_x(delta);
465}
466
467fn print_mesh_count(
469 time: Res<Time>,
470 mut timer: Local<PrintingTimer>,
471 sprites: Query<(&Mesh3d, &ViewVisibility)>,
472) {
473 timer.tick(time.delta());
474
475 if timer.just_finished() {
476 info!(
477 "Meshes: {} - Visible Meshes {}",
478 sprites.iter().len(),
479 sprites.iter().filter(|(_, vis)| vis.get()).count(),
480 );
481 }
482}
483
484#[derive(Deref, DerefMut)]
485struct PrintingTimer(Timer);
486
487impl Default for PrintingTimer {
488 fn default() -> Self {
489 Self(Timer::from_seconds(1.0, TimerMode::Repeating))
490 }
491}
492
493fn update_materials(mut materials: ResMut<Assets<StandardMaterial>>, time: Res<Time>) {
494 let elapsed = time.elapsed_secs();
495 for (i, (_, material)) in materials.iter_mut().enumerate() {
496 let hue = (elapsed + i as f32 * 0.005).rem_euclid(1.0);
497 let color = fast_hue_to_rgb(hue);
499 material.base_color = Color::linear_rgb(color.x, color.y, color.z);
500 }
501}
502
503#[inline]
504fn fast_hue_to_rgb(hue: f32) -> Vec3 {
505 (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)
506}