viewport_lib/runtime/plugins/skeleton_plugin/
skeleton.rs1use crate::resources::SkinWeights;
30
31pub const MAX_JOINTS: usize = 128;
33
34#[derive(Clone)]
36pub struct Joint {
37 pub name: String,
39 pub parent: Option<u8>,
44 pub inverse_bind: glam::Affine3A,
49}
50
51#[derive(Clone)]
57pub struct Skeleton {
58 joints: Vec<Joint>,
59}
60
61impl Skeleton {
62 pub fn new(joints: Vec<Joint>) -> Self {
67 debug_assert!(joints.len() <= MAX_JOINTS, "skeleton exceeds MAX_JOINTS");
68 for (i, j) in joints.iter().enumerate() {
69 if let Some(p) = j.parent {
70 debug_assert!((p as usize) < i, "joint {i} has parent {p} >= own index");
71 }
72 }
73 Self { joints }
74 }
75
76 pub fn joints(&self) -> &[Joint] {
78 &self.joints
79 }
80
81 pub fn joint_count(&self) -> usize {
83 self.joints.len()
84 }
85
86 pub fn find_joint(&self, name: &str) -> Option<usize> {
88 self.joints.iter().position(|j| j.name == name)
89 }
90}
91
92#[derive(Clone)]
99pub struct Pose {
100 pub local_transforms: Vec<glam::Affine3A>,
103}
104
105impl Pose {
106 pub fn identity(joint_count: usize) -> Self {
108 Self {
109 local_transforms: vec![glam::Affine3A::IDENTITY; joint_count],
110 }
111 }
112
113 pub fn joint_count(&self) -> usize {
115 self.local_transforms.len()
116 }
117}
118
119pub struct JointMatrices {
125 matrices: Vec<glam::Affine3A>,
126}
127
128impl JointMatrices {
129 pub fn compute(skeleton: &Skeleton, pose: &Pose) -> Self {
134 let n = skeleton.joint_count();
135 let mut world = vec![glam::Affine3A::IDENTITY; n];
136
137 for (i, joint) in skeleton.joints().iter().enumerate() {
138 let local = pose.local_transforms.get(i).copied().unwrap_or(glam::Affine3A::IDENTITY);
139 world[i] = match joint.parent {
140 Some(p) => world[p as usize] * local,
141 None => local,
142 };
143 }
144
145 let matrices = world.iter().zip(skeleton.joints().iter())
146 .map(|(w, j)| *w * j.inverse_bind)
147 .collect();
148
149 Self { matrices }
150 }
151
152 pub fn as_slice(&self) -> &[glam::Affine3A] {
154 &self.matrices
155 }
156}
157
158pub fn apply_skin(
167 positions: &[[f32; 3]],
168 normals: &[[f32; 3]],
169 weights: &SkinWeights,
170 joint_matrices: &JointMatrices,
171) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
172 let n = positions.len();
173 let mut out_pos = vec![[0.0f32; 3]; n];
174 let mut out_nrm = vec![[0.0f32; 3]; n];
175
176 for i in 0..n {
177 let p = glam::Vec3::from(positions[i]);
178 let nm = glam::Vec3::from(normals[i]);
179 let indices = weights.joint_indices[i];
180 let ws = weights.joint_weights[i];
181
182 let mut blended_p = glam::Vec3::ZERO;
183 let mut blended_n = glam::Vec3::ZERO;
184
185 for k in 0..4 {
186 let w = ws[k];
187 if w < 1e-6 {
188 continue;
189 }
190 let m = joint_matrices.matrices[indices[k] as usize];
191 blended_p += w * m.transform_point3(p);
192 blended_n += w * m.transform_vector3(nm);
193 }
194
195 out_pos[i] = blended_p.to_array();
196 out_nrm[i] = blended_n.normalize_or_zero().to_array();
197 }
198
199 (out_pos, out_nrm)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::resources::SkinWeights;
206 use glam::{Affine3A, Vec3};
207
208 fn two_joint_skeleton(joint_z: f32) -> Skeleton {
209 Skeleton::new(vec![
210 Joint {
211 name: "root".into(),
212 parent: None,
213 inverse_bind: Affine3A::IDENTITY,
214 },
215 Joint {
216 name: "child".into(),
217 parent: Some(0),
218 inverse_bind: Affine3A::from_translation(-Vec3::new(0.0, 0.0, joint_z)),
219 },
220 ])
221 }
222
223 fn bind_pose(joint_z: f32) -> Pose {
227 let mut p = Pose::identity(2);
228 p.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z));
229 p
230 }
231
232 fn approx_eq(a: [f32; 3], b: [f32; 3], eps: f32) -> bool {
233 (a[0] - b[0]).abs() < eps && (a[1] - b[1]).abs() < eps && (a[2] - b[2]).abs() < eps
234 }
235
236 #[test]
237 fn bind_pose_produces_identity_skinning_matrices() {
238 let joint_z = 2.0;
239 let sk = two_joint_skeleton(joint_z);
240 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
241 for m in jm.as_slice() {
242 let p = m.transform_point3(Vec3::new(1.0, 2.0, 3.0));
243 assert!(approx_eq(p.to_array(), [1.0, 2.0, 3.0], 1e-5), "got {:?}", p);
244 }
245 }
246
247 #[test]
248 fn apply_skin_at_bind_pose_returns_input() {
249 let joint_z = 2.0;
250 let sk = two_joint_skeleton(joint_z);
251 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
252 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 1.0], [0.0, 0.0, 4.0]];
253 let normals = vec![[1.0, 0.0, 0.0]; 3];
254 let weights = SkinWeights {
255 joint_indices: vec![[0, 1, 0, 0]; 3],
256 joint_weights: vec![[1.0, 0.0, 0.0, 0.0], [0.5, 0.5, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]],
257 };
258 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
259 for i in 0..3 {
260 assert!(approx_eq(out_p[i], positions[i], 1e-5), "pos {i}: {:?}", out_p[i]);
261 assert!(approx_eq(out_n[i], normals[i], 1e-5), "nrm {i}: {:?}", out_n[i]);
262 }
263 }
264
265 #[test]
266 fn child_rotation_bends_around_joint() {
267 let joint_z = 2.0;
270 let sk = two_joint_skeleton(joint_z);
271 let mut pose = bind_pose(joint_z);
272 pose.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z))
273 * Affine3A::from_rotation_x(std::f32::consts::FRAC_PI_2);
274 let jm = JointMatrices::compute(&sk, &pose);
275
276 let positions = vec![[0.0, 0.0, joint_z + 1.0]];
277 let normals = vec![[0.0, 0.0, 1.0]];
278 let weights = SkinWeights {
279 joint_indices: vec![[0, 1, 0, 0]],
280 joint_weights: vec![[0.0, 1.0, 0.0, 0.0]],
281 };
282 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
283 assert!(approx_eq(out_p[0], [0.0, -1.0, joint_z], 1e-4), "got {:?}", out_p[0]);
284 assert!(approx_eq(out_n[0], [0.0, -1.0, 0.0], 1e-4), "got {:?}", out_n[0]);
285 }
286
287 #[test]
288 fn zero_weight_slots_are_skipped() {
289 let joint_z = 2.0;
290 let sk = two_joint_skeleton(joint_z);
291 let mut pose = bind_pose(joint_z);
292 pose.local_transforms[1] = pose.local_transforms[1]
294 * Affine3A::from_translation(Vec3::new(100.0, 0.0, 0.0));
295 let jm = JointMatrices::compute(&sk, &pose);
296
297 let positions = vec![[0.0, 0.0, 0.0]];
298 let normals = vec![[1.0, 0.0, 0.0]];
299 let weights = SkinWeights {
300 joint_indices: vec![[0, 1, 1, 1]],
301 joint_weights: vec![[1.0, 0.0, 0.0, 0.0]],
302 };
303 let (out_p, _) = apply_skin(&positions, &normals, &weights, &jm);
304 assert!(approx_eq(out_p[0], [0.0, 0.0, 0.0], 1e-5));
305 }
306}