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 std::sync::Arc;
6
7use crate::anim::{BonePose, Clip, Pose};
8use crate::math::{Mat4, Vec3};
9use crate::model::{Model, Quad};
10use crate::wire::{Anchor, BoneOp, Mode, 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 mode: Mode,
34    pub glint: bool,
35    pub argb: u32,
36}
37
38impl Rig {
39    pub fn new(model: Arc<Model>, anchor: Anchor) -> Rig {
40        Rig { model, anchor, layers: Vec::new(), manual: Pose::default(), mode: Mode::Cutout, glint: false, argb: 0xFFFF_FFFF }
41    }
42
43    /// Plays `clip` from `now` (replacing the same clip if it plays).
44    pub fn play(&mut self, clip: Arc<Clip>, now: f32) {
45        self.layers.retain(|l| l.clip.name != clip.name);
46        self.layers.push(Layer { clip, start: now, speed: 1.0, weight: 1.0 });
47    }
48
49    /// Stops `clip`.
50    pub fn stop(&mut self, clip: &str) {
51        self.layers.retain(|l| l.clip.name != clip);
52    }
53
54    pub fn playing(&self, clip: &str) -> bool {
55        self.layers.iter().any(|l| l.clip.name == clip)
56    }
57
58    /// The pose at `now`: the clips by weight, then the manual pose. Clips
59    /// that ended (`once`) are dropped.
60    pub fn pose(&mut self, now: f32) -> Pose {
61        self.layers.retain(|l| !l.clip.finished((now - l.start) * l.speed));
62        let mut pose = Pose::default();
63        for l in &self.layers {
64            pose.add(&l.clip.sample((now - l.start) * l.speed), l.weight);
65        }
66        pose.add(&self.manual, 1.0);
67        pose
68    }
69
70    /// Each bone's matrix in the anchor's space.
71    pub fn matrices(&self, pose: &Pose) -> Vec<Mat4> {
72        let mut out: Vec<Mat4> = Vec::with_capacity(self.model.bones.len());
73        for b in &self.model.bones {
74            let p = pose.get(&b.name);
75            let local = crate::math::part(b.pivot + p.position, b.rotation + p.rotation, p.scale);
76            out.push(match b.parent {
77                Some(i) => out[i] * local,
78                None => local,
79            });
80        }
81        out
82    }
83
84    /// The model's faces, posed, in the anchor's space.
85    pub fn quads(&self, pose: &Pose) -> Vec<Quad> {
86        let m = self.matrices(pose);
87        self.model.bones.iter().zip(&m).flat_map(|(b, m)| b.quads.iter().map(move |q| q.transformed(m))).collect()
88    }
89
90    /// Where locator `name` is now, in the anchor's space.
91    pub fn locator(&self, name: &str, pose: &Pose) -> Option<Vec3> {
92        let m = self.matrices(pose);
93        self.model.bones.iter().zip(&m).find_map(|(b, m)| {
94            b.locators.iter().find(|(n, _)| n == name).map(|(_, p)| m.transform_point(*p))
95        })
96    }
97
98    /// Writes the rig at `now` into `ops`: its quads, and the pose of the
99    /// vanilla parts its clips move (bones the model lacks).
100    pub fn write(&mut self, ops: &mut Ops, now: f32) {
101        let pose = self.pose(now);
102        for (name, p) in &pose.0 {
103            if self.model.bone(name).is_none() {
104                ops.bone(name, &vanilla_op(p));
105            }
106        }
107        let quads = self.quads(&pose);
108        ops.mesh(&self.anchor, &self.model.texture, self.mode, self.glint, self.argb, &quads);
109    }
110}
111
112/// A clip's pose of a vanilla part, added to vanilla's.
113pub fn vanilla_op(p: &BonePose) -> BoneOp {
114    BoneOp {
115        rotation: p.rotation,
116        position: p.position,
117        scale: (p.scale != Vec3::ONE).then_some(p.scale),
118        ..Default::default()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::format::{BoneDef, CubeDef, ModelFile};
126    use yog_data::Ordered;
127
128    fn model() -> Arc<Model> {
129        let file = ModelFile {
130            texture: "x.png".into(),
131            texture_size: [64, 32],
132            bones: vec![
133                BoneDef { name: "a".into(), parent: None, pivot: [0.0, 0.0, 0.0], rotation: [0.0; 3], cubes: vec![], locators: Ordered::default() },
134                BoneDef {
135                    name: "b".into(),
136                    parent: Some("a".into()),
137                    pivot: [0.0, 10.0, 0.0],
138                    rotation: [0.0; 3],
139                    cubes: vec![CubeDef { origin: [0.0; 3], size: [1.0; 3], uv: [0.0; 2], inflate: 0.0, mirror: false }],
140                    locators: Ordered(vec![("tip".into(), [0.0, 5.0, 0.0])]),
141                },
142            ],
143        };
144        Arc::new(Model::new(&file, "m").unwrap())
145    }
146
147    #[test]
148    fn children_follow_parents() {
149        let mut rig = Rig::new(model(), Anchor::Model);
150        rig.manual.bone("a").rotation = Vec3::new(0.0, 0.0, std::f32::consts::FRAC_PI_2);
151        let pose = rig.pose(0.0);
152        let tip = rig.locator("tip", &pose).unwrap();
153        assert!((tip - Vec3::new(-15.0, 0.0, 0.0)).length() < 1e-4, "{tip:?}");
154        assert_eq!(rig.quads(&pose).len(), 6);
155    }
156}