use crate::three::math::{Mat4, Vec3};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Interpolation {
Linear,
Step,
}
#[derive(Clone, Debug)]
pub enum TrackValues {
Translation(Vec<[f32; 3]>),
Rotation(Vec<[f32; 4]>),
Scale(Vec<[f32; 3]>),
}
#[derive(Clone, Debug)]
pub struct Track {
pub node: usize,
pub times: Vec<f32>,
pub values: TrackValues,
pub interpolation: Interpolation,
}
#[derive(Copy, Clone, Debug)]
pub struct NodePose {
pub translation: Vec3,
pub rotation: [f32; 4],
pub scale: Vec3,
}
impl NodePose {
pub fn matrix(&self) -> Mat4 {
Mat4::from_trs(
self.translation,
(
self.rotation[0],
self.rotation[1],
self.rotation[2],
self.rotation[3],
),
self.scale,
)
}
}
#[derive(Clone, Debug)]
pub struct Animation {
pub name: Option<String>,
pub tracks: Vec<Track>,
duration: f32,
}
impl Animation {
pub fn new(name: Option<String>, tracks: Vec<Track>) -> Animation {
let duration = tracks
.iter()
.filter_map(|t| t.times.last().copied())
.fold(0.0f32, f32::max);
Animation {
name,
tracks,
duration,
}
}
pub fn duration(&self) -> f32 {
self.duration
}
pub fn sample(&self, t: f32, poses: &mut [NodePose]) {
for track in &self.tracks {
let Some(pose) = poses.get_mut(track.node) else {
continue;
};
let (i0, i1, k) = locate(&track.times, t);
let k = match track.interpolation {
Interpolation::Step => 0.0,
Interpolation::Linear => k,
};
match &track.values {
TrackValues::Translation(v) => {
pose.translation = lerp3(v[i0], v[i1], k);
}
TrackValues::Scale(v) => {
pose.scale = lerp3(v[i0], v[i1], k);
}
TrackValues::Rotation(v) => {
pose.rotation = nlerp_quat(v[i0], v[i1], k);
}
}
}
}
}
fn locate(times: &[f32], t: f32) -> (usize, usize, f32) {
debug_assert!(!times.is_empty());
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(t > times[0]) || times.len() == 1 {
return (0, 0, 0.0);
}
let last = times.len() - 1;
if t >= times[last] {
return (last, last, 0.0);
}
let i = times.partition_point(|&x| x <= t);
let (i0, i1) = (i - 1, i);
let span = times[i1] - times[i0];
let k = if span > 0.0 {
(t - times[i0]) / span
} else {
0.0
};
(i0, i1, k)
}
fn lerp3(a: [f32; 3], b: [f32; 3], k: f32) -> Vec3 {
Vec3::new(
a[0] + (b[0] - a[0]) * k,
a[1] + (b[1] - a[1]) * k,
a[2] + (b[2] - a[2]) * k,
)
}
fn nlerp_quat(a: [f32; 4], mut b: [f32; 4], k: f32) -> [f32; 4] {
let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
if dot < 0.0 {
for c in &mut b {
*c = -*c;
}
}
let mut out = [0.0f32; 4];
for i in 0..4 {
out[i] = a[i] + (b[i] - a[i]) * k;
}
normalize_or(out, a)
}
fn normalize_or(q: [f32; 4], left: [f32; 4]) -> [f32; 4] {
let n = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
if n.is_finite() && n > 1e-6 {
return [q[0] / n, q[1] / n, q[2] / n, q[3] / n];
}
let ln = (left[0] * left[0] + left[1] * left[1] + left[2] * left[2] + left[3] * left[3]).sqrt();
if ln.is_finite() && ln > 1e-6 {
return [left[0] / ln, left[1] / ln, left[2] / ln, left[3] / ln];
}
[0.0, 0.0, 0.0, 1.0]
}
pub(crate) fn build_animations(
doc: &crate::three::doc::Doc,
bin: Option<&[u8]>,
) -> crate::base::Result<(Vec<Animation>, Vec<String>)> {
use crate::base::Error;
use crate::three::extract::{read_scalar_f32, read_vec3_f32, read_vec4_f32};
let mut out = Vec::with_capacity(doc.animations.len());
let mut warnings = Vec::new();
for (ai, def) in doc.animations.iter().enumerate() {
let mut tracks = Vec::new();
for ch in &def.channels {
let sampler = &def.samplers[ch.sampler]; let interpolation = match sampler.interpolation.as_str() {
"LINEAR" => Interpolation::Linear,
"STEP" => Interpolation::Step,
"CUBICSPLINE" => {
warnings.push(format!(
"#FALLBACK animation {ai}: CUBICSPLINE channel on node {} skipped \
(tangent triplets unsupported)",
ch.target_node
));
continue;
}
other => {
return Err(Error::Parse(format!(
"gltf: animation {ai}: unknown interpolation {other:?}"
)))
}
};
let what = format!("animation {ai} times");
let times = read_scalar_f32(doc, sampler.input, bin, &what)?;
if times.windows(2).any(|w| w[1] < w[0]) {
return Err(Error::Parse(format!(
"gltf: animation {ai}: keyframe times decrease"
)));
}
if times.iter().any(|t| !t.is_finite()) {
return Err(Error::Parse(format!(
"gltf: animation {ai}: non-finite keyframe time"
)));
}
let what = format!("animation {ai} values");
let values = match ch.target_path.as_str() {
"translation" => {
TrackValues::Translation(read_vec3_f32(doc, sampler.output, bin, &what)?)
}
"scale" => TrackValues::Scale(read_vec3_f32(doc, sampler.output, bin, &what)?),
"rotation" => {
TrackValues::Rotation(read_vec4_f32(doc, sampler.output, bin, &what)?)
}
"weights" => {
warnings.push(format!(
"#FALLBACK animation {ai}: morph-target weights channel skipped (no morph pipeline)"
));
continue;
}
other => {
return Err(Error::Parse(format!(
"gltf: animation {ai}: unknown channel path {other:?}"
)))
}
};
let n_values = match &values {
TrackValues::Translation(v) | TrackValues::Scale(v) => v.len(),
TrackValues::Rotation(v) => v.len(),
};
if n_values != times.len() || times.is_empty() {
return Err(Error::Parse(format!(
"gltf: animation {ai}: {} keyframe times vs {n_values} values",
times.len()
)));
}
tracks.push(Track {
node: ch.target_node,
times,
values,
interpolation,
});
}
out.push(Animation::new(def.name.clone(), tracks));
}
Ok((out, warnings))
}
#[cfg(test)]
mod tests {
use super::*;
fn track_t(times: Vec<f32>, values: Vec<[f32; 3]>, interp: Interpolation) -> Track {
Track {
node: 0,
times,
values: TrackValues::Translation(values),
interpolation: interp,
}
}
fn rest() -> NodePose {
NodePose {
translation: Vec3::ZERO,
rotation: [0.0, 0.0, 0.0, 1.0],
scale: Vec3::new(1.0, 1.0, 1.0),
}
}
#[test]
fn linear_interpolates_and_clamps() {
let anim = Animation::new(
None,
vec![track_t(
vec![1.0, 3.0],
vec![[0.0, 0.0, 0.0], [10.0, -4.0, 2.0]],
Interpolation::Linear,
)],
);
assert_eq!(anim.duration(), 3.0);
let mut poses = [rest()];
anim.sample(2.0, &mut poses); assert_eq!(poses[0].translation, Vec3::new(5.0, -2.0, 1.0));
anim.sample(0.0, &mut poses); assert_eq!(poses[0].translation, Vec3::ZERO);
anim.sample(99.0, &mut poses); assert_eq!(poses[0].translation, Vec3::new(10.0, -4.0, 2.0));
}
#[test]
fn step_holds_until_the_next_key() {
let anim = Animation::new(
None,
vec![track_t(
vec![0.0, 1.0],
vec![[0.0, 0.0, 0.0], [8.0, 0.0, 0.0]],
Interpolation::Step,
)],
);
let mut poses = [rest()];
anim.sample(0.999, &mut poses);
assert_eq!(poses[0].translation, Vec3::ZERO, "STEP holds the left key");
anim.sample(1.0, &mut poses);
assert_eq!(poses[0].translation, Vec3::new(8.0, 0.0, 0.0));
}
#[test]
fn rotation_nlerp_shortest_path() {
let s = std::f32::consts::FRAC_1_SQRT_2;
let track = Track {
node: 0,
times: vec![0.0, 1.0],
values: TrackValues::Rotation(vec![[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, s, s]]),
interpolation: Interpolation::Linear,
};
let anim = Animation::new(None, vec![track]);
let mut poses = [rest()];
anim.sample(0.5, &mut poses);
let q = poses[0].rotation;
let expected = (std::f32::consts::FRAC_PI_8).sin(); assert!((q[2] - expected).abs() < 1e-3, "{q:?}");
let track = Track {
node: 0,
times: vec![0.0, 1.0],
values: TrackValues::Rotation(vec![[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, -s, -s]]),
interpolation: Interpolation::Linear,
};
let anim2 = Animation::new(None, vec![track]);
let mut poses2 = [rest()];
anim2.sample(0.5, &mut poses2);
let q2 = poses2[0].rotation;
assert!(
(q2[2].abs() - expected).abs() < 1e-3,
"shortest path: {q2:?}"
);
}
#[test]
fn degenerate_rotation_keys_resolve_deterministically() {
let track = Track {
node: 0,
times: vec![0.0, 1.0],
values: TrackValues::Rotation(vec![[0.0; 4], [0.0; 4]]),
interpolation: Interpolation::Linear,
};
let anim = Animation::new(None, vec![track]);
let mut poses = [rest()];
anim.sample(0.5, &mut poses);
assert_eq!(poses[0].rotation, [0.0, 0.0, 0.0, 1.0]);
let track = Track {
node: 0,
times: vec![0.0, 1.0],
values: TrackValues::Rotation(vec![[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0]]),
interpolation: Interpolation::Linear,
};
let anim = Animation::new(None, vec![track]);
let mut a = [rest()];
anim.sample(0.5, &mut a);
let q = a[0].rotation;
let norm = (q.iter().map(|c| c * c).sum::<f32>()).sqrt();
assert!((norm - 1.0).abs() < 1e-5, "unit at the tie: {q:?}");
let s = std::f32::consts::FRAC_1_SQRT_2;
assert!((q[2] - s).abs() < 1e-4 && (q[3] - s).abs() < 1e-4, "{q:?}");
let mut b = [rest()];
anim.sample(0.5, &mut b);
assert_eq!(a[0].rotation, b[0].rotation, "tie-break is stable");
let track = Track {
node: 0,
times: vec![0.0, 1.0],
values: TrackValues::Rotation(vec![[f32::NAN; 4], [0.0, 0.0, 1.0, 0.0]]),
interpolation: Interpolation::Linear,
};
let anim = Animation::new(None, vec![track]);
let mut poses = [rest()];
anim.sample(0.5, &mut poses);
assert!(poses[0].rotation.iter().all(|c| c.is_finite()));
}
#[test]
fn nan_time_clamps_to_first_key() {
let anim = Animation::new(
None,
vec![track_t(
vec![1.0, 2.0],
vec![[3.0, 0.0, 0.0], [7.0, 0.0, 0.0]],
Interpolation::Linear,
)],
);
let mut poses = [rest()];
anim.sample(f32::NAN, &mut poses);
assert_eq!(poses[0].translation.x, 3.0);
}
#[test]
fn duplicate_times_hold_left() {
let anim = Animation::new(
None,
vec![track_t(
vec![0.0, 1.0, 1.0, 2.0],
vec![[0.0; 3], [1.0, 0.0, 0.0], [5.0, 0.0, 0.0], [6.0, 0.0, 0.0]],
Interpolation::Linear,
)],
);
let mut poses = [rest()];
anim.sample(1.0, &mut poses);
assert_eq!(poses[0].translation.x, 5.0);
anim.sample(1.5, &mut poses);
assert_eq!(poses[0].translation.x, 5.5);
}
#[test]
fn untracked_nodes_keep_rest_pose() {
let anim = Animation::new(
None,
vec![track_t(
vec![0.0],
vec![[9.0, 9.0, 9.0]],
Interpolation::Linear,
)],
);
let mut poses = [rest(), rest()];
anim.sample(0.5, &mut poses);
assert_eq!(poses[0].translation.x, 9.0);
assert_eq!(poses[1].translation, Vec3::ZERO, "node 1 untouched");
}
}