viewport_lib/plugins/skeleton/
skeleton.rs1use crate::plugins::skinning::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,
52}
53
54#[derive(Clone)]
60pub struct Skeleton {
61 joints: Vec<Joint>,
62}
63
64impl Skeleton {
65 pub fn new(joints: Vec<Joint>) -> Self {
70 debug_assert!(joints.len() <= MAX_JOINTS, "skeleton exceeds MAX_JOINTS");
71 for (i, j) in joints.iter().enumerate() {
72 if let Some(p) = j.parent {
73 debug_assert!((p as usize) < i, "joint {i} has parent {p} >= own index");
74 }
75 }
76 Self { joints }
77 }
78
79 pub fn joints(&self) -> &[Joint] {
81 &self.joints
82 }
83
84 pub fn joint_count(&self) -> usize {
86 self.joints.len()
87 }
88
89 pub fn find_joint(&self, name: &str) -> Option<usize> {
91 self.joints.iter().position(|j| j.name == name)
92 }
93}
94
95#[derive(Clone)]
102pub struct Pose {
103 pub local_transforms: Vec<glam::Affine3A>,
106}
107
108impl Pose {
109 pub fn identity(joint_count: usize) -> Self {
111 Self {
112 local_transforms: vec![glam::Affine3A::IDENTITY; joint_count],
113 }
114 }
115
116 pub fn joint_count(&self) -> usize {
118 self.local_transforms.len()
119 }
120}
121
122pub struct JointMatrices {
131 matrices: Vec<glam::Affine3A>,
132}
133
134impl JointMatrices {
135 pub fn compute(skeleton: &Skeleton, pose: &Pose) -> Self {
140 let n = skeleton.joint_count();
141 let mut world = vec![glam::Affine3A::IDENTITY; n];
142
143 for (i, joint) in skeleton.joints().iter().enumerate() {
144 let local = pose
145 .local_transforms
146 .get(i)
147 .copied()
148 .unwrap_or(glam::Affine3A::IDENTITY);
149 world[i] = match joint.parent {
150 Some(p) => world[p as usize] * local,
151 None => local,
152 };
153 }
154
155 let matrices = world
156 .iter()
157 .zip(skeleton.joints().iter())
158 .map(|(w, j)| *w * j.inverse_bind)
159 .collect();
160
161 Self { matrices }
162 }
163
164 pub fn as_slice(&self) -> &[glam::Affine3A] {
166 &self.matrices
167 }
168}
169
170pub fn apply_skin(
179 positions: &[[f32; 3]],
180 normals: &[[f32; 3]],
181 weights: &SkinWeights,
182 joint_matrices: &JointMatrices,
183) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
184 let n = positions.len();
185 let mut out_pos = vec![[0.0f32; 3]; n];
186 let mut out_nrm = vec![[0.0f32; 3]; n];
187
188 for i in 0..n {
189 let p = glam::Vec3::from(positions[i]);
190 let nm = glam::Vec3::from(normals[i]);
191 let indices = weights.joint_indices[i];
192 let ws = weights.joint_weights[i];
193
194 let mut blended_p = glam::Vec3::ZERO;
195 let mut blended_n = glam::Vec3::ZERO;
196
197 for k in 0..4 {
198 let w = ws[k];
199 if w < 1e-6 {
200 continue;
201 }
202 let m = joint_matrices.matrices[indices[k] as usize];
203 blended_p += w * m.transform_point3(p);
204 blended_n += w * m.transform_vector3(nm);
205 }
206
207 out_pos[i] = blended_p.to_array();
208 out_nrm[i] = blended_n.normalize_or_zero().to_array();
209 }
210
211 (out_pos, out_nrm)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::plugins::skinning::SkinWeights;
218 use glam::{Affine3A, Vec3};
219
220 fn two_joint_skeleton(joint_z: f32) -> Skeleton {
221 Skeleton::new(vec![
222 Joint {
223 name: "root".into(),
224 parent: None,
225 inverse_bind: Affine3A::IDENTITY,
226 },
227 Joint {
228 name: "child".into(),
229 parent: Some(0),
230 inverse_bind: Affine3A::from_translation(-Vec3::new(0.0, 0.0, joint_z)),
231 },
232 ])
233 }
234
235 fn bind_pose(joint_z: f32) -> Pose {
239 let mut p = Pose::identity(2);
240 p.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z));
241 p
242 }
243
244 fn approx_eq(a: [f32; 3], b: [f32; 3], eps: f32) -> bool {
245 (a[0] - b[0]).abs() < eps && (a[1] - b[1]).abs() < eps && (a[2] - b[2]).abs() < eps
246 }
247
248 #[test]
249 fn bind_pose_produces_identity_skinning_matrices() {
250 let joint_z = 2.0;
251 let sk = two_joint_skeleton(joint_z);
252 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
253 for m in jm.as_slice() {
254 let p = m.transform_point3(Vec3::new(1.0, 2.0, 3.0));
255 assert!(
256 approx_eq(p.to_array(), [1.0, 2.0, 3.0], 1e-5),
257 "got {:?}",
258 p
259 );
260 }
261 }
262
263 #[test]
264 fn apply_skin_at_bind_pose_returns_input() {
265 let joint_z = 2.0;
266 let sk = two_joint_skeleton(joint_z);
267 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
268 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 1.0], [0.0, 0.0, 4.0]];
269 let normals = vec![[1.0, 0.0, 0.0]; 3];
270 let weights = SkinWeights {
271 joint_indices: vec![[0, 1, 0, 0]; 3],
272 joint_weights: vec![
273 [1.0, 0.0, 0.0, 0.0],
274 [0.5, 0.5, 0.0, 0.0],
275 [0.0, 1.0, 0.0, 0.0],
276 ],
277 };
278 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
279 for i in 0..3 {
280 assert!(
281 approx_eq(out_p[i], positions[i], 1e-5),
282 "pos {i}: {:?}",
283 out_p[i]
284 );
285 assert!(
286 approx_eq(out_n[i], normals[i], 1e-5),
287 "nrm {i}: {:?}",
288 out_n[i]
289 );
290 }
291 }
292
293 #[test]
294 fn child_rotation_bends_around_joint() {
295 let joint_z = 2.0;
298 let sk = two_joint_skeleton(joint_z);
299 let mut pose = bind_pose(joint_z);
300 pose.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z))
301 * Affine3A::from_rotation_x(std::f32::consts::FRAC_PI_2);
302 let jm = JointMatrices::compute(&sk, &pose);
303
304 let positions = vec![[0.0, 0.0, joint_z + 1.0]];
305 let normals = vec![[0.0, 0.0, 1.0]];
306 let weights = SkinWeights {
307 joint_indices: vec![[0, 1, 0, 0]],
308 joint_weights: vec![[0.0, 1.0, 0.0, 0.0]],
309 };
310 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
311 assert!(
312 approx_eq(out_p[0], [0.0, -1.0, joint_z], 1e-4),
313 "got {:?}",
314 out_p[0]
315 );
316 assert!(
317 approx_eq(out_n[0], [0.0, -1.0, 0.0], 1e-4),
318 "got {:?}",
319 out_n[0]
320 );
321 }
322
323 #[test]
324 fn zero_weight_slots_are_skipped() {
325 let joint_z = 2.0;
326 let sk = two_joint_skeleton(joint_z);
327 let mut pose = bind_pose(joint_z);
328 pose.local_transforms[1] =
330 pose.local_transforms[1] * Affine3A::from_translation(Vec3::new(100.0, 0.0, 0.0));
331 let jm = JointMatrices::compute(&sk, &pose);
332
333 let positions = vec![[0.0, 0.0, 0.0]];
334 let normals = vec![[1.0, 0.0, 0.0]];
335 let weights = SkinWeights {
336 joint_indices: vec![[0, 1, 1, 1]],
337 joint_weights: vec![[1.0, 0.0, 0.0, 0.0]],
338 };
339 let (out_p, _) = apply_skin(&positions, &normals, &weights, &jm);
340 assert!(approx_eq(out_p[0], [0.0, 0.0, 0.0], 1e-5));
341 }
342}