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