Skip to main content

flock_parallelism/
flock-parallelism.rs

1//! A flock of butterflies, each butterfly's next position and heading read from the
2//! previous tick's own neighborhood by the three rules of a flock:
3//! separation, alignment and cohesion.
4//! The flock is kept in cell order: one array of positions and one of
5//! each butterfly's velocity, sorted by the cell of a box each butterfly lies in,
6//! with the start of every cell beside them. A tick steers each cell's butterflies
7//! against the cells around it, each cell its own task on the workers the
8//! engine builds at startup, with nothing configured for it, and then
9//! sorts the next arrays into cells again by one counting pass. The
10//! flock's center, which the camera turns about, is summed by the chunked
11//! fold that `mirage_engine::rayon`'s own docs show, so it reads the same
12//! bits at any worker count.
13//!
14//! `cargo run --example flock-parallelism`. The camera holds no controls of its own:
15//! it turns about the flock's center for the whole run. A panel names the
16//! pool's own workers, offers three flock sizes and times the last tick. A
17//! checkbox sets the same step to run one cell after another, so the tick
18//! time shows the parallel run's own gain.
19
20use core::f32::consts::{PI, TAU};
21use core::ops::Range;
22
23use mirage_engine::prelude::*;
24use mirage_engine::rayon::{self, prelude::*};
25
26/// Flock sizes the panel offers, ascending.
27const FLOCK_SIZES: [u32; 3] = [1_000, 10_000, 40_000];
28/// Flock size the example starts at: [`FLOCK_SIZES`]'s middle value.
29const DEFAULT_FLOCK_SIZE: u32 = FLOCK_SIZES[1];
30
31/// Cubic meters of the flock's own sphere given to each butterfly, so the
32/// flock's density is the same at every size in [`FLOCK_SIZES`].
33const WORLD_VOLUME_PER_BUTTERFLY: f32 = 3.0;
34/// Meters between the ground and the bottom of the flock's own sphere.
35const FLOCK_CLEARANCE: f32 = 4.0;
36/// How far past the flock's own sphere [`Cells`]' own box reaches, as a
37/// fraction of the sphere's radius, so a butterfly turning back at the bound
38/// keeps a cell of its own.
39const BOX_MARGIN: f32 = 1.25;
40
41/// Meters within which a butterfly reads a neighbor's heading and position for
42/// alignment and cohesion; also the side of one cell of the box.
43const NEIGHBOR_RADIUS: f32 = 3.0;
44/// Meters within which a butterfly steers away from a neighbor.
45const SEPARATION_RADIUS: f32 = 1.3;
46
47/// Steering acceleration scales, in 1/second, each rule's own vector is
48/// multiplied by before it is added to a butterfly's velocity.
49const SEPARATION_WEIGHT: f32 = 2.5;
50const ALIGNMENT_WEIGHT: f32 = 1.2;
51const COHESION_WEIGHT: f32 = 1.6;
52/// Steering acceleration scale, in 1/second, that turns a butterfly back toward
53/// the center once it leaves the flock's own sphere.
54const BOUND_WEIGHT: f32 = 4.0;
55
56/// The slowest and fastest speed a butterfly holds, in meters/second.
57const MIN_SPEED: f32 = 3.0;
58const MAX_SPEED: f32 = 7.0;
59
60/// The butterfly's own source, next to the other example assets, the root
61/// node the source names it under, and the image inside it the wings are
62/// drawn with, read grey so a tint colors it.
63const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
64const BUTTERFLY_ROOT: &str = "Butterfly";
65const BUTTERFLY_SKIN: &str = "butterfly-skin";
66/// The tints the flock's butterflies are drawn in, one per butterfly by
67/// its own integer-hash, over the grey skin; past `1.0`, since the grey
68/// skin is darker than the color it stands in for.
69const 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];
77/// How many times the model's own size a butterfly is drawn at: the model
78/// is a quarter meter across, so the flock's are a meter and more.
79const BUTTERFLY_SCALE: f32 = 5.0;
80/// Flaps per second of a butterfly's own `Fly` clip, at the middle of the
81/// spread below.
82const FLAP_RATE: f32 = 2.5;
83/// How far the flap groups' rates spread about [`FLAP_RATE`], as a fraction
84/// of it from the slowest group to the fastest.
85const FLAP_RATE_SPREAD: f32 = 0.6;
86/// The three waves a group's flap rate rises and falls by, each a depth as
87/// a fraction of the rate and the seconds one wave takes: a butterfly flaps
88/// more and then less by turns, and the three waves never line up.
89const FLAP_WAVES: [(f32, f32); 3] = [(0.35, 1.7), (0.3, 4.3), (0.25, 11.0)];
90/// How many flap groups the flock shares, each with a rate that rises and
91/// falls and a phase of its own: every butterfly belongs to one, so a frame
92/// poses the clip this many times, never once per butterfly.
93const FLAP_GROUPS: usize = 24;
94
95/// The ground plane's side length, in meters, far enough that its edge
96/// meets the horizon from the camera's height.
97const GROUND_SIZE: f32 = 4000.0;
98const GROUND_COLOR: Color = Color::rgb(0.4, 0.31, 0.25);
99
100/// The sun's direction, low enough that the flock's shadow lands on clear
101/// ground beside it.
102const 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;
109/// The sky's own color under the horizon, so a butterfly's own underside and
110/// the ground reflect light from the sky instead of reading black.
111const SKY_GROUND: Color = Color::rgb(0.42, 0.34, 0.28);
112
113/// The camera's height above and distance from the flock's center, each a
114/// fraction of the flock's own radius, so the view frames every size.
115const CAMERA_HEIGHT_FRACTION: f32 = 0.45;
116const CAMERA_DISTANCE_FRACTION: f32 = 2.2;
117/// How far above the flock's center the camera looks, as a fraction of
118/// the flock's own radius, so the flock sits below the panel.
119const CAMERA_AIM_LIFT_FRACTION: f32 = 0.25;
120const CAMERA_FOV: f32 = 75.0;
121/// Radians the camera turns about the flock's center per second.
122const CAMERA_ANGULAR_SPEED: f32 = 0.08;
123
124/// Butterflies a chunk of [`Butterflies::center`]'s fold sums at a time, so the sum
125/// reads the same bits at any worker count.
126const CENTER_CHUNK_SIZE: usize = 1024;
127
128const PANEL_PADDING: i8 = 8;
129
130meshes! { enum Shape { Butterfly, Plane } }
131
132/// The one sky this game draws, a gradient set each frame.
133#[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/// The one clip a butterfly is posed by, named as the source names it.
152#[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
166/// `skin` with every texel at its own grey level, its alpha kept, so a tint
167/// colors it whole.
168fn 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/// What one butterfly looks like: the flap group that poses it and the tint
184/// it is drawn in, both its own for the whole run.
185#[derive(Clone, Copy, Default)]
186struct Kind {
187    flap: u8,
188    tint: u8,
189}
190
191impl Kind {
192    /// The kind butterfly `index` is given, by its own integer-hash.
193    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/// A machine of one state, holding the flap at the phase it is given: one
202/// machine per flap group poses every butterfly of that group.
203#[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
225/// The phase flap group `group` holds `flown` seconds into the run: the
226/// flaps its own rate has run, spread about [`FLAP_RATE`] by
227/// [`FLAP_RATE_SPREAD`], the rate rising and falling by [`FLAP_WAVES`] at
228/// the group's own offsets, from the group's own start. The count is the
229/// sum of the rate over the seconds `flown`, taken whole rather than tick by
230/// tick, so no step adds up an error.
231fn 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/// The sphere the flock is bound to: its center, held above the ground by
247/// [`FLOCK_CLEARANCE`], and its radius, sized so each butterfly has
248/// [`WORLD_VOLUME_PER_BUTTERFLY`].
249#[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/// The box of cells the flock is kept in: cubes [`NEIGHBOR_RADIUS`] across,
266/// `side` to an axis, from `least` on every axis.
267#[derive(Clone, Copy)]
268struct Cells {
269    side: usize,
270    least: Vec3A,
271}
272
273impl Cells {
274    /// The box over `world`'s sphere and [`BOX_MARGIN`] past it.
275    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    /// How many cells the box holds.
285    fn count(self) -> usize {
286        self.side * self.side * self.side
287    }
288
289    /// The cell `position` lies in, held inside the box on every axis.
290    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    /// `cell` and every cell beside it, at most 27, inside the box.
300    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
312/// The flock: every butterfly's position and velocity in cell order, where each
313/// cell's butterflies start, and the arrays the next tick is written into.
314struct Butterflies {
315    cells: Cells,
316    /// Where each cell's butterflies start, and one more for the end of the last.
317    start: Vec<u32>,
318    position: Vec<Vec3A>,
319    velocity: Vec<Vec3A>,
320    /// Each butterfly's kind, in the same order.
321    kind: Vec<Kind>,
322    next_position: Vec<Vec3A>,
323    next_velocity: Vec<Vec3A>,
324    next_kind: Vec<Kind>,
325    /// The cell each butterfly of the next arrays lies in.
326    next_cell: Vec<u32>,
327}
328
329impl Butterflies {
330    /// `count` butterflies scattered through `world`'s sphere, each with a level
331    /// heading at [`MIN_SPEED`], sorted into cells.
332    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    /// One tick of `dt` seconds: every cell's butterflies steered against the
368    /// cells around it into the next arrays, on the engine's workers or one
369    /// cell after another, then the next arrays sorted into cells again.
370    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    /// Sorts the next arrays into the current ones by cell: a count per
401    /// cell, a running total, and a placement in order, so the result reads
402    /// the same at any worker count.
403    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    /// The flock's center: positions summed in fixed chunks and then
422    /// folded in order, the fold that `mirage_engine::rayon`'s own docs
423    /// show, so it reads the same bits at any worker count.
424    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    /// Every butterfly's position, velocity and kind, in cell order.
439    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
448/// The flock as one tick reads it: the current arrays and the cells they
449/// are sorted by, shared by every cell's step.
450struct 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    /// The butterflies of `cell`, as a range of the arrays.
460    fn range(&self, cell: usize) -> Range<usize> {
461        self.start[cell] as usize..self.start[cell + 1] as usize
462    }
463
464    /// Steers every butterfly of `cell` by separation, alignment and cohesion
465    /// against the butterflies of the cells around it, and back toward the center
466    /// once it leaves `world`'s sphere, writing the next state into `out`.
467    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
526/// One cell's share of the next arrays, written by that cell's step alone.
527struct 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    /// The next arrays split into one share per cell of `flock`, in cell
536    /// order, each as long as that cell's range.
537    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
575/// An integer-hash of `seed` and `salt`.
576fn 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
586/// `hash`, scaled to `0.0..1.0`.
587fn hash_unit(seed: u32, salt: u32) -> f32 {
588    hash(seed, salt) as f32 / u32::MAX as f32
589}
590
591/// The load a player chooses: how many butterflies the flock holds, and whether
592/// the step below runs one cell after another instead of on the engine's
593/// workers.
594struct 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    /// Seconds the ticks have run, which paces the flap.
615    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    /// Rebuilds the flock where the chosen size changed since the last
635    /// frame.
636    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    /// The camera at `elapsed`, turning about `center` at
646    /// [`CAMERA_HEIGHT_FRACTION`] and [`CAMERA_DISTANCE_FRACTION`] of the
647    /// flock's own radius, looking [`CAMERA_AIM_LIFT_FRACTION`] above it.
648    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    /// Every butterfly at its own scale, turned to face its velocity, posed
673    /// by the flap machine of its own group and tinted its own color.
674    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    /// The load controls, the pool's own worker count, and the last tick's
691    /// own cost.
692    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}