1use core::f32::consts::{PI, TAU};
21use core::ops::Range;
22
23use mirage_engine::prelude::*;
24use mirage_engine::rayon::{self, prelude::*};
25
26const FLOCK_SIZES: [u32; 3] = [1_000, 10_000, 40_000];
28const DEFAULT_FLOCK_SIZE: u32 = FLOCK_SIZES[1];
30
31const WORLD_VOLUME_PER_BUTTERFLY: f32 = 3.0;
34const FLOCK_CLEARANCE: f32 = 4.0;
36const BOX_MARGIN: f32 = 1.25;
40
41const NEIGHBOR_RADIUS: f32 = 3.0;
44const SEPARATION_RADIUS: f32 = 1.3;
46
47const SEPARATION_WEIGHT: f32 = 2.5;
50const ALIGNMENT_WEIGHT: f32 = 1.2;
51const COHESION_WEIGHT: f32 = 1.6;
52const BOUND_WEIGHT: f32 = 4.0;
55
56const MIN_SPEED: f32 = 3.0;
58const MAX_SPEED: f32 = 7.0;
59
60const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
64const BUTTERFLY_ROOT: &str = "Butterfly";
65const BUTTERFLY_SKIN: &str = "butterfly-skin";
66const TINTS: [Color; 6] = [
70 Color::rgb(1.8, 1.0, 0.3),
71 Color::rgb(0.6, 1.0, 1.8),
72 Color::rgb(1.8, 1.6, 0.5),
73 Color::rgb(1.7, 1.7, 1.6),
74 Color::rgb(1.7, 0.45, 0.55),
75 Color::rgb(1.3, 0.7, 1.7),
76];
77const BUTTERFLY_SCALE: f32 = 5.0;
80const FLAP_RATE: f32 = 2.5;
83const FLAP_RATE_SPREAD: f32 = 0.6;
86const FLAP_WAVES: [(f32, f32); 3] = [(0.35, 1.7), (0.3, 4.3), (0.25, 11.0)];
90const FLAP_GROUPS: usize = 24;
94
95const GROUND_SIZE: f32 = 4000.0;
98const GROUND_COLOR: Color = Color::rgb(0.4, 0.31, 0.25);
99
100const SUN_DIRECTION: Vec3 = Vec3::new(-0.8, -0.55, -0.5);
103const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
104
105const SKY_ZENITH: Color = Color::rgb(0.15, 0.22, 0.42);
106const SKY_HORIZON: Color = Color::rgb(0.7, 0.48, 0.34);
107const SKY_NADIR: Color = Color::rgb(0.2, 0.16, 0.14);
108const SKY_LIGHT: f32 = 0.65;
109const SKY_GROUND: Color = Color::rgb(0.42, 0.34, 0.28);
112
113const CAMERA_HEIGHT_FRACTION: f32 = 0.45;
116const CAMERA_DISTANCE_FRACTION: f32 = 2.2;
117const CAMERA_AIM_LIFT_FRACTION: f32 = 0.25;
120const CAMERA_FOV: f32 = 75.0;
121const CAMERA_ANGULAR_SPEED: f32 = 0.08;
123
124const CENTER_CHUNK_SIZE: usize = 1024;
127
128const PANEL_PADDING: i8 = 8;
129
130meshes! { enum Shape { Butterfly, Plane } }
131
132#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
134enum Sky {
135 Day,
136}
137
138impl Skyboxes for Sky {
139 fn build(&self, _assets: &Assets) -> SkyboxData {
140 match self {
141 Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR)
142 .lit_by(SKY_LIGHT)
143 .with_ground(SKY_GROUND),
144 }
145 }
146}
147
148#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
149struct Butterfly;
150
151#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
153enum ButterflyClip {
154 #[clip("fly")]
155 Fly,
156}
157
158impl Mesh<NoParts, ButterflyClip> for Butterfly {
159 fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
160 assets
161 .model(BUTTERFLY_ROOT)
162 .with_texture(greyed(&assets.texture(BUTTERFLY_SKIN)))
163 }
164}
165
166fn greyed(skin: &TextureData) -> TextureData {
169 let pixels = skin
170 .pixels()
171 .chunks_exact(4)
172 .flat_map(|texel| {
173 let [red, green, blue, alpha] = [texel[0], texel[1], texel[2], texel[3]];
174 let grey =
175 (0.2126 * f32::from(red) + 0.7152 * f32::from(green) + 0.0722 * f32::from(blue))
176 .round() as u8;
177 [grey, grey, grey, alpha]
178 })
179 .collect();
180 TextureData::rgba8(skin.size(), pixels)
181}
182
183#[derive(Clone, Copy, Default)]
186struct Kind {
187 flap: u8,
188 tint: u8,
189}
190
191impl Kind {
192 fn of(index: u32) -> Self {
194 Self {
195 flap: (hash(index, 4) % FLAP_GROUPS as u32) as u8,
196 tint: (hash(index, 5) % TINTS.len() as u32) as u8,
197 }
198 }
199}
200
201#[derive(Clone, Copy, Eq, PartialEq, Debug)]
204enum FlapState {
205 Flapping,
206}
207
208impl AnimationStates for FlapState {
209 type Clip = ButterflyClip;
210 type Input = f32;
211
212 fn entry() -> Self {
213 Self::Flapping
214 }
215
216 fn motion(&self, phase: &f32) -> Motion<ButterflyClip> {
217 Motion::scrubbed(ButterflyClip::Fly, *phase)
218 }
219
220 fn next(&self, _phase: &f32, _at: Progress) -> Option<Transition<Self>> {
221 None
222 }
223}
224
225fn flap_phase(group: usize, flown: f32) -> f32 {
232 let share = group as f32 / FLAP_GROUPS as f32;
233 let rate = FLAP_RATE * (1.0 + FLAP_RATE_SPREAD * (share - 0.5));
234 let waved: f32 = FLAP_WAVES
235 .iter()
236 .enumerate()
237 .map(|(wave, &(depth, period))| {
238 let angular = TAU / period;
239 let offset = hash_unit(group as u32, 6 + wave as u32) * TAU;
240 -depth / angular * (angular * flown + offset).cos()
241 })
242 .sum();
243 (rate * (flown + waved) + share).fract()
244}
245
246#[derive(Clone, Copy)]
250struct World {
251 center: Vec3A,
252 radius: f32,
253}
254
255impl World {
256 fn for_flock(count: u32) -> Self {
257 let radius = (count as f32 * WORLD_VOLUME_PER_BUTTERFLY * 3.0 / (4.0 * PI)).cbrt();
258 Self {
259 center: Vec3A::new(0.0, radius + FLOCK_CLEARANCE, 0.0),
260 radius,
261 }
262 }
263}
264
265#[derive(Clone, Copy)]
268struct Cells {
269 side: usize,
270 least: Vec3A,
271}
272
273impl Cells {
274 fn covering(world: World) -> Self {
276 let reach = world.radius * BOX_MARGIN;
277 let side = ((2.0 * reach) / NEIGHBOR_RADIUS).ceil().max(1.0) as usize;
278 Self {
279 side,
280 least: world.center - Vec3A::splat(reach),
281 }
282 }
283
284 fn count(self) -> usize {
286 self.side * self.side * self.side
287 }
288
289 fn of(self, position: Vec3A) -> usize {
291 let scaled = (position - self.least) / NEIGHBOR_RADIUS;
292 let most = (self.side - 1) as f32;
293 let x = scaled.x.clamp(0.0, most) as usize;
294 let y = scaled.y.clamp(0.0, most) as usize;
295 let z = scaled.z.clamp(0.0, most) as usize;
296 (x * self.side + y) * self.side + z
297 }
298
299 fn around(self, cell: usize) -> impl Iterator<Item = usize> {
301 let side = self.side;
302 let z = cell % side;
303 let y = (cell / side) % side;
304 let x = cell / (side * side);
305 let span = move |at: usize| at.saturating_sub(1)..(at + 2).min(side);
306 span(x).flat_map(move |cx| {
307 span(y).flat_map(move |cy| span(z).map(move |cz| (cx * side + cy) * side + cz))
308 })
309 }
310}
311
312struct Butterflies {
315 cells: Cells,
316 start: Vec<u32>,
318 position: Vec<Vec3A>,
319 velocity: Vec<Vec3A>,
320 kind: Vec<Kind>,
322 next_position: Vec<Vec3A>,
323 next_velocity: Vec<Vec3A>,
324 next_kind: Vec<Kind>,
325 next_cell: Vec<u32>,
327}
328
329impl Butterflies {
330 fn scattered(count: u32, world: World) -> Self {
333 let cells = Cells::covering(world);
334 let count = count as usize;
335 let mut swarm = Self {
336 cells,
337 start: vec![0; cells.count() + 1],
338 position: vec![Vec3A::ZERO; count],
339 velocity: vec![Vec3A::ZERO; count],
340 kind: vec![Kind::default(); count],
341 next_position: Vec::with_capacity(count),
342 next_velocity: Vec::with_capacity(count),
343 next_kind: Vec::with_capacity(count),
344 next_cell: Vec::with_capacity(count),
345 };
346 for index in 0..count as u32 {
347 let radius = world.radius * hash_unit(index, 0).cbrt();
348 let inclination = hash_unit(index, 1) * PI;
349 let azimuth = hash_unit(index, 2) * TAU;
350 let position = world.center
351 + Vec3A::new(
352 radius * inclination.sin() * azimuth.cos(),
353 radius * inclination.cos(),
354 radius * inclination.sin() * azimuth.sin(),
355 );
356 let heading = hash_unit(index, 3) * TAU;
357 let velocity = Vec3A::new(heading.cos(), 0.0, heading.sin()) * MIN_SPEED;
358 swarm.next_position.push(position);
359 swarm.next_velocity.push(velocity);
360 swarm.next_kind.push(Kind::of(index));
361 swarm.next_cell.push(cells.of(position) as u32);
362 }
363 swarm.sort();
364 swarm
365 }
366
367 fn step(&mut self, dt: f32, world: World, sequential: bool) {
371 let Self {
372 cells,
373 start,
374 position,
375 velocity,
376 kind,
377 next_position,
378 next_velocity,
379 next_kind,
380 next_cell,
381 } = self;
382 let flock = Flying {
383 cells: *cells,
384 start,
385 position,
386 velocity,
387 kind,
388 };
389 let mut out = CellOut::split(&flock, next_position, next_velocity, next_kind, next_cell);
390 let steer = |(cell, out): (usize, &mut CellOut<'_>)| flock.steer(cell, out, dt, world);
391 if sequential {
392 out.iter_mut().enumerate().for_each(steer);
393 } else {
394 out.par_iter_mut().enumerate().for_each(steer);
395 }
396 drop(out);
397 self.sort();
398 }
399
400 fn sort(&mut self) {
404 self.start.iter_mut().for_each(|start| *start = 0);
405 for &cell in &self.next_cell {
406 self.start[cell as usize + 1] += 1;
407 }
408 for cell in 0..self.cells.count() {
409 self.start[cell + 1] += self.start[cell];
410 }
411 let mut fill = self.start.clone();
412 for (index, &cell) in self.next_cell.iter().enumerate() {
413 let at = fill[cell as usize] as usize;
414 fill[cell as usize] += 1;
415 self.position[at] = self.next_position[index];
416 self.velocity[at] = self.next_velocity[index];
417 self.kind[at] = self.next_kind[index];
418 }
419 }
420
421 fn center(&self) -> Vec3A {
425 if self.position.is_empty() {
426 return Vec3A::ZERO;
427 }
428 let sum: Vec3A = self
429 .position
430 .par_chunks(CENTER_CHUNK_SIZE)
431 .map(|chunk| chunk.iter().copied().sum::<Vec3A>())
432 .collect::<Vec<Vec3A>>()
433 .into_iter()
434 .sum();
435 sum / self.position.len() as f32
436 }
437
438 fn each(&self) -> impl Iterator<Item = (Vec3A, Vec3A, Kind)> + '_ {
440 self.position
441 .iter()
442 .zip(&self.velocity)
443 .zip(&self.kind)
444 .map(|((&position, &velocity), &kind)| (position, velocity, kind))
445 }
446}
447
448struct Flying<'a> {
451 cells: Cells,
452 start: &'a [u32],
453 position: &'a [Vec3A],
454 velocity: &'a [Vec3A],
455 kind: &'a [Kind],
456}
457
458impl Flying<'_> {
459 fn range(&self, cell: usize) -> Range<usize> {
461 self.start[cell] as usize..self.start[cell + 1] as usize
462 }
463
464 fn steer(&self, cell: usize, out: &mut CellOut<'_>, dt: f32, world: World) {
468 let mut around: [Range<usize>; 27] = core::array::from_fn(|_| 0..0);
469 let mut near_count = 0;
470 for near in self.cells.around(cell) {
471 around[near_count] = self.range(near);
472 near_count += 1;
473 }
474 let around = &around[..near_count];
475
476 for (at, index) in self.range(cell).enumerate() {
477 let position = self.position[index];
478 let velocity = self.velocity[index];
479 let mut separation = Vec3A::ZERO;
480 let mut heading_sum = Vec3A::ZERO;
481 let mut position_sum = Vec3A::ZERO;
482 let mut neighbors = 0u32;
483
484 for near in around {
485 for other in near.clone() {
486 if other == index {
487 continue;
488 }
489 let offset = position - self.position[other];
490 let squared = offset.length_squared();
491 if squared > NEIGHBOR_RADIUS * NEIGHBOR_RADIUS || squared <= f32::EPSILON {
492 continue;
493 }
494 if squared < SEPARATION_RADIUS * SEPARATION_RADIUS {
495 separation += offset / squared.sqrt();
496 }
497 heading_sum += self.velocity[other];
498 position_sum += self.position[other];
499 neighbors += 1;
500 }
501 }
502
503 let mut steering = separation * SEPARATION_WEIGHT;
504 if neighbors > 0 {
505 let share = 1.0 / neighbors as f32;
506 steering += (heading_sum * share - velocity) * ALIGNMENT_WEIGHT
507 + (position_sum * share - position) * COHESION_WEIGHT;
508 }
509 let from_center = position - world.center;
510 if from_center.length() > world.radius {
511 steering -= from_center.normalize() * BOUND_WEIGHT;
512 }
513
514 let next = velocity + steering * dt;
515 let speed = next.length().clamp(MIN_SPEED, MAX_SPEED);
516 let next_velocity = next.normalize_or_zero() * speed;
517 let next_position = position + next_velocity * dt;
518 out.position[at] = next_position;
519 out.velocity[at] = next_velocity;
520 out.kind[at] = self.kind[index];
521 out.cell[at] = self.cells.of(next_position) as u32;
522 }
523 }
524}
525
526struct CellOut<'a> {
528 position: &'a mut [Vec3A],
529 velocity: &'a mut [Vec3A],
530 kind: &'a mut [Kind],
531 cell: &'a mut [u32],
532}
533
534impl<'a> CellOut<'a> {
535 fn split(
538 flock: &Flying<'_>,
539 position: &'a mut Vec<Vec3A>,
540 velocity: &'a mut Vec<Vec3A>,
541 kind: &'a mut Vec<Kind>,
542 cell: &'a mut Vec<u32>,
543 ) -> Vec<Self> {
544 let count = flock.position.len();
545 position.resize(count, Vec3A::ZERO);
546 velocity.resize(count, Vec3A::ZERO);
547 kind.resize(count, Kind::default());
548 cell.resize(count, 0);
549 let mut out = Vec::with_capacity(flock.cells.count());
550 let mut position = position.as_mut_slice();
551 let mut velocity = velocity.as_mut_slice();
552 let mut kind = kind.as_mut_slice();
553 let mut cell = cell.as_mut_slice();
554 for at in 0..flock.cells.count() {
555 let len = flock.range(at).len();
556 let (own, rest) = position.split_at_mut(len);
557 position = rest;
558 let (own_velocity, rest) = velocity.split_at_mut(len);
559 velocity = rest;
560 let (own_kind, rest) = kind.split_at_mut(len);
561 kind = rest;
562 let (own_cell, rest) = cell.split_at_mut(len);
563 cell = rest;
564 out.push(Self {
565 position: own,
566 velocity: own_velocity,
567 kind: own_kind,
568 cell: own_cell,
569 });
570 }
571 out
572 }
573}
574
575fn hash(seed: u32, salt: u32) -> u32 {
577 let mut x = seed ^ salt.wrapping_mul(0x9E37_79B9);
578 x ^= x >> 16;
579 x = x.wrapping_mul(0x7FEB_352D);
580 x ^= x >> 15;
581 x = x.wrapping_mul(0x846C_A68B);
582 x ^= x >> 16;
583 x
584}
585
586fn hash_unit(seed: u32, salt: u32) -> f32 {
588 hash(seed, salt) as f32 / u32::MAX as f32
589}
590
591struct Settings {
595 flock_size: u32,
596 sequential: bool,
597}
598
599impl Default for Settings {
600 fn default() -> Self {
601 Self {
602 flock_size: DEFAULT_FLOCK_SIZE,
603 sequential: false,
604 }
605 }
606}
607
608struct Flock {
609 settings: Settings,
610 applied_flock_size: u32,
611 world: World,
612 butterflies: Butterflies,
613 flaps: [Animator<Butterfly, FlapState>; FLAP_GROUPS],
614 flown: f32,
616 last_tick_ms: f32,
617}
618
619impl Flock {
620 fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
621 let settings = Settings::default();
622 let world = World::for_flock(settings.flock_size);
623 Ok(Self {
624 applied_flock_size: settings.flock_size,
625 butterflies: Butterflies::scattered(settings.flock_size, world),
626 settings,
627 world,
628 flaps: core::array::from_fn(|_| Animator::new()),
629 flown: 0.0,
630 last_tick_ms: 0.0,
631 })
632 }
633
634 fn apply_settings(&mut self) {
637 if self.settings.flock_size == self.applied_flock_size {
638 return;
639 }
640 self.world = World::for_flock(self.settings.flock_size);
641 self.butterflies = Butterflies::scattered(self.settings.flock_size, self.world);
642 self.applied_flock_size = self.settings.flock_size;
643 }
644
645 fn camera(center: Vec3, world: World, elapsed: f32) -> Camera {
649 let angle = elapsed * CAMERA_ANGULAR_SPEED;
650 let eye = center
651 + Vec3::new(
652 angle.cos() * world.radius * CAMERA_DISTANCE_FRACTION,
653 world.radius * CAMERA_HEIGHT_FRACTION,
654 angle.sin() * world.radius * CAMERA_DISTANCE_FRACTION,
655 );
656 let aim = center + Vec3::Y * world.radius * CAMERA_AIM_LIFT_FRACTION;
657 Camera::new(View::look_at(eye, aim), Projection::perspective(CAMERA_FOV))
658 }
659
660 fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
661 ctx.draw(
662 Plane
663 .at(Transform::from_scale(Vec3::new(
664 GROUND_SIZE,
665 1.0,
666 GROUND_SIZE,
667 )))
668 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
669 );
670 }
671
672 fn draw_butterflies(&self, ctx: &mut FrameContext<'_, Self>) {
675 for (position, velocity, kind) in self.butterflies.each() {
676 let rotation = Quat::from_rotation_arc(Vec3::Z, Vec3::from(velocity).normalize());
677 ctx.draw(
678 Butterfly
679 .at(Transform::from_scale_rotation_translation(
680 Vec3::splat(BUTTERFLY_SCALE),
681 rotation,
682 Vec3::from(position),
683 ))
684 .posed(&self.flaps[usize::from(kind.flap)])
685 .material(Material::lit(TINTS[usize::from(kind.tint)])),
686 );
687 }
688 }
689
690 fn panel(&mut self, ctx: &mut FrameContext<'_, Self>) {
693 let workers = rayon::current_num_threads();
694 let tick_ms = self.last_tick_ms;
695
696 ctx.ui(|ui| {
697 egui::Frame::new()
698 .fill(egui::Color32::from_gray(24))
699 .inner_margin(PANEL_PADDING)
700 .corner_radius(f32::from(PANEL_PADDING))
701 .show(ui, |ui| {
702 ui.label(format!("workers {workers}"));
703 ui.horizontal(|ui| {
704 for size in FLOCK_SIZES {
705 ui.radio_value(
706 &mut self.settings.flock_size,
707 size,
708 format!("{size} butterflies"),
709 );
710 }
711 });
712 ui.checkbox(&mut self.settings.sequential, "sequential update");
713 ui.separator();
714 ui.label(format!("tick time {tick_ms:.2}ms"));
715 });
716 });
717 }
718}
719
720impl Game for Flock {
721 type Meshes = Shape;
722 type Sounds = NoSounds;
723 type InputActions = NoInputActions;
724 type Skyboxes = Sky;
725 type SurfaceStyles = NoSurfaceStyles;
726 type PostEffects = NoPostEffects;
727
728 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
729 let dt = ctx.dt().as_secs_f32();
730 let start = Instant::now();
731 self.butterflies
732 .step(dt, self.world, self.settings.sequential);
733 self.last_tick_ms = start.elapsed().as_secs_f32() * 1000.0;
734
735 self.flown += dt;
736 for (group, flap) in self.flaps.iter_mut().enumerate() {
737 ctx.animate(Butterfly, flap, &flap_phase(group, self.flown));
738 }
739 }
740
741 fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
742 self.apply_settings();
743
744 let center = Vec3::from(self.butterflies.center());
745 let elapsed = ctx.elapsed().as_secs_f32();
746 ctx.set_camera(Self::camera(center, self.world, elapsed));
747 ctx.set_skybox(Sky::Day);
748 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
749
750 Self::draw_ground(ctx);
751 self.draw_butterflies(ctx);
752 self.panel(ctx);
753 }
754}
755
756fn main() {
757 run(
758 Config::new("Mirage: flock parallelism")
759 .with_size(1280, 720)
760 .with_assets([BUTTERFLY_SOURCE]),
761 Flock::init,
762 );
763}