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
139 .local_transforms
140 .get(i)
141 .copied()
142 .unwrap_or(glam::Affine3A::IDENTITY);
143 world[i] = match joint.parent {
144 Some(p) => world[p as usize] * local,
145 None => local,
146 };
147 }
148
149 let matrices = world
150 .iter()
151 .zip(skeleton.joints().iter())
152 .map(|(w, j)| *w * j.inverse_bind)
153 .collect();
154
155 Self { matrices }
156 }
157
158 pub fn as_slice(&self) -> &[glam::Affine3A] {
160 &self.matrices
161 }
162}
163
164pub fn apply_skin(
173 positions: &[[f32; 3]],
174 normals: &[[f32; 3]],
175 weights: &SkinWeights,
176 joint_matrices: &JointMatrices,
177) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
178 let n = positions.len();
179 let mut out_pos = vec![[0.0f32; 3]; n];
180 let mut out_nrm = vec![[0.0f32; 3]; n];
181
182 for i in 0..n {
183 let p = glam::Vec3::from(positions[i]);
184 let nm = glam::Vec3::from(normals[i]);
185 let indices = weights.joint_indices[i];
186 let ws = weights.joint_weights[i];
187
188 let mut blended_p = glam::Vec3::ZERO;
189 let mut blended_n = glam::Vec3::ZERO;
190
191 for k in 0..4 {
192 let w = ws[k];
193 if w < 1e-6 {
194 continue;
195 }
196 let m = joint_matrices.matrices[indices[k] as usize];
197 blended_p += w * m.transform_point3(p);
198 blended_n += w * m.transform_vector3(nm);
199 }
200
201 out_pos[i] = blended_p.to_array();
202 out_nrm[i] = blended_n.normalize_or_zero().to_array();
203 }
204
205 (out_pos, out_nrm)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::resources::SkinWeights;
212 use glam::{Affine3A, Vec3};
213
214 fn two_joint_skeleton(joint_z: f32) -> Skeleton {
215 Skeleton::new(vec![
216 Joint {
217 name: "root".into(),
218 parent: None,
219 inverse_bind: Affine3A::IDENTITY,
220 },
221 Joint {
222 name: "child".into(),
223 parent: Some(0),
224 inverse_bind: Affine3A::from_translation(-Vec3::new(0.0, 0.0, joint_z)),
225 },
226 ])
227 }
228
229 fn bind_pose(joint_z: f32) -> Pose {
233 let mut p = Pose::identity(2);
234 p.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z));
235 p
236 }
237
238 fn approx_eq(a: [f32; 3], b: [f32; 3], eps: f32) -> bool {
239 (a[0] - b[0]).abs() < eps && (a[1] - b[1]).abs() < eps && (a[2] - b[2]).abs() < eps
240 }
241
242 #[test]
243 fn bind_pose_produces_identity_skinning_matrices() {
244 let joint_z = 2.0;
245 let sk = two_joint_skeleton(joint_z);
246 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
247 for m in jm.as_slice() {
248 let p = m.transform_point3(Vec3::new(1.0, 2.0, 3.0));
249 assert!(
250 approx_eq(p.to_array(), [1.0, 2.0, 3.0], 1e-5),
251 "got {:?}",
252 p
253 );
254 }
255 }
256
257 #[test]
258 fn apply_skin_at_bind_pose_returns_input() {
259 let joint_z = 2.0;
260 let sk = two_joint_skeleton(joint_z);
261 let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
262 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 1.0], [0.0, 0.0, 4.0]];
263 let normals = vec![[1.0, 0.0, 0.0]; 3];
264 let weights = SkinWeights {
265 joint_indices: vec![[0, 1, 0, 0]; 3],
266 joint_weights: vec![
267 [1.0, 0.0, 0.0, 0.0],
268 [0.5, 0.5, 0.0, 0.0],
269 [0.0, 1.0, 0.0, 0.0],
270 ],
271 };
272 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
273 for i in 0..3 {
274 assert!(
275 approx_eq(out_p[i], positions[i], 1e-5),
276 "pos {i}: {:?}",
277 out_p[i]
278 );
279 assert!(
280 approx_eq(out_n[i], normals[i], 1e-5),
281 "nrm {i}: {:?}",
282 out_n[i]
283 );
284 }
285 }
286
287 #[test]
288 fn child_rotation_bends_around_joint() {
289 let joint_z = 2.0;
292 let sk = two_joint_skeleton(joint_z);
293 let mut pose = bind_pose(joint_z);
294 pose.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z))
295 * Affine3A::from_rotation_x(std::f32::consts::FRAC_PI_2);
296 let jm = JointMatrices::compute(&sk, &pose);
297
298 let positions = vec![[0.0, 0.0, joint_z + 1.0]];
299 let normals = vec![[0.0, 0.0, 1.0]];
300 let weights = SkinWeights {
301 joint_indices: vec![[0, 1, 0, 0]],
302 joint_weights: vec![[0.0, 1.0, 0.0, 0.0]],
303 };
304 let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
305 assert!(
306 approx_eq(out_p[0], [0.0, -1.0, joint_z], 1e-4),
307 "got {:?}",
308 out_p[0]
309 );
310 assert!(
311 approx_eq(out_n[0], [0.0, -1.0, 0.0], 1e-4),
312 "got {:?}",
313 out_n[0]
314 );
315 }
316
317 #[test]
318 fn zero_weight_slots_are_skipped() {
319 let joint_z = 2.0;
320 let sk = two_joint_skeleton(joint_z);
321 let mut pose = bind_pose(joint_z);
322 pose.local_transforms[1] =
324 pose.local_transforms[1] * Affine3A::from_translation(Vec3::new(100.0, 0.0, 0.0));
325 let jm = JointMatrices::compute(&sk, &pose);
326
327 let positions = vec![[0.0, 0.0, 0.0]];
328 let normals = vec![[1.0, 0.0, 0.0]];
329 let weights = SkinWeights {
330 joint_indices: vec![[0, 1, 1, 1]],
331 joint_weights: vec![[1.0, 0.0, 0.0, 0.0]],
332 };
333 let (out_p, _) = apply_skin(&positions, &normals, &weights, &jm);
334 assert!(approx_eq(out_p[0], [0.0, 0.0, 0.0], 1e-5));
335 }
336}