use glam::Mat4;
#[derive(Debug, Clone)]
pub struct Joint {
pub name: String,
pub parent: Option<usize>,
pub inverse_bind_matrix: Mat4,
}
#[derive(Debug, Clone)]
pub struct Skeleton {
pub joints: Vec<Joint>,
}
impl Skeleton {
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(Debug, Clone)]
pub struct SkinData {
pub joint_indices: Vec<[u16; 4]>,
pub joint_weights: Vec<[f32; 4]>,
}
impl SkinData {
pub fn vertex_count(&self) -> usize {
self.joint_indices.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Interpolation {
Step,
Linear,
CubicSpline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationProperty {
Translation,
Rotation,
Scale,
}
#[derive(Debug, Clone)]
pub struct AnimationChannel {
pub joint_index: usize,
pub property: AnimationProperty,
pub interpolation: Interpolation,
pub keyframes: Vec<Keyframe>,
}
impl AnimationChannel {
pub fn duration(&self) -> f32 {
self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
}
pub fn sample(&self, time: f32) -> [f32; 4] {
if self.keyframes.is_empty() {
return [0.0; 4];
}
if time <= self.keyframes[0].time {
return self.keyframes[0].value;
}
if time >= self.keyframes.last().unwrap().time {
return self.keyframes.last().unwrap().value;
}
for i in 0..self.keyframes.len() - 1 {
let a = &self.keyframes[i];
let b = &self.keyframes[i + 1];
if time >= a.time && time <= b.time {
match self.interpolation {
Interpolation::Step => return a.value,
Interpolation::Linear => {
let t = (time - a.time) / (b.time - a.time);
return [
a.value[0] + (b.value[0] - a.value[0]) * t,
a.value[1] + (b.value[1] - a.value[1]) * t,
a.value[2] + (b.value[2] - a.value[2]) * t,
a.value[3] + (b.value[3] - a.value[3]) * t,
];
}
Interpolation::CubicSpline => {
let dt_seg = b.time - a.time;
let t = (time - a.time) / dt_seg;
let t2 = t * t;
let t3 = t2 * t;
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h01 = -2.0 * t3 + 3.0 * t2;
return [
h00 * a.value[0] + h01 * b.value[0],
h00 * a.value[1] + h01 * b.value[1],
h00 * a.value[2] + h01 * b.value[2],
h00 * a.value[3] + h01 * b.value[3],
];
}
}
}
}
self.keyframes.last().unwrap().value
}
}
#[derive(Debug, Clone)]
pub struct Keyframe {
pub time: f32,
pub value: [f32; 4],
}
#[derive(Debug, Clone)]
pub struct AnimationClip {
pub name: String,
pub channels: Vec<AnimationChannel>,
}
impl AnimationClip {
pub fn duration(&self) -> f32 {
self.channels.iter().map(|c| c.duration()).fold(0.0f32, f32::max)
}
}
#[derive(Debug, Clone)]
pub struct AnimationPlayer {
pub clip: AnimationClip,
pub current_time: f32,
pub playing: bool,
pub looping: bool,
pub speed: f32,
}
impl AnimationPlayer {
pub fn new(clip: AnimationClip) -> Self {
Self {
clip,
current_time: 0.0,
playing: false,
looping: true,
speed: 1.0,
}
}
pub fn advance(&mut self, dt: f32) {
if !self.playing { return; }
self.current_time += dt * self.speed;
let duration = self.clip.duration();
if duration > 0.0 && self.current_time > duration {
if self.looping {
self.current_time %= duration;
} else {
self.current_time = duration;
self.playing = false;
}
}
}
}
pub fn compute_bone_matrices(skeleton: &Skeleton, player: &AnimationPlayer) -> Vec<Mat4> {
let n = skeleton.joint_count();
let mut local_transforms: Vec<Mat4> = vec![Mat4::IDENTITY; n];
let mut translations: Vec<Option<glam::Vec3>> = vec![None; n];
let mut rotations: Vec<Option<glam::Quat>> = vec![None; n];
let mut scales: Vec<Option<glam::Vec3>> = vec![None; n];
for channel in &player.clip.channels {
let idx = channel.joint_index;
if idx >= n { continue; }
let value = channel.sample(player.current_time);
match channel.property {
AnimationProperty::Translation => {
translations[idx] = Some(glam::Vec3::new(value[0], value[1], value[2]));
}
AnimationProperty::Rotation => {
rotations[idx] = Some(
glam::Quat::from_xyzw(value[0], value[1], value[2], value[3]).normalize()
);
}
AnimationProperty::Scale => {
scales[idx] = Some(glam::Vec3::new(value[0], value[1], value[2]));
}
}
}
for idx in 0..n {
let t = translations[idx].unwrap_or(glam::Vec3::ZERO);
let r = rotations[idx].unwrap_or(glam::Quat::IDENTITY);
let s = scales[idx].unwrap_or(glam::Vec3::ONE);
local_transforms[idx] = Mat4::from_scale_rotation_translation(s, r, t);
}
let mut global_transforms: Vec<Mat4> = vec![Mat4::IDENTITY; n];
for j in 0..n {
global_transforms[j] = if let Some(parent) = skeleton.joints[j].parent {
global_transforms[parent] * local_transforms[j]
} else {
local_transforms[j]
};
}
global_transforms
.iter()
.enumerate()
.map(|(j, g)| *g * skeleton.joints[j].inverse_bind_matrix)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skeleton() {
let skeleton = Skeleton {
joints: vec![
Joint { name: "Root".into(), parent: None, inverse_bind_matrix: Mat4::IDENTITY },
Joint { name: "Spine".into(), parent: Some(0), inverse_bind_matrix: Mat4::IDENTITY },
],
};
assert_eq!(skeleton.joint_count(), 2);
assert_eq!(skeleton.find_joint("Spine"), Some(1));
assert_eq!(skeleton.find_joint("Unknown"), None);
}
#[test]
fn test_channel_sample() {
let channel = AnimationChannel {
joint_index: 0,
property: AnimationProperty::Translation,
interpolation: Interpolation::Linear,
keyframes: vec![
Keyframe { time: 0.0, value: [0.0, 0.0, 0.0, 0.0] },
Keyframe { time: 1.0, value: [2.0, 0.0, 0.0, 0.0] },
],
};
let v = channel.sample(0.5);
assert!((v[0] - 1.0).abs() < 0.001);
}
#[test]
fn test_animation_player_loop() {
let clip = AnimationClip {
name: "Test".into(),
channels: vec![AnimationChannel {
joint_index: 0,
property: AnimationProperty::Translation,
interpolation: Interpolation::Linear,
keyframes: vec![
Keyframe { time: 0.0, value: [0.0; 4] },
Keyframe { time: 2.0, value: [1.0; 4] },
],
}],
};
let mut player = AnimationPlayer::new(clip);
player.playing = true;
player.looping = true;
player.advance(2.5);
assert!((player.current_time - 0.5).abs() < 0.001);
}
#[test]
fn test_compute_bone_matrices_identity() {
let skeleton = Skeleton {
joints: vec![
Joint { name: "Root".into(), parent: None, inverse_bind_matrix: Mat4::IDENTITY },
],
};
let clip = AnimationClip { name: "Empty".into(), channels: vec![] };
let player = AnimationPlayer::new(clip);
let matrices = compute_bone_matrices(&skeleton, &player);
assert_eq!(matrices.len(), 1);
let diff = (matrices[0] - Mat4::IDENTITY).abs_diff_eq(Mat4::ZERO, 0.001);
assert!(diff);
}
#[test]
fn test_compute_bone_matrices_translation() {
let skeleton = Skeleton {
joints: vec![
Joint { name: "Root".into(), parent: None, inverse_bind_matrix: Mat4::IDENTITY },
],
};
let clip = AnimationClip {
name: "Move".into(),
channels: vec![AnimationChannel {
joint_index: 0,
property: AnimationProperty::Translation,
interpolation: Interpolation::Linear,
keyframes: vec![
Keyframe { time: 0.0, value: [0.0, 0.0, 0.0, 0.0] },
Keyframe { time: 1.0, value: [2.0, 0.0, 0.0, 0.0] },
],
}],
};
let mut player = AnimationPlayer::new(clip);
player.current_time = 0.5;
let matrices = compute_bone_matrices(&skeleton, &player);
let col3 = matrices[0].col(3);
assert!((col3.x - 1.0).abs() < 0.01);
}
}