use glam::{Quat, Vec3};
use super::data::{AnimationData, TransformAnimation, VertexAnimation};
fn quat_blend_replace(acc: Quat, layer: Quat, w: f32) -> Quat {
let layer = if acc.dot(layer) < 0.0 { -layer } else { layer };
acc.slerp(layer, w)
}
fn vec3_lerp(acc: Vec3, layer: Vec3, w: f32) -> Vec3 {
acc + (layer - acc) * w
}
fn weights_blend_replace(acc: &[f32], layer: &[f32], w: f32) -> Vec<f32> {
let mut out = acc.to_vec();
for (i, out_i) in out.iter_mut().enumerate() {
if let Some(&l) = layer.get(i) {
*out_i += (l - *out_i) * w;
}
}
out
}
fn quat_scaled_delta(layer: Quat, reference: Quat, w: f32) -> Quat {
let mut delta = (layer * reference.inverse()).normalize();
if delta.w < 0.0 {
delta = -delta;
}
Quat::IDENTITY.slerp(delta, w)
}
fn transform_blend_replace(
acc: &TransformAnimation,
layer: &TransformAnimation,
w: f32,
) -> TransformAnimation {
let translation = match (acc.translation, layer.translation) {
(Some(a), Some(l)) => Some(vec3_lerp(a, l, w)),
(a, _) => a,
};
let rotation = match (acc.rotation, layer.rotation) {
(Some(a), Some(l)) => Some(quat_blend_replace(a, l, w)),
(a, _) => a,
};
let scale = match (acc.scale, layer.scale) {
(Some(a), Some(l)) => Some(vec3_lerp(a, l, w)),
(a, _) => a,
};
TransformAnimation {
translation,
rotation,
scale,
}
}
pub fn blend_replace(acc: &AnimationData, layer: &AnimationData, w: f32) -> AnimationData {
match (acc, layer) {
(AnimationData::F32(a), AnimationData::F32(l)) => AnimationData::F32(a + (l - a) * w),
(AnimationData::F64(a), AnimationData::F64(l)) => {
AnimationData::F64(a + (l - a) * w as f64)
}
(AnimationData::Vec2(a), AnimationData::Vec2(l)) => AnimationData::Vec2(*a + (*l - *a) * w),
(AnimationData::Vec3(a), AnimationData::Vec3(l)) => {
AnimationData::Vec3(vec3_lerp(*a, *l, w))
}
(AnimationData::Vec4(a), AnimationData::Vec4(l)) => AnimationData::Vec4(*a + (*l - *a) * w),
(AnimationData::Quat(a), AnimationData::Quat(l)) => {
AnimationData::Quat(quat_blend_replace(*a, *l, w))
}
(AnimationData::Vertex(a), AnimationData::Vertex(l)) => AnimationData::Vertex(
VertexAnimation::new(weights_blend_replace(&a.weights, &l.weights, w)),
),
(AnimationData::Transform(a), AnimationData::Transform(l)) => {
AnimationData::Transform(transform_blend_replace(a, l, w))
}
_ => acc.clone(),
}
}
fn transform_blend_additive(
acc: &TransformAnimation,
layer: &TransformAnimation,
reference: &TransformAnimation,
w: f32,
) -> TransformAnimation {
let translation = match (acc.translation, layer.translation) {
(Some(a), Some(l)) => {
let r = reference.translation.unwrap_or(Vec3::ZERO);
Some(a + (l - r) * w)
}
(a, _) => a,
};
let rotation = match (acc.rotation, layer.rotation) {
(Some(a), Some(l)) => {
let r = reference.rotation.unwrap_or(Quat::IDENTITY);
Some((quat_scaled_delta(l, r, w) * a).normalize())
}
(a, _) => a,
};
let scale = match (acc.scale, layer.scale) {
(Some(a), Some(l)) => {
let r = reference.scale.unwrap_or(Vec3::ONE);
Some(a + (l - r) * w)
}
(a, _) => a,
};
TransformAnimation {
translation,
rotation,
scale,
}
}
pub fn blend_additive(
acc: &AnimationData,
layer: &AnimationData,
reference: &AnimationData,
w: f32,
) -> AnimationData {
match (acc, layer, reference) {
(AnimationData::F32(a), AnimationData::F32(l), AnimationData::F32(r)) => {
AnimationData::F32(a + w * (l - r))
}
(AnimationData::F64(a), AnimationData::F64(l), AnimationData::F64(r)) => {
AnimationData::F64(a + w as f64 * (l - r))
}
(AnimationData::Vec2(a), AnimationData::Vec2(l), AnimationData::Vec2(r)) => {
AnimationData::Vec2(*a + (*l - *r) * w)
}
(AnimationData::Vec3(a), AnimationData::Vec3(l), AnimationData::Vec3(r)) => {
AnimationData::Vec3(*a + (*l - *r) * w)
}
(AnimationData::Vec4(a), AnimationData::Vec4(l), AnimationData::Vec4(r)) => {
AnimationData::Vec4(*a + (*l - *r) * w)
}
(AnimationData::Quat(a), AnimationData::Quat(l), AnimationData::Quat(r)) => {
AnimationData::Quat((quat_scaled_delta(*l, *r, w) * *a).normalize())
}
(AnimationData::Vertex(a), AnimationData::Vertex(l), AnimationData::Vertex(r)) => {
let mut out = a.weights.clone();
for (i, out_i) in out.iter_mut().enumerate() {
let lv = l.weights.get(i).copied().unwrap_or(0.0);
let rv = r.weights.get(i).copied().unwrap_or(0.0);
*out_i += w * (lv - rv);
}
AnimationData::Vertex(VertexAnimation::new(out))
}
(AnimationData::Transform(a), AnimationData::Transform(l), AnimationData::Transform(r)) => {
AnimationData::Transform(transform_blend_additive(a, l, r, w))
}
_ => acc.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn as_f32(d: &AnimationData) -> f32 {
match d {
AnimationData::F32(v) => *v,
other => panic!("expected F32, got {other:?}"),
}
}
fn as_vec3(d: &AnimationData) -> Vec3 {
match d {
AnimationData::Vec3(v) => *v,
other => panic!("expected Vec3, got {other:?}"),
}
}
fn as_quat(d: &AnimationData) -> Quat {
match d {
AnimationData::Quat(q) => *q,
other => panic!("expected Quat, got {other:?}"),
}
}
fn as_transform(d: &AnimationData) -> TransformAnimation {
match d {
AnimationData::Transform(t) => t.clone(),
other => panic!("expected Transform, got {other:?}"),
}
}
#[test]
fn replace_f32_endpoints_and_midpoint() {
let acc = AnimationData::F32(2.0);
let layer = AnimationData::F32(10.0);
assert!((as_f32(&blend_replace(&acc, &layer, 0.0)) - 2.0).abs() < 1e-6);
assert!((as_f32(&blend_replace(&acc, &layer, 1.0)) - 10.0).abs() < 1e-6);
assert!((as_f32(&blend_replace(&acc, &layer, 0.5)) - 6.0).abs() < 1e-6);
}
#[test]
fn replace_vec3_endpoints_and_midpoint() {
let acc = AnimationData::Vec3(Vec3::new(0.0, 0.0, 0.0));
let layer = AnimationData::Vec3(Vec3::new(2.0, 4.0, 6.0));
assert!(as_vec3(&blend_replace(&acc, &layer, 0.0)).abs_diff_eq(Vec3::ZERO, 1e-6));
assert!(
as_vec3(&blend_replace(&acc, &layer, 1.0)).abs_diff_eq(Vec3::new(2.0, 4.0, 6.0), 1e-6)
);
assert!(
as_vec3(&blend_replace(&acc, &layer, 0.5)).abs_diff_eq(Vec3::new(1.0, 2.0, 3.0), 1e-6)
);
}
#[test]
fn replace_quat_endpoints_and_midpoint() {
let acc = AnimationData::Quat(Quat::IDENTITY);
let layer = AnimationData::Quat(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2));
assert!(as_quat(&blend_replace(&acc, &layer, 0.0)).abs_diff_eq(Quat::IDENTITY, 1e-5));
assert!(as_quat(&blend_replace(&acc, &layer, 1.0))
.abs_diff_eq(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2), 1e-5));
let mid = as_quat(&blend_replace(&acc, &layer, 0.5));
let expected = Quat::from_rotation_z(std::f32::consts::FRAC_PI_4);
assert!(
mid.abs_diff_eq(expected, 1e-5) || mid.abs_diff_eq(-expected, 1e-5),
"mid={mid:?} expected={expected:?}"
);
}
#[test]
fn replace_quat_takes_short_arc() {
let acc = AnimationData::Quat(Quat::IDENTITY);
let far = -Quat::from_rotation_z(std::f32::consts::FRAC_PI_2);
let mid = as_quat(&blend_replace(&acc, &AnimationData::Quat(far), 0.5));
let expected = Quat::from_rotation_z(std::f32::consts::FRAC_PI_4);
assert!(
mid.abs_diff_eq(expected, 1e-5) || mid.abs_diff_eq(-expected, 1e-5),
"short-arc failed: mid={mid:?}"
);
}
#[test]
fn replace_transform_per_field_optionality() {
let acc = AnimationData::Transform(TransformAnimation {
translation: Some(Vec3::new(1.0, 1.0, 1.0)),
rotation: Some(Quat::IDENTITY),
scale: Some(Vec3::splat(2.0)),
});
let layer = AnimationData::Transform(TransformAnimation {
translation: Some(Vec3::new(3.0, 3.0, 3.0)),
rotation: None,
scale: None,
});
let out = as_transform(&blend_replace(&acc, &layer, 1.0));
assert!(out
.translation
.unwrap()
.abs_diff_eq(Vec3::new(3.0, 3.0, 3.0), 1e-6));
assert!(out.rotation.unwrap().abs_diff_eq(Quat::IDENTITY, 1e-6));
assert!(out.scale.unwrap().abs_diff_eq(Vec3::splat(2.0), 1e-6));
}
#[test]
fn replace_mismatched_returns_acc() {
let acc = AnimationData::F32(5.0);
let layer = AnimationData::Vec3(Vec3::ONE);
assert!((as_f32(&blend_replace(&acc, &layer, 1.0)) - 5.0).abs() < 1e-6);
}
#[test]
fn additive_f32_adds_scaled_delta() {
let acc = AnimationData::F32(5.0);
let layer = AnimationData::F32(9.0);
let reference = AnimationData::F32(7.0);
assert!((as_f32(&blend_additive(&acc, &layer, &reference, 0.0)) - 5.0).abs() < 1e-6);
assert!((as_f32(&blend_additive(&acc, &layer, &reference, 1.0)) - 7.0).abs() < 1e-6);
assert!((as_f32(&blend_additive(&acc, &layer, &reference, 0.5)) - 6.0).abs() < 1e-6);
}
#[test]
fn additive_f32_ref_equals_layer_no_change() {
let acc = AnimationData::F32(5.0);
let layer = AnimationData::F32(9.0);
let reference = AnimationData::F32(9.0);
assert!((as_f32(&blend_additive(&acc, &layer, &reference, 1.0)) - 5.0).abs() < 1e-6);
}
#[test]
fn additive_quat_ref_equals_layer_is_identity() {
let acc = Quat::from_rotation_x(0.3);
let layer = Quat::from_rotation_y(0.7);
let out = as_quat(&blend_additive(
&AnimationData::Quat(acc),
&AnimationData::Quat(layer),
&AnimationData::Quat(layer), 1.0,
));
assert!(out.abs_diff_eq(acc, 1e-5), "out={out:?} acc={acc:?}");
}
#[test]
fn additive_quat_delta_composes() {
let acc = Quat::from_rotation_x(0.2);
let layer = Quat::from_rotation_z(std::f32::consts::FRAC_PI_2);
let reference = Quat::IDENTITY;
let out = as_quat(&blend_additive(
&AnimationData::Quat(acc),
&AnimationData::Quat(layer),
&AnimationData::Quat(reference),
1.0,
));
let expected = (layer * acc).normalize();
assert!(
out.abs_diff_eq(expected, 1e-5) || out.abs_diff_eq(-expected, 1e-5),
"out={out:?} expected={expected:?}"
);
}
#[test]
fn additive_transform_per_field_optionality() {
let acc = AnimationData::Transform(TransformAnimation {
translation: Some(Vec3::new(1.0, 0.0, 0.0)),
rotation: Some(Quat::IDENTITY),
scale: Some(Vec3::ONE),
});
let layer = AnimationData::Transform(TransformAnimation {
translation: Some(Vec3::new(5.0, 0.0, 0.0)),
rotation: None,
scale: None,
});
let reference = AnimationData::Transform(TransformAnimation {
translation: Some(Vec3::new(2.0, 0.0, 0.0)),
rotation: None,
scale: None,
});
let out = as_transform(&blend_additive(&acc, &layer, &reference, 1.0));
assert!(out
.translation
.unwrap()
.abs_diff_eq(Vec3::new(4.0, 0.0, 0.0), 1e-6));
assert!(out.rotation.unwrap().abs_diff_eq(Quat::IDENTITY, 1e-6));
assert!(out.scale.unwrap().abs_diff_eq(Vec3::ONE, 1e-6));
}
#[test]
fn additive_mismatched_returns_acc() {
let acc = AnimationData::F32(5.0);
let layer = AnimationData::Vec3(Vec3::ONE);
let reference = AnimationData::Vec3(Vec3::ZERO);
assert!((as_f32(&blend_additive(&acc, &layer, &reference, 1.0)) - 5.0).abs() < 1e-6);
}
#[test]
fn additive_seed_from_rest_no_drift_f32() {
let rest = AnimationData::F32(5.0);
let layer = AnimationData::F32(9.0);
let reference = AnimationData::F32(7.0);
let mut first: Option<f32> = None;
for _ in 0..100 {
let acc = rest.clone();
let out = as_f32(&blend_additive(&acc, &layer, &reference, 0.5));
match first {
None => first = Some(out),
Some(f) => assert!((out - f).abs() < 1e-7, "drift: {out} != {f}"),
}
}
assert!((first.unwrap() - 6.0).abs() < 1e-6);
}
#[test]
fn additive_seed_from_rest_no_drift_quat() {
let rest = Quat::from_rotation_x(0.2);
let layer = Quat::from_rotation_z(0.8);
let reference = Quat::from_rotation_z(0.1);
let mut first: Option<Quat> = None;
for _ in 0..100 {
let acc = AnimationData::Quat(rest);
let out = as_quat(&blend_additive(
&acc,
&AnimationData::Quat(layer),
&AnimationData::Quat(reference),
0.5,
));
match first {
None => first = Some(out),
Some(f) => assert!(out.abs_diff_eq(f, 1e-6), "drift: {out:?} != {f:?}"),
}
}
}
#[test]
fn additive_seed_from_rest_no_drift_transform() {
let rest = TransformAnimation {
translation: Some(Vec3::new(1.0, 2.0, 3.0)),
rotation: Some(Quat::from_rotation_y(0.3)),
scale: Some(Vec3::splat(1.5)),
};
let layer = TransformAnimation {
translation: Some(Vec3::new(4.0, 4.0, 4.0)),
rotation: Some(Quat::from_rotation_y(0.9)),
scale: Some(Vec3::splat(2.0)),
};
let reference = TransformAnimation {
translation: Some(Vec3::new(2.0, 2.0, 2.0)),
rotation: Some(Quat::from_rotation_y(0.1)),
scale: Some(Vec3::splat(1.0)),
};
let mut first: Option<TransformAnimation> = None;
for _ in 0..100 {
let acc = AnimationData::Transform(rest.clone());
let out = as_transform(&blend_additive(
&acc,
&AnimationData::Transform(layer.clone()),
&AnimationData::Transform(reference.clone()),
0.5,
));
match &first {
None => first = Some(out),
Some(f) => {
assert!(out
.translation
.unwrap()
.abs_diff_eq(f.translation.unwrap(), 1e-6));
assert!(out.rotation.unwrap().abs_diff_eq(f.rotation.unwrap(), 1e-6));
assert!(out.scale.unwrap().abs_diff_eq(f.scale.unwrap(), 1e-6));
}
}
}
}
}