use gizmo_math::{Mat4, Quat, Vec3};
#[derive(Clone, Copy, Debug)]
pub struct Keyframe<T> {
pub time: f32,
pub value: T,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum InterpolationMode {
Linear,
Step,
CubicSpline,
}
#[derive(Clone, Debug)]
pub struct Track<T> {
pub target_node: usize,
pub target_node_name: Option<String>,
pub interpolation: InterpolationMode,
pub keyframes: Vec<Keyframe<T>>,
}
impl<T: Clone + Copy> Track<T> {
pub fn get_interpolated(
&self,
time: f32,
mut interpolator: impl FnMut(T, T, f32) -> T,
) -> Option<T> {
if self.keyframes.is_empty() {
return None;
}
if self.keyframes.len() == 1 || time <= self.keyframes[0].time {
return Some(self.keyframes[0].value);
}
let last_idx = self.keyframes.len() - 1;
if time >= self.keyframes[last_idx].time {
return Some(self.keyframes[last_idx].value);
}
let idx = self.keyframes.partition_point(|k| k.time < time);
if idx == 0 {
return Some(self.keyframes[0].value);
}
let i = idx - 1;
let k1 = &self.keyframes[i];
let k2 = &self.keyframes[(i + 1).min(last_idx)];
let dt = k2.time - k1.time;
let t = if dt > 0.0 { (time - k1.time) / dt } else { 0.0 };
match self.interpolation {
InterpolationMode::Step => Some(k1.value),
InterpolationMode::Linear | InterpolationMode::CubicSpline => {
Some(interpolator(k1.value, k2.value, t))
}
}
}
}
#[derive(Clone, Debug)]
pub struct AnimationClip {
pub name: String,
pub duration: f32,
pub translations: Vec<Track<Vec3>>,
pub rotations: Vec<Track<Quat>>,
pub scales: Vec<Track<Vec3>>,
}
#[derive(Clone, Debug)]
pub struct SkeletonJoint {
pub name: String,
pub node_index: usize, pub inverse_bind_matrix: Mat4,
pub parent_index: Option<usize>,
pub local_bind_transform: Mat4,
pub bind_translation: Vec3,
pub bind_rotation: Quat,
pub bind_scale: Vec3,
}
#[derive(Clone, Debug)]
pub struct SkeletonHierarchy {
pub joints: Vec<SkeletonJoint>,
pub root_transform: Mat4,
}
impl SkeletonHierarchy {
pub fn calculate_global_matrices(&self, local_poses: &[Mat4]) -> Vec<Mat4> {
let mut globals: Vec<Option<Mat4>> = vec![None; self.joints.len()];
let mut children_map = vec![vec![]; self.joints.len()];
let mut roots = Vec::new();
for (i, joint) in self.joints.iter().enumerate() {
if let Some(parent_idx) = joint.parent_index {
children_map[parent_idx].push(i);
} else {
roots.push(i);
}
}
let mut queue = roots;
while let Some(node) = queue.pop() {
let local_mat = local_poses[node];
let global_mat = if let Some(parent_idx) = self.joints[node].parent_index {
globals[parent_idx].unwrap() * local_mat
} else {
self.root_transform * local_mat
};
globals[node] = Some(global_mat);
for &child in &children_map[node] {
queue.push(child);
}
}
debug_assert!(
globals.iter().all(|g| g.is_some()),
"SkeletonHierarchy: Bazı joint'lere ulaşılamadı! Dairesel bağımlılık veya kopuk hiyerarşi olabilir."
);
globals.into_iter().map(|m| m.unwrap_or(Mat4::IDENTITY)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_track(keyframes: Vec<(f32, f32)>, interp: InterpolationMode) -> Track<f32> {
Track {
target_node: 0,
target_node_name: None,
interpolation: interp,
keyframes: keyframes.into_iter().map(|(t, v)| Keyframe { time: t, value: v }).collect(),
}
}
#[test]
fn test_track_empty() {
let track = make_track(vec![], InterpolationMode::Linear);
assert!(track.get_interpolated(0.5, |a, b, t| a + (b - a) * t).is_none());
}
#[test]
fn test_track_single_keyframe() {
let track = make_track(vec![(1.0, 42.0)], InterpolationMode::Linear);
assert_eq!(track.get_interpolated(0.0, |a, b, t| a + (b - a) * t), Some(42.0));
assert_eq!(track.get_interpolated(5.0, |a, b, t| a + (b - a) * t), Some(42.0));
}
#[test]
fn test_track_linear_interpolation() {
let track = make_track(vec![(0.0, 0.0), (1.0, 10.0)], InterpolationMode::Linear);
let v = track.get_interpolated(0.5, |a, b, t| a + (b - a) * t).unwrap();
assert!((v - 5.0).abs() < 0.001, "Expected 5.0, got {v}");
}
#[test]
fn test_track_step_interpolation() {
let track = make_track(vec![(0.0, 0.0), (1.0, 10.0)], InterpolationMode::Step);
let v = track.get_interpolated(0.5, |a, b, t| a + (b - a) * t).unwrap();
assert_eq!(v, 0.0, "Step mode should hold the first keyframe value");
}
#[test]
fn test_track_clamp_before_first() {
let track = make_track(vec![(1.0, 5.0), (2.0, 10.0)], InterpolationMode::Linear);
assert_eq!(track.get_interpolated(0.0, |a, b, t| a + (b - a) * t), Some(5.0));
}
#[test]
fn test_track_clamp_after_last() {
let track = make_track(vec![(1.0, 5.0), (2.0, 10.0)], InterpolationMode::Linear);
assert_eq!(track.get_interpolated(100.0, |a, b, t| a + (b - a) * t), Some(10.0));
}
#[test]
fn test_track_many_keyframes_binary_search() {
let keyframes: Vec<(f32, f32)> = (0..100).map(|i| (i as f32, i as f32 * 2.0)).collect();
let track = make_track(keyframes, InterpolationMode::Linear);
let v = track.get_interpolated(50.5, |a, b, t| a + (b - a) * t).unwrap();
assert!((v - 101.0).abs() < 0.001, "Expected 101.0, got {v}");
}
#[test]
fn test_track_zero_duration_keyframe() {
let track = make_track(vec![(1.0, 5.0), (1.0, 10.0)], InterpolationMode::Linear);
let v = track.get_interpolated(1.0, |a, b, t| a + (b - a) * t).unwrap();
assert_eq!(v, 5.0, "dt=0 durumunda ilk keyframe değeri döndürülmeli");
}
fn make_joint(name: &str, idx: usize, parent: Option<usize>) -> SkeletonJoint {
SkeletonJoint {
name: name.into(),
node_index: idx,
inverse_bind_matrix: Mat4::IDENTITY,
parent_index: parent,
local_bind_transform: Mat4::IDENTITY,
bind_translation: Vec3::ZERO,
bind_rotation: Quat::IDENTITY,
bind_scale: Vec3::ONE,
}
}
#[test]
fn test_skeleton_single_root() {
let hierarchy = SkeletonHierarchy {
joints: vec![make_joint("root", 0, None)],
root_transform: Mat4::IDENTITY,
};
let local_poses = vec![Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0))];
let globals = hierarchy.calculate_global_matrices(&local_poses);
assert_eq!(globals.len(), 1);
let pos = Vec3::new(globals[0].w_axis.x, globals[0].w_axis.y, globals[0].w_axis.z);
assert!((pos - Vec3::new(1.0, 2.0, 3.0)).length() < 0.001);
}
#[test]
fn test_skeleton_chain_propagation() {
let hierarchy = SkeletonHierarchy {
joints: vec![
make_joint("root", 0, None),
make_joint("child", 1, Some(0)),
],
root_transform: Mat4::IDENTITY,
};
let local_poses = vec![
Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)),
Mat4::from_translation(Vec3::new(0.0, 2.0, 0.0)),
];
let globals = hierarchy.calculate_global_matrices(&local_poses);
let child_pos = Vec3::new(globals[1].w_axis.x, globals[1].w_axis.y, globals[1].w_axis.z);
assert!((child_pos - Vec3::new(1.0, 2.0, 0.0)).length() < 0.001,
"Child global = root + child local");
}
#[test]
fn test_skeleton_root_transform_applied() {
let hierarchy = SkeletonHierarchy {
joints: vec![make_joint("root", 0, None)],
root_transform: Mat4::from_translation(Vec3::new(10.0, 0.0, 0.0)),
};
let local_poses = vec![Mat4::from_translation(Vec3::new(0.0, 5.0, 0.0))];
let globals = hierarchy.calculate_global_matrices(&local_poses);
let pos = Vec3::new(globals[0].w_axis.x, globals[0].w_axis.y, globals[0].w_axis.z);
assert!((pos - Vec3::new(10.0, 5.0, 0.0)).length() < 0.001,
"Root transform should be applied to root joints");
}
#[test]
fn test_skeleton_branching() {
let hierarchy = SkeletonHierarchy {
joints: vec![
make_joint("root", 0, None),
make_joint("left", 1, Some(0)),
make_joint("right", 2, Some(0)),
],
root_transform: Mat4::IDENTITY,
};
let local_poses = vec![
Mat4::from_translation(Vec3::new(0.0, 1.0, 0.0)),
Mat4::from_translation(Vec3::new(-1.0, 0.0, 0.0)),
Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)),
];
let globals = hierarchy.calculate_global_matrices(&local_poses);
let left_pos = Vec3::new(globals[1].w_axis.x, globals[1].w_axis.y, globals[1].w_axis.z);
let right_pos = Vec3::new(globals[2].w_axis.x, globals[2].w_axis.y, globals[2].w_axis.z);
assert!((left_pos - Vec3::new(-1.0, 1.0, 0.0)).length() < 0.001);
assert!((right_pos - Vec3::new(1.0, 1.0, 0.0)).length() < 0.001);
}
#[test]
fn test_skeleton_deep_chain() {
let hierarchy = SkeletonHierarchy {
joints: (0..5).map(|i| make_joint(&format!("j{i}"), i, if i == 0 { None } else { Some(i-1) })).collect(),
root_transform: Mat4::IDENTITY,
};
let local_poses: Vec<Mat4> = (0..5).map(|_| Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0))).collect();
let globals = hierarchy.calculate_global_matrices(&local_poses);
let tip_x = globals[4].w_axis.x;
assert!((tip_x - 5.0).abs() < 0.001, "5 joint zinciri, tip X=5.0 olmalı, got {tip_x}");
}
}