Skip to main content

concinnity_core/components/
skeleton_pose.rs

1// src/components/skeleton_pose.rs
2
3use alloc::vec::Vec;
4
5use crate::ecs::SkinnedMeshHandle;
6use crate::gfx::pose_scratch::PoseScratch;
7use crate::gfx::proportions::ProportionLayer;
8use crate::gfx::skeleton::Skeleton;
9use crate::gfx::transform::Mat4;
10
11/// Runtime-only link between a skinned mesh and its animation state.
12///
13/// `GraphicsSystem` publishes one `SkeletonPose` per `SkinnedMesh` during
14/// init: it carries the resolved bind-pose `Skeleton` and the index of the
15/// mesh's skinned draw object in the backend. `AnimationSystem` then ticks the
16/// matching `Animation` clip each frame and writes the resulting skinning
17/// matrices into `joint_matrices`; `GraphicsSystem` reads them back and
18/// uploads them to the GPU. The one-frame producer/consumer hand-off is
19/// invisible at animation rates.
20///
21/// A `CharacterShape` targeting the mesh seeds the static layers: `morph_base`
22/// sits under every clip's morph track and `proportions` re-shapes every
23/// sampled pose before skinning.
24///
25/// Not authored in world files: it has no `args`.
26#[derive(Debug)]
27pub struct SkeletonPose {
28    /// The `SkinnedMesh` resource this pose belongs to. Used by
29    /// `AnimationSystem` to match an `Animation` clip to its target.
30    pub mesh_id: SkinnedMeshHandle,
31    /// Index of this mesh's skinned draw object in the render backend.
32    pub skinned_index: usize,
33    /// Bind-pose joint hierarchy, used to compose skinning matrices.
34    pub skeleton: Skeleton,
35    /// Current skinning matrices, one per joint. Seeded to the bind pose
36    /// (identity skinning) and overwritten by `AnimationSystem` each frame.
37    pub joint_matrices: Vec<Mat4>,
38    /// Current morph-target weights, one per target of the mesh: the base
39    /// layer plus whatever a clip's morph track adds. Empty for a mesh with
40    /// neither a base layer nor a morph clip.
41    pub morph_weights: Vec<f32>,
42    /// Static morph weights from the mesh's `CharacterShape`, one per target;
43    /// empty without one. Clip morph tracks are added onto these.
44    pub morph_base: Vec<f32>,
45    /// Per-joint proportion changes from the mesh's `CharacterShape`, applied
46    /// to every sampled pose before the skinning matrices are built.
47    pub proportions: ProportionLayer,
48    /// True while `joint_matrices` / `morph_weights` hold data the render
49    /// backend has not consumed yet. Set by whoever writes the pose, cleared
50    /// after upload, so an unanimated pose is uploaded exactly once.
51    pub updated: bool,
52    /// Reusable sampling buffers owned by this pose, so the per-frame
53    /// sample/blend/skinning chain allocates nothing in steady state.
54    pub scratch: PoseScratch,
55}
56
57impl SkeletonPose {
58    /// Build a pose for `mesh_id`'s skinned draw object, seeded to the bind
59    /// pose so the mesh renders undeformed until an animation drives it.
60    pub fn new(mesh_id: SkinnedMeshHandle, skinned_index: usize, skeleton: Skeleton) -> Self {
61        let joint_matrices = skeleton.bind_skinning_matrices();
62        Self {
63            mesh_id,
64            skinned_index,
65            skeleton,
66            joint_matrices,
67            morph_weights: Vec::new(),
68            morph_base: Vec::new(),
69            proportions: ProportionLayer::default(),
70            updated: true,
71            scratch: PoseScratch::default(),
72        }
73    }
74
75    /// Install the static shape layers and re-seed the rest pose through them,
76    /// so a mesh with no clip renders shaped.
77    pub fn with_shape(mut self, morph_base: Vec<f32>, proportions: ProportionLayer) -> Self {
78        self.set_shape(morph_base, proportions);
79        self
80    }
81
82    /// Replace the static shape layers in place and re-seed the rest pose
83    /// through them; an animated pose picks the new layers up on its next
84    /// sample. For editing a shape on a live pose without rebuilding it.
85    pub fn set_shape(&mut self, morph_base: Vec<f32>, proportions: ProportionLayer) {
86        self.morph_weights = morph_base.clone();
87        self.morph_base = morph_base;
88        self.proportions = proportions;
89        self.scratch.locals.clear();
90        self.scratch
91            .locals
92            .extend_from_slice(self.skeleton.bind_locals());
93        self.proportions.apply(&mut self.scratch.locals);
94        self.skeleton
95            .skinning_matrices_into(&self.scratch.locals, &mut self.joint_matrices);
96        self.updated = true;
97    }
98
99    /// A fresh pose sharing this one's skeleton and shape layers, for a
100    /// runtime-spawned copy of the mesh at draw slot `skinned_index`.
101    pub fn clone_for_slot(&self, skinned_index: usize) -> Self {
102        Self::new(self.mesh_id, skinned_index, self.skeleton.clone())
103            .with_shape(self.morph_base.clone(), self.proportions.clone())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::gfx::skeleton::{Joint, JointPose};
111    use alloc::string::String;
112    use alloc::vec;
113
114    fn two_joint_chain() -> Skeleton {
115        Skeleton::new(vec![
116            Joint {
117                name: String::from("root"),
118                parent: None,
119                bind: JointPose::default(),
120            },
121            Joint {
122                name: String::from("tip"),
123                parent: Some(0),
124                bind: JointPose {
125                    translation: [0.0, 1.0, 0.0],
126                    ..Default::default()
127                },
128            },
129        ])
130    }
131
132    #[test]
133    fn a_shaped_rest_pose_is_seeded_through_the_layers() {
134        let skeleton = two_joint_chain();
135        let layer = ProportionLayer::resolve(
136            &skeleton,
137            &[crate::components::JointProportion {
138                joint: String::from("root"),
139                scale: 2.0,
140                length: 0.0,
141            }],
142        );
143        let pose =
144            SkeletonPose::new(SkinnedMeshHandle(1), 3, skeleton).with_shape(vec![0.25, 0.5], layer);
145        assert_eq!(pose.morph_weights, [0.25, 0.5]);
146        // Root doubled: its skinning matrix is a pure scale of 2 (bind is
147        // identity there), and the tip's bind position (0, 1, 0) skins to
148        // (0, 2, 0).
149        assert_eq!(pose.joint_matrices[0][0][0], 2.0);
150        let tip = pose.joint_matrices[1];
151        assert!((tip[1][1] + tip[3][1] - 2.0).abs() < 1e-5, "{tip:?}");
152        assert!(pose.updated);
153        let copy = pose.clone_for_slot(7);
154        assert_eq!(copy.skinned_index, 7);
155        assert_eq!(copy.joint_matrices, pose.joint_matrices);
156        assert_eq!(copy.morph_base, pose.morph_base);
157    }
158}