Skip to main content

yog_body/
rig.rs

1//! A [`Rig`]: a model with clips playing on it, layered, and poses a mod
2//! sets by hand on top — evaluated into quads to draw, vanilla parts to
3//! pose, and locators to follow.
4
5use yog_sync::Arc;
6
7use crate::anim::{BonePose, Clip, Pose};
8use crate::math::{Mat4, Vec3};
9use crate::model::{Model, Quad};
10use crate::wire::{Anchor, BoneOp, Look, Ops};
11
12/// A clip playing.
13#[derive(Debug, Clone)]
14pub struct Layer {
15    pub clip: Arc<Clip>,
16    /// When it started, in the rig's seconds.
17    pub start: f32,
18    pub speed: f32,
19    /// How much of it shows (0–1); fades lay clips over each other.
20    pub weight: f32,
21}
22
23/// A model and what it does.
24#[derive(Debug, Clone)]
25pub struct Rig {
26    pub model: Arc<Model>,
27    /// Where the model's root bones hang: on a vanilla part, the model, the
28    /// entity, or a block.
29    pub anchor: Anchor,
30    pub layers: Vec<Layer>,
31    /// Set by hand each frame, laid over the clips.
32    pub manual: Pose,
33    pub look: Look,
34}
35
36impl Rig {
37    pub fn new(model: Arc<Model>, anchor: Anchor) -> Rig {
38        Rig {
39            model,
40            anchor,
41            layers: Vec::new(),
42            manual: Pose::default(),
43            look: Look::default(),
44        }
45    }
46
47    /// Plays `clip` from `now` (replacing the same clip if it plays).
48    pub fn play(&mut self, clip: Arc<Clip>, now: f32) {
49        self.layers.retain(|l| l.clip.name != clip.name);
50        self.layers.push(Layer {
51            clip,
52            start: now,
53            speed: 1.0,
54            weight: 1.0,
55        });
56    }
57
58    /// Stops `clip`.
59    pub fn stop(&mut self, clip: &str) {
60        self.layers.retain(|l| l.clip.name != clip);
61    }
62
63    pub fn playing(&self, clip: &str) -> bool {
64        self.layers.iter().any(|l| l.clip.name == clip)
65    }
66
67    /// The pose at `now`: the clips by weight, then the manual pose. Clips
68    /// that ended (`once`) are dropped.
69    pub fn pose(&mut self, now: f32) -> Pose {
70        self.layers
71            .retain(|l| !l.clip.finished((now - l.start) * l.speed));
72        let mut pose = Pose::default();
73        for l in &self.layers {
74            pose.add(&l.clip.sample((now - l.start) * l.speed), l.weight);
75        }
76        pose.add(&self.manual, 1.0);
77        pose
78    }
79
80    /// Each bone's matrix in the anchor's space.
81    pub fn matrices(&self, pose: &Pose) -> Vec<Mat4> {
82        let mut out: Vec<Mat4> = Vec::with_capacity(self.model.bones.len());
83        for b in &self.model.bones {
84            let p = pose.get(&b.name);
85            let local = crate::math::part(b.pivot + p.position, b.rotation + p.rotation, p.scale);
86            out.push(match b.parent {
87                Some(i) => out[i] * local,
88                None => local,
89            });
90        }
91        out
92    }
93
94    /// The model's faces, posed, in the anchor's space.
95    pub fn quads(&self, pose: &Pose) -> Vec<Quad> {
96        let m = self.matrices(pose);
97        self.model
98            .bones
99            .iter()
100            .zip(&m)
101            .flat_map(|(b, m)| b.quads.iter().map(move |q| q.transformed(m)))
102            .collect()
103    }
104
105    /// Where locator `name` is now, in the anchor's space.
106    pub fn locator(&self, name: &str, pose: &Pose) -> Option<Vec3> {
107        let m = self.matrices(pose);
108        self.model.bones.iter().zip(&m).find_map(|(b, m)| {
109            b.locators
110                .iter()
111                .find(|(n, _)| n == name)
112                .map(|(_, p)| m.transform_point(*p))
113        })
114    }
115
116    /// Writes the rig at `now` into `ops`: its quads, and the pose of the
117    /// vanilla parts its clips move (bones the model lacks).
118    pub fn write(&mut self, ops: &mut Ops, now: f32) {
119        let pose = self.pose(now);
120        for (name, p) in &pose.0 {
121            if self.model.bone(name).is_none() {
122                ops.bone(name, &vanilla_op(p));
123            }
124        }
125        let quads = self.quads(&pose);
126        ops.mesh(&self.anchor, &self.model.texture, self.look, &quads);
127    }
128}
129
130/// A clip's pose of a vanilla part, added to vanilla's.
131pub fn vanilla_op(p: &BonePose) -> BoneOp {
132    BoneOp {
133        rotation: p.rotation,
134        position: p.position,
135        scale: (p.scale != Vec3::ONE).then_some(p.scale),
136        ..Default::default()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::format::{BoneDef, CubeDef, ModelFile};
144    use yog_data::Ordered;
145
146    fn model() -> Arc<Model> {
147        let file = ModelFile {
148            texture: "m:x".into(),
149            texture_size: [64, 32],
150            bones: vec![
151                BoneDef {
152                    name: "a".into(),
153                    parent: None,
154                    pivot: [0.0, 0.0, 0.0],
155                    rotation: [0.0; 3],
156                    cubes: vec![],
157                    locators: Ordered::default(),
158                },
159                BoneDef {
160                    name: "b".into(),
161                    parent: Some("a".into()),
162                    pivot: [0.0, 10.0, 0.0],
163                    rotation: [0.0; 3],
164                    cubes: vec![CubeDef {
165                        origin: [0.0; 3],
166                        size: [1.0; 3],
167                        uv: [0.0; 2],
168                        inflate: 0.0,
169                        mirror: false,
170                    }],
171                    locators: Ordered(vec![("tip".into(), [0.0, 5.0, 0.0])]),
172                },
173            ],
174        };
175        Arc::new(Model::new(&file).unwrap())
176    }
177
178    #[test]
179    fn children_follow_parents() {
180        let mut rig = Rig::new(model(), Anchor::Model);
181        rig.manual.bone("a").rotation = Vec3::new(0.0, 0.0, std::f32::consts::FRAC_PI_2);
182        let pose = rig.pose(0.0);
183        let tip = rig.locator("tip", &pose).unwrap();
184        assert!(
185            (tip - Vec3::new(-15.0, 0.0, 0.0)).length() < 1e-4,
186            "{tip:?}"
187        );
188        assert_eq!(rig.quads(&pose).len(), 6);
189    }
190}