Skip to main content

yog_body/
morph.rs

1//! Morphing one body into another, as the old Morph mod did: the faces of
2//! the first slide, turn and stretch into the faces of the second, while the
3//! first's skin fades into the second's.
4//!
5//! Faces pair up in the order the models give them (cube by cube, as their
6//! parts come); faces one side has more of grow from — or shrink into — the
7//! middle of the other body.
8
9use std::collections::BTreeMap;
10
11use crate::math::Vec3;
12use crate::model::Quad;
13use crate::wire::{Anchor, Mesh, Mode, Ops};
14
15/// One face of a body, with its texture.
16#[derive(Clone, Copy)]
17struct Face<'a> {
18    texture: &'a str,
19    quad: Quad,
20}
21
22fn faces(meshes: &[Mesh]) -> Vec<Face<'_>> {
23    meshes.iter().flat_map(|m| m.quads.iter().map(move |q| Face { texture: &m.texture, quad: *q })).collect()
24}
25
26fn middle(faces: &[Face<'_>]) -> Vec3 {
27    if faces.is_empty() {
28        return Vec3::ZERO;
29    }
30    faces.iter().map(|f| f.quad.center()).fold(Vec3::ZERO, |a, b| a + b) * (1.0 / faces.len() as f32)
31}
32
33/// A face shrunk to a point.
34fn point(at: Vec3, like: &Quad) -> Quad {
35    Quad { pos: [at; 4], uv: like.uv, normal: like.normal }
36}
37
38fn lerp(a: &Quad, b: &Quad, t: f32) -> [Vec3; 4] {
39    [0, 1, 2, 3].map(|i| a.pos[i].lerp(b.pos[i], t))
40}
41
42/// Writes `from` turning into `to` at `t` (0: all `from`, 1: all `to`)
43/// into `ops`, in entity space — both as [`crate::wire::Capture`]s give
44/// them. Smooth `t` (an ease) is the caller's choice.
45pub fn blend(ops: &mut Ops, from: &[Mesh], to: &[Mesh], t: f32) {
46    let t = t.clamp(0.0, 1.0);
47    let (a, b) = (faces(from), faces(to));
48    let (ma, mb) = (middle(&a), middle(&b));
49    // Texture → (quads in the old skin, quads in the new).
50    let mut old: BTreeMap<&str, Vec<Quad>> = BTreeMap::new();
51    let mut new: BTreeMap<&str, Vec<Quad>> = BTreeMap::new();
52    for i in 0..a.len().max(b.len()) {
53        let fa = a.get(i).map(|f| (f.texture, f.quad));
54        let fb = b.get(i).map(|f| (f.texture, f.quad));
55        let (qa, qb) = match (fa, fb) {
56            (Some((_, qa)), Some((_, qb))) => (qa, qb),
57            (Some((_, qa)), None) => (qa, point(mb, &qa)),
58            (None, Some((_, qb))) => (point(ma, &qb), qb),
59            (None, None) => continue,
60        };
61        let pos = lerp(&qa, &qb, t);
62        let normal = qa.normal.lerp(qb.normal, t).normalize();
63        if let Some((tex, q)) = fa {
64            old.entry(tex).or_default().push(Quad { pos, uv: q.uv, normal });
65        }
66        if let Some((tex, q)) = fb {
67            new.entry(tex).or_default().push(Quad { pos, uv: q.uv, normal });
68        }
69    }
70    let alpha = |k: f32| ((k.clamp(0.0, 1.0) * 255.0).round() as u32) << 24 | 0x00FF_FFFF;
71    for (tex, quads) in &old {
72        if t < 1.0 {
73            ops.mesh(&Anchor::Entity, tex, Mode::Translucent, false, alpha(1.0 - t), quads);
74        }
75    }
76    for (tex, quads) in &new {
77        if t > 0.0 {
78            ops.mesh(&Anchor::Entity, tex, Mode::Translucent, false, alpha(t), quads);
79        }
80    }
81}
82
83/// `t` eased in and out (smoothstep) — how a morph looks natural.
84pub fn ease(t: f32) -> f32 {
85    let t = t.clamp(0.0, 1.0);
86    t * t * (3.0 - 2.0 * t)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn mesh(tex: &str, at: f32, n: usize) -> Mesh {
94        let q = Quad { pos: [Vec3::new(at, 0.0, 0.0); 4], uv: [[0.0; 2]; 4], normal: Vec3::new(0.0, 1.0, 0.0) };
95        Mesh { texture: tex.into(), quads: vec![q; n] }
96    }
97
98    #[test]
99    fn half_way_draws_both_skins() {
100        let mut ops = Ops::default();
101        blend(&mut ops, &[mesh("a", 0.0, 2)], &[mesh("b", 2.0, 3)], 0.5);
102        assert!(ops.0.iter().filter(|b| **b == 3).count() >= 2);
103        let mut none = Ops::default();
104        blend(&mut none, &[mesh("a", 0.0, 2)], &[mesh("b", 2.0, 3)], 0.0);
105        assert!(!none.is_empty());
106    }
107}