use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::World;
use nalgebra::{Quaternion, UnitQuaternion, Vector3};
use serde::{Deserialize, Serialize};
use super::{
find_target, Animation, AnimationClip, AnimationProperty, AnimationTrack,
Children, Keyframe, Name,
};
use crate::Transform;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct HumanoidBone {
pub bone: String,
pub path: String,
}
struct Rest {
local: UnitQuaternion<f32>,
rig: UnitQuaternion<f32>,
height: f32,
position: Vector3<f32>,
}
fn rest(world: &World, root: Entity, path: &str) -> Option<Rest> {
let mut rig = UnitQuaternion::identity();
let mut at = Vector3::zeros();
let mut entity = root;
let mut transform = Transform::default();
for part in path.split('/').filter(|part| !part.is_empty()) {
entity = find_target(world, entity, part)?;
transform = *world.get::<Transform>(entity)?;
at += rig * Vector3::from(transform.position);
rig *= rotation(&transform);
}
Some(Rest {
local: rotation(&transform),
rig,
height: at.y,
position: Vector3::from(transform.position),
})
}
fn rotation(transform: &Transform) -> UnitQuaternion<f32> {
let [x, y, z] = transform.rotation;
UnitQuaternion::from_euler_angles(x, y, z)
}
fn key(name: &str) -> String {
let name = name.rsplit('/').next().unwrap_or(name);
let name = name.rsplit(':').next().unwrap_or(name);
name.chars()
.filter(|c| c.is_alphanumeric())
.flat_map(char::to_lowercase)
.collect()
}
fn bone_of(world: &World, root: Entity, path: &str) -> String {
world
.get::<Animation>(root)
.and_then(|a| a.humanoid.iter().find(|b| b.path == path))
.map_or_else(|| key(path), |b| key(&b.bone))
}
fn path_of(world: &World, root: Entity, bone: &str) -> Option<String> {
if let Some(entry) = world
.get::<Animation>(root)
.and_then(|a| a.humanoid.iter().find(|b| key(&b.bone) == bone))
{
return Some(entry.path.clone());
}
let mut queue = std::collections::VecDeque::from([(root, String::new())]);
while let Some((entity, path)) = queue.pop_front() {
for &child in world.get::<Children>(entity).map_or(&[][..], |c| &c.0) {
let Some(name) = world.get::<Name>(child) else {
continue;
};
let path = if path.is_empty() {
name.0.clone()
} else {
format!("{path}/{}", name.0)
};
if key(&name.0) == bone {
return Some(path);
}
queue.push_back((child, path));
}
}
None
}
pub fn retarget_clip(
world: &World,
from: Entity,
clip: &str,
to: Entity,
) -> Result<AnimationClip, String> {
let animation = world
.get::<Animation>(from)
.ok_or("the source object has no rusting.animation")?;
let source = animation
.clip(clip)
.map(|i| &animation.clips[i])
.ok_or_else(|| format!("the source has no clip `{clip}`"))?;
if !source.blend.is_empty() {
return Err(format!(
"`{clip}` is a blend space; retarget its point clips one by one"
));
}
let scale = match (
path_of(world, from, "hips").and_then(|p| rest(world, from, &p)),
path_of(world, to, "hips").and_then(|p| rest(world, to, &p)),
) {
(Some(a), Some(b)) if a.height > 0.0 && b.height > 0.0 => {
b.height / a.height
}
_ => 1.0,
};
let mut tracks = Vec::new();
for track in &source.tracks {
if track.target.is_empty() {
tracks.push(track.clone());
continue;
}
let bone = bone_of(world, from, &track.target);
let Some(path) = path_of(world, to, &bone) else {
continue;
};
let (Some(a), Some(b)) =
(rest(world, from, &track.target), rest(world, to, &path))
else {
continue;
};
let (parent_a, parent_b) =
(a.rig * a.local.inverse(), b.rig * b.local.inverse());
let mut keys = Vec::with_capacity(track.keys.len());
let property = match track.property {
AnimationProperty::Rotation | AnimationProperty::Orientation => {
let mut last: Option<UnitQuaternion<f32>> = None;
for k in &track.keys {
let q = if track.property == AnimationProperty::Rotation {
let at = |i| k.value.get(i).copied().unwrap_or(0.0);
UnitQuaternion::from_euler_angles(at(0), at(1), at(2))
} else {
let at = |i| {
k.value.get(i).copied().unwrap_or(f32::from(i == 3))
};
UnitQuaternion::new_normalize(Quaternion::new(
at(3),
at(0),
at(1),
at(2),
))
};
let turn = parent_a * q * a.rig.inverse();
let mut out = parent_b.inverse() * turn * b.rig;
if last
.is_some_and(|last| last.coords.dot(&out.coords) < 0.0)
{
out = UnitQuaternion::new_unchecked(-out.into_inner());
}
last = Some(out);
keys.push(Keyframe {
time: k.time,
value: vec![out.i, out.j, out.k, out.w],
});
}
AnimationProperty::Orientation
}
AnimationProperty::Position if bone == "hips" => {
let turn = parent_b.inverse() * parent_a;
for k in &track.keys {
let at = |i| k.value.get(i).copied().unwrap_or(0.0);
let moved = Vector3::new(at(0), at(1), at(2)) - a.position;
let p = b.position + turn * moved * scale;
keys.push(Keyframe {
time: k.time,
value: vec![p.x, p.y, p.z],
});
}
AnimationProperty::Position
}
_ => continue,
};
tracks.push(AnimationTrack {
target: path,
property,
interpolation: track.interpolation,
keys,
});
}
Ok(AnimationClip {
tracks,
..source.clone()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::{hierarchy::set_parent, Interpolation, TweenRepeat};
fn bone(
world: &mut World,
name: &str,
parent: Entity,
t: Transform,
) -> Entity {
let entity = world.spawn((Name(name.into()), t)).id();
set_parent(world, entity, parent).unwrap();
entity
}
fn at(position: [f32; 3], rotation: [f32; 3]) -> Transform {
Transform {
position,
rotation,
..Transform::default()
}
}
#[test]
fn clips_move_a_differently_built_rig_alike() {
use std::f32::consts::FRAC_PI_2;
let mut world = World::new();
let source = world
.spawn((Name("Mixamo".into()), Transform::default()))
.id();
let hips = bone(
&mut world,
"mixamorig:Hips",
source,
at([0.0, 1.0, 0.0], [0.0; 3]),
);
bone(
&mut world,
"mixamorig:LeftArm",
hips,
at([0.2, 0.4, 0.0], [0.0; 3]),
);
let target = world
.spawn((Name("Robot".into()), Transform::default()))
.id();
let pelvis =
bone(&mut world, "Pelvis", target, at([0.0, 2.0, 0.0], [0.0; 3]));
bone(
&mut world,
"ArmL",
pelvis,
at([0.4, 0.8, 0.0], [0.0, 0.0, FRAC_PI_2]),
);
world.entity_mut(target).insert(Animation {
humanoid: vec![
HumanoidBone {
bone: "Hips".into(),
path: "Pelvis".into(),
},
HumanoidBone {
bone: "LeftArm".into(),
path: "Pelvis/ArmL".into(),
},
],
..Animation::default()
});
let key = |time: f32, value: &[f32]| Keyframe {
time,
value: value.to_vec(),
};
let track = |target: &str, property, keys| AnimationTrack {
target: target.into(),
property,
interpolation: Interpolation::Linear,
keys,
};
world.entity_mut(source).insert(Animation {
clips: vec![AnimationClip {
name: "wave".into(),
repeat: TweenRepeat::Loop,
tracks: vec![
track(
"",
AnimationProperty::Scale,
vec![key(0.0, &[1.0; 3])],
),
track(
"mixamorig:Hips",
AnimationProperty::Position,
vec![
key(0.0, &[0.0, 1.0, 0.0]),
key(1.0, &[0.5, 0.9, 0.0]),
],
),
track(
"mixamorig:Hips/mixamorig:LeftArm",
AnimationProperty::Rotation,
vec![key(0.0, &[0.0; 3]), key(1.0, &[0.0, 0.7, 0.0])],
),
track(
"mixamorig:Hips/mixamorig:Ghost",
AnimationProperty::Rotation,
vec![],
),
],
..AnimationClip::default()
}],
..Animation::default()
});
let clip = retarget_clip(&world, source, "wave", target).unwrap();
let targets: Vec<&str> =
clip.tracks.iter().map(|t| t.target.as_str()).collect();
assert_eq!(targets, ["", "Pelvis", "Pelvis/ArmL"]);
let hips = &clip.tracks[1].keys[1].value;
assert!((hips[0] - 1.0).abs() < 1e-5 && (hips[1] - 1.8).abs() < 1e-5);
let quaternion = |v: &[f32]| {
UnitQuaternion::new_normalize(Quaternion::new(
v[3], v[0], v[1], v[2],
))
};
let rest = UnitQuaternion::from_euler_angles(0.0, 0.0, FRAC_PI_2);
let arm = &clip.tracks[2].keys;
assert!(quaternion(&arm[0].value).angle_to(&rest) < 1e-5);
let swing = UnitQuaternion::from_euler_angles(0.0, 0.7, 0.0);
assert!(quaternion(&arm[1].value).angle_to(&(swing * rest)) < 1e-5);
assert!(retarget_clip(&world, source, "missing", target).is_err());
}
}