use super::skeleton::Pose;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Channel {
Translation,
Rotation,
Scale,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Interpolation {
Step,
Linear,
}
#[derive(Copy, Clone, Debug)]
pub enum TrackValue {
Vec3(glam::Vec3),
Quat(glam::Quat),
}
#[derive(Clone, Debug)]
pub enum TrackValues {
Vec3(Vec<glam::Vec3>),
Quat(Vec<glam::Quat>),
}
impl TrackValues {
fn len(&self) -> usize {
match self {
TrackValues::Vec3(v) => v.len(),
TrackValues::Quat(v) => v.len(),
}
}
}
#[derive(Clone, Debug)]
pub struct Sampler {
pub interpolation: Interpolation,
pub times: Vec<f32>,
pub values: TrackValues,
}
impl Sampler {
pub fn sample(&self, t: f32) -> TrackValue {
debug_assert!(!self.times.is_empty(), "sampler has no keyframes");
debug_assert_eq!(self.times.len(), self.values.len(), "times/values length mismatch");
let n = self.times.len();
if t <= self.times[0] {
return self.value_at(0);
}
if t >= self.times[n - 1] {
return self.value_at(n - 1);
}
let i = self.times.partition_point(|&x| x <= t).saturating_sub(1);
let j = i + 1;
match self.interpolation {
Interpolation::Step => self.value_at(i),
Interpolation::Linear => {
let t0 = self.times[i];
let t1 = self.times[j];
let alpha = (t - t0) / (t1 - t0);
self.lerp(i, j, alpha)
}
}
}
fn value_at(&self, i: usize) -> TrackValue {
match &self.values {
TrackValues::Vec3(v) => TrackValue::Vec3(v[i]),
TrackValues::Quat(v) => TrackValue::Quat(v[i]),
}
}
fn lerp(&self, a: usize, b: usize, alpha: f32) -> TrackValue {
match &self.values {
TrackValues::Vec3(v) => TrackValue::Vec3(v[a].lerp(v[b], alpha)),
TrackValues::Quat(v) => TrackValue::Quat(v[a].slerp(v[b], alpha)),
}
}
}
#[derive(Clone, Debug)]
pub struct Track {
pub joint: usize,
pub channel: Channel,
pub sampler: Sampler,
}
#[derive(Clone, Debug)]
pub struct AnimationClip {
pub duration: f32,
pub tracks: Vec<Track>,
}
impl AnimationClip {
pub fn sample_into(&self, t: f32, pose: &mut Pose) {
let mut affected: Vec<(usize, [Option<TrackValue>; 3])> = Vec::new();
for track in &self.tracks {
if track.joint >= pose.local_transforms.len() {
continue;
}
let v = track.sampler.sample(t);
let slot = channel_slot(track.channel);
if let Some((_, channels)) = affected.iter_mut().find(|(j, _)| *j == track.joint) {
channels[slot] = Some(v);
} else {
let mut channels = [None, None, None];
channels[slot] = Some(v);
affected.push((track.joint, channels));
}
}
for (joint_idx, channels) in affected {
let local = &mut pose.local_transforms[joint_idx];
let (s_old, r_old, t_old) = local.to_scale_rotation_translation();
let t_new = match channels[0] {
Some(TrackValue::Vec3(v)) => v,
_ => t_old,
};
let r_new = match channels[1] {
Some(TrackValue::Quat(q)) => q,
_ => r_old,
};
let s_new = match channels[2] {
Some(TrackValue::Vec3(v)) => v,
_ => s_old,
};
*local = glam::Affine3A::from_scale_rotation_translation(s_new, r_new, t_new);
}
}
}
fn channel_slot(c: Channel) -> usize {
match c {
Channel::Translation => 0,
Channel::Rotation => 1,
Channel::Scale => 2,
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::{Quat, Vec3};
fn approx_eq_vec3(a: Vec3, b: Vec3, eps: f32) -> bool {
(a - b).length() < eps
}
fn approx_eq_quat(a: Quat, b: Quat, eps: f32) -> bool {
(a.dot(b).abs() - 1.0).abs() < eps
}
#[test]
fn linear_sampler_lerps_vec3_between_keyframes() {
let s = Sampler {
interpolation: Interpolation::Linear,
times: vec![0.0, 1.0, 2.0],
values: TrackValues::Vec3(vec![Vec3::ZERO, Vec3::new(2.0, 0.0, 0.0), Vec3::ZERO]),
};
match s.sample(0.5) {
TrackValue::Vec3(v) => assert!(approx_eq_vec3(v, Vec3::new(1.0, 0.0, 0.0), 1e-5)),
_ => panic!("wrong variant"),
}
match s.sample(1.75) {
TrackValue::Vec3(v) => assert!(approx_eq_vec3(v, Vec3::new(0.5, 0.0, 0.0), 1e-5)),
_ => panic!("wrong variant"),
}
}
#[test]
fn step_sampler_holds_lower_keyframe() {
let s = Sampler {
interpolation: Interpolation::Step,
times: vec![0.0, 1.0, 2.0],
values: TrackValues::Vec3(vec![Vec3::ZERO, Vec3::X, Vec3::Y]),
};
match s.sample(0.999) {
TrackValue::Vec3(v) => assert_eq!(v, Vec3::ZERO),
_ => panic!(),
}
match s.sample(1.0) {
TrackValue::Vec3(v) => assert_eq!(v, Vec3::X),
_ => panic!(),
}
match s.sample(1.5) {
TrackValue::Vec3(v) => assert_eq!(v, Vec3::X),
_ => panic!(),
}
}
#[test]
fn sampler_clamps_outside_range() {
let s = Sampler {
interpolation: Interpolation::Linear,
times: vec![0.0, 1.0],
values: TrackValues::Vec3(vec![Vec3::ZERO, Vec3::X]),
};
match s.sample(-5.0) {
TrackValue::Vec3(v) => assert_eq!(v, Vec3::ZERO),
_ => panic!(),
}
match s.sample(100.0) {
TrackValue::Vec3(v) => assert_eq!(v, Vec3::X),
_ => panic!(),
}
}
#[test]
fn quat_sampler_slerps_between_keyframes() {
let s = Sampler {
interpolation: Interpolation::Linear,
times: vec![0.0, 1.0],
values: TrackValues::Quat(vec![
Quat::IDENTITY,
Quat::from_rotation_x(std::f32::consts::FRAC_PI_2),
]),
};
match s.sample(0.5) {
TrackValue::Quat(q) => {
let expected = Quat::from_rotation_x(std::f32::consts::FRAC_PI_4);
assert!(approx_eq_quat(q, expected, 1e-4), "got {q:?}");
}
_ => panic!(),
}
}
#[test]
fn sample_into_overwrites_only_animated_channels() {
let mut pose = Pose::identity(2);
pose.local_transforms[1] =
glam::Affine3A::from_translation(Vec3::new(0.0, 0.0, 2.0));
let clip = AnimationClip {
duration: 1.0,
tracks: vec![Track {
joint: 1,
channel: Channel::Rotation,
sampler: Sampler {
interpolation: Interpolation::Linear,
times: vec![0.0, 1.0],
values: TrackValues::Quat(vec![
Quat::IDENTITY,
Quat::from_rotation_x(std::f32::consts::FRAC_PI_2),
]),
},
}],
};
clip.sample_into(0.5, &mut pose);
let (scale, rot, trans) = pose.local_transforms[1].to_scale_rotation_translation();
assert!(approx_eq_vec3(trans, Vec3::new(0.0, 0.0, 2.0), 1e-4));
let expected_rot = Quat::from_rotation_x(std::f32::consts::FRAC_PI_4);
assert!(approx_eq_quat(rot, expected_rot, 1e-4));
assert!(approx_eq_vec3(scale, Vec3::ONE, 1e-4));
}
#[test]
fn sample_into_two_tracks_compose_correctly() {
let mut pose = Pose::identity(1);
let clip = AnimationClip {
duration: 2.0,
tracks: vec![
Track {
joint: 0,
channel: Channel::Translation,
sampler: Sampler {
interpolation: Interpolation::Linear,
times: vec![0.0, 2.0],
values: TrackValues::Vec3(vec![Vec3::ZERO, Vec3::new(4.0, 0.0, 0.0)]),
},
},
Track {
joint: 0,
channel: Channel::Scale,
sampler: Sampler {
interpolation: Interpolation::Step,
times: vec![0.0, 1.0],
values: TrackValues::Vec3(vec![Vec3::ONE, Vec3::splat(2.0)]),
},
},
],
};
clip.sample_into(1.5, &mut pose);
let (scale, _rot, trans) = pose.local_transforms[0].to_scale_rotation_translation();
assert!(approx_eq_vec3(trans, Vec3::new(3.0, 0.0, 0.0), 1e-4));
assert!(approx_eq_vec3(scale, Vec3::splat(2.0), 1e-4));
}
#[test]
fn out_of_range_joint_index_is_skipped() {
let mut pose = Pose::identity(2);
let clip = AnimationClip {
duration: 1.0,
tracks: vec![Track {
joint: 99,
channel: Channel::Translation,
sampler: Sampler {
interpolation: Interpolation::Step,
times: vec![0.0],
values: TrackValues::Vec3(vec![Vec3::new(1.0, 2.0, 3.0)]),
},
}],
};
clip.sample_into(0.5, &mut pose);
assert_eq!(pose.local_transforms[0], glam::Affine3A::IDENTITY);
assert_eq!(pose.local_transforms[1], glam::Affine3A::IDENTITY);
}
}