use crate::resources::SkinWeights;
pub const MAX_JOINTS: usize = 128;
#[derive(Clone)]
pub struct Joint {
pub name: String,
pub parent: Option<u8>,
pub inverse_bind: glam::Affine3A,
}
#[derive(Clone)]
pub struct Skeleton {
joints: Vec<Joint>,
}
impl Skeleton {
pub fn new(joints: Vec<Joint>) -> Self {
debug_assert!(joints.len() <= MAX_JOINTS, "skeleton exceeds MAX_JOINTS");
for (i, j) in joints.iter().enumerate() {
if let Some(p) = j.parent {
debug_assert!((p as usize) < i, "joint {i} has parent {p} >= own index");
}
}
Self { joints }
}
pub fn joints(&self) -> &[Joint] {
&self.joints
}
pub fn joint_count(&self) -> usize {
self.joints.len()
}
pub fn find_joint(&self, name: &str) -> Option<usize> {
self.joints.iter().position(|j| j.name == name)
}
}
#[derive(Clone)]
pub struct Pose {
pub local_transforms: Vec<glam::Affine3A>,
}
impl Pose {
pub fn identity(joint_count: usize) -> Self {
Self {
local_transforms: vec![glam::Affine3A::IDENTITY; joint_count],
}
}
pub fn joint_count(&self) -> usize {
self.local_transforms.len()
}
}
pub struct JointMatrices {
matrices: Vec<glam::Affine3A>,
}
impl JointMatrices {
pub fn compute(skeleton: &Skeleton, pose: &Pose) -> Self {
let n = skeleton.joint_count();
let mut world = vec![glam::Affine3A::IDENTITY; n];
for (i, joint) in skeleton.joints().iter().enumerate() {
let local = pose
.local_transforms
.get(i)
.copied()
.unwrap_or(glam::Affine3A::IDENTITY);
world[i] = match joint.parent {
Some(p) => world[p as usize] * local,
None => local,
};
}
let matrices = world
.iter()
.zip(skeleton.joints().iter())
.map(|(w, j)| *w * j.inverse_bind)
.collect();
Self { matrices }
}
pub fn as_slice(&self) -> &[glam::Affine3A] {
&self.matrices
}
}
pub fn apply_skin(
positions: &[[f32; 3]],
normals: &[[f32; 3]],
weights: &SkinWeights,
joint_matrices: &JointMatrices,
) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
let n = positions.len();
let mut out_pos = vec![[0.0f32; 3]; n];
let mut out_nrm = vec![[0.0f32; 3]; n];
for i in 0..n {
let p = glam::Vec3::from(positions[i]);
let nm = glam::Vec3::from(normals[i]);
let indices = weights.joint_indices[i];
let ws = weights.joint_weights[i];
let mut blended_p = glam::Vec3::ZERO;
let mut blended_n = glam::Vec3::ZERO;
for k in 0..4 {
let w = ws[k];
if w < 1e-6 {
continue;
}
let m = joint_matrices.matrices[indices[k] as usize];
blended_p += w * m.transform_point3(p);
blended_n += w * m.transform_vector3(nm);
}
out_pos[i] = blended_p.to_array();
out_nrm[i] = blended_n.normalize_or_zero().to_array();
}
(out_pos, out_nrm)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resources::SkinWeights;
use glam::{Affine3A, Vec3};
fn two_joint_skeleton(joint_z: f32) -> Skeleton {
Skeleton::new(vec![
Joint {
name: "root".into(),
parent: None,
inverse_bind: Affine3A::IDENTITY,
},
Joint {
name: "child".into(),
parent: Some(0),
inverse_bind: Affine3A::from_translation(-Vec3::new(0.0, 0.0, joint_z)),
},
])
}
fn bind_pose(joint_z: f32) -> Pose {
let mut p = Pose::identity(2);
p.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z));
p
}
fn approx_eq(a: [f32; 3], b: [f32; 3], eps: f32) -> bool {
(a[0] - b[0]).abs() < eps && (a[1] - b[1]).abs() < eps && (a[2] - b[2]).abs() < eps
}
#[test]
fn bind_pose_produces_identity_skinning_matrices() {
let joint_z = 2.0;
let sk = two_joint_skeleton(joint_z);
let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
for m in jm.as_slice() {
let p = m.transform_point3(Vec3::new(1.0, 2.0, 3.0));
assert!(
approx_eq(p.to_array(), [1.0, 2.0, 3.0], 1e-5),
"got {:?}",
p
);
}
}
#[test]
fn apply_skin_at_bind_pose_returns_input() {
let joint_z = 2.0;
let sk = two_joint_skeleton(joint_z);
let jm = JointMatrices::compute(&sk, &bind_pose(joint_z));
let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 1.0], [0.0, 0.0, 4.0]];
let normals = vec![[1.0, 0.0, 0.0]; 3];
let weights = SkinWeights {
joint_indices: vec![[0, 1, 0, 0]; 3],
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],
],
};
let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
for i in 0..3 {
assert!(
approx_eq(out_p[i], positions[i], 1e-5),
"pos {i}: {:?}",
out_p[i]
);
assert!(
approx_eq(out_n[i], normals[i], 1e-5),
"nrm {i}: {:?}",
out_n[i]
);
}
}
#[test]
fn child_rotation_bends_around_joint() {
let joint_z = 2.0;
let sk = two_joint_skeleton(joint_z);
let mut pose = bind_pose(joint_z);
pose.local_transforms[1] = Affine3A::from_translation(Vec3::new(0.0, 0.0, joint_z))
* Affine3A::from_rotation_x(std::f32::consts::FRAC_PI_2);
let jm = JointMatrices::compute(&sk, &pose);
let positions = vec![[0.0, 0.0, joint_z + 1.0]];
let normals = vec![[0.0, 0.0, 1.0]];
let weights = SkinWeights {
joint_indices: vec![[0, 1, 0, 0]],
joint_weights: vec![[0.0, 1.0, 0.0, 0.0]],
};
let (out_p, out_n) = apply_skin(&positions, &normals, &weights, &jm);
assert!(
approx_eq(out_p[0], [0.0, -1.0, joint_z], 1e-4),
"got {:?}",
out_p[0]
);
assert!(
approx_eq(out_n[0], [0.0, -1.0, 0.0], 1e-4),
"got {:?}",
out_n[0]
);
}
#[test]
fn zero_weight_slots_are_skipped() {
let joint_z = 2.0;
let sk = two_joint_skeleton(joint_z);
let mut pose = bind_pose(joint_z);
pose.local_transforms[1] =
pose.local_transforms[1] * Affine3A::from_translation(Vec3::new(100.0, 0.0, 0.0));
let jm = JointMatrices::compute(&sk, &pose);
let positions = vec![[0.0, 0.0, 0.0]];
let normals = vec![[1.0, 0.0, 0.0]];
let weights = SkinWeights {
joint_indices: vec![[0, 1, 1, 1]],
joint_weights: vec![[1.0, 0.0, 0.0, 0.0]],
};
let (out_p, _) = apply_skin(&positions, &normals, &weights, &jm);
assert!(approx_eq(out_p[0], [0.0, 0.0, 0.0], 1e-5));
}
}