use super::{AnimationClip, Skeleton, Transform3D};
use glam::{Quat, Vec2, Vec3};
use std::sync::Arc;
#[derive(Debug, Clone, Copy)]
pub enum AnimationParameter {
Float(f32),
Bool(bool),
Trigger(bool),
}
#[derive(Debug, Clone)]
pub enum Motion {
Clip(AnimationClip),
Blend1D {
parameter: usize,
children: Vec<(f32, MotionChild)>,
},
Blend2D {
x: usize,
y: usize,
children: Vec<(Vec2, MotionChild)>,
},
}
#[derive(Debug, Clone)]
pub struct MotionChild {
pub motion: Motion,
pub time_scale: f32,
}
#[derive(Debug, Clone)]
pub struct AnimationEvent {
pub time: f32,
pub name: Arc<str>,
}
#[derive(Debug, Clone)]
pub struct AnimationState {
pub motion: Motion,
pub speed: f32,
pub speed_parameter: Option<usize>,
pub looping: bool,
pub events: Vec<AnimationEvent>,
}
impl AnimationState {
pub fn new(motion: Motion) -> Self {
Self {
motion,
speed: 1.,
speed_parameter: None,
looping: true,
events: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum AnimationCondition {
Greater(usize, f32),
Less(usize, f32),
Bool(usize, bool),
Trigger(usize),
}
#[derive(Debug, Clone)]
pub struct AnimationTransition {
pub from: Option<usize>,
pub to: usize,
pub conditions: Vec<AnimationCondition>,
pub exit_time: Option<f32>,
pub duration: f32,
pub offset: f32,
}
#[derive(Debug)]
pub struct AnimationGraph {
parameters: Vec<AnimationParameter>,
states: Vec<AnimationState>,
transitions: Vec<AnimationTransition>,
initial: usize,
}
impl AnimationGraph {
pub fn new(
parameters: Vec<AnimationParameter>,
mut states: Vec<AnimationState>,
transitions: Vec<AnimationTransition>,
initial: usize,
) -> Arc<Self> {
assert!(initial < states.len(), "AnimationGraph: invalid initial state");
for p in ¶meters {
if let AnimationParameter::Float(v) = p {
assert!(v.is_finite(), "AnimationGraph: non-finite parameter");
}
}
for s in &mut states {
s.motion.validate(¶meters);
assert!(s.speed.is_finite() && s.speed >= 0., "AnimationGraph: invalid speed");
if let Some(p) = s.speed_parameter {
float_parameter(¶meters, p);
}
for e in &s.events {
assert!(
e.time.is_finite() && (0. ..1.).contains(&e.time),
"AnimationGraph: invalid event time"
);
}
s.events.sort_by(|a, b| a.time.total_cmp(&b.time));
}
for t in &transitions {
assert!(
t.to < states.len() && t.from.is_none_or(|i| i < states.len()),
"AnimationGraph: invalid transition state"
);
assert!(
t.duration.is_finite()
&& t.duration >= 0.
&& t.offset.is_finite()
&& t.offset >= 0.
&& t.exit_time.is_none_or(|v| v.is_finite() && v >= 0.),
"AnimationGraph: invalid transition clock"
);
for c in &t.conditions {
match *c {
AnimationCondition::Greater(i, v) | AnimationCondition::Less(i, v) => {
float_parameter(¶meters, i);
assert!(v.is_finite(), "AnimationGraph: invalid threshold");
}
AnimationCondition::Bool(i, _) => assert!(
matches!(parameters.get(i), Some(AnimationParameter::Bool(_))),
"AnimationGraph: expected bool"
),
AnimationCondition::Trigger(i) => assert!(
matches!(parameters.get(i), Some(AnimationParameter::Trigger(_))),
"AnimationGraph: expected trigger"
),
}
}
}
Arc::new(Self {
parameters,
states,
transitions,
initial,
})
}
}
fn float_parameter(p: &[AnimationParameter], i: usize) -> f32 {
match p.get(i) {
Some(AnimationParameter::Float(v)) => *v,
_ => panic!("AnimationGraph: expected float parameter"),
}
}
impl Motion {
fn validate(&self, p: &[AnimationParameter]) {
match self {
Self::Clip(_) => (),
Self::Blend1D { parameter, children } => {
float_parameter(p, *parameter);
assert!(
!children.is_empty()
&& children.iter().all(|(v, _)| v.is_finite())
&& children.windows(2).all(|c| c[0].0 < c[1].0),
"AnimationGraph: invalid 1D thresholds"
);
for (_, c) in children {
c.validate(p);
}
}
Self::Blend2D { x, y, children } => {
float_parameter(p, *x);
float_parameter(p, *y);
assert!(
!children.is_empty() && children.iter().all(|(v, _)| v.is_finite()),
"AnimationGraph: invalid 2D points"
);
for (_, c) in children {
c.validate(p);
}
}
}
}
fn duration(&self) -> f32 {
match self {
Self::Clip(c) => c.duration(),
Self::Blend1D { children, .. } => children
.iter()
.map(|(_, c)| c.motion.duration() / c.time_scale)
.fold(0., f32::max),
Self::Blend2D { children, .. } => children
.iter()
.map(|(_, c)| c.motion.duration() / c.time_scale)
.fold(0., f32::max),
}
}
#[allow(clippy::too_many_arguments)]
fn sample(
&self,
time: f32,
looping: bool,
weight: f32,
p: &[AnimationParameter],
output: &mut Skeleton,
scratch: &mut Skeleton,
total: &mut f32,
) {
if weight <= 0. {
return;
}
match self {
Self::Clip(c) => {
scratch.reset_pose();
let t = if looping && c.duration() > 0. {
time.rem_euclid(c.duration())
} else {
time
};
c.sample(t, scratch);
*total += weight;
mix(&mut output.pose, &scratch.pose, weight / *total);
output.morphs.mix(&scratch.morphs, weight / *total);
}
Self::Blend1D { parameter, children } => {
let v = float_parameter(p, *parameter);
let hi = children.partition_point(|(t, _)| *t <= v);
if hi == 0 {
children[0].1.sample(time, looping, weight, p, output, scratch, total);
} else if hi == children.len() {
children[hi - 1]
.1
.sample(time, looping, weight, p, output, scratch, total);
} else {
let f = (v - children[hi - 1].0) / (children[hi].0 - children[hi - 1].0);
children[hi - 1]
.1
.sample(time, looping, weight * (1. - f), p, output, scratch, total);
children[hi]
.1
.sample(time, looping, weight * f, p, output, scratch, total);
}
}
Self::Blend2D { x, y, children } => {
let v = Vec2::new(float_parameter(p, *x), float_parameter(p, *y));
if let Some((_, c)) = children.iter().find(|(point, _)| *point == v) {
c.sample(time, looping, weight, p, output, scratch, total);
} else {
let distance = |point: Vec2| {
let dx = f64::from(point.x) - f64::from(v.x);
let dy = f64::from(point.y) - f64::from(v.y);
dx * dx + dy * dy
};
let sum: f64 = children.iter().map(|(point, _)| 1. / distance(*point)).sum();
for (point, c) in children {
c.sample(
time,
looping,
weight * ((1. / distance(*point)) / sum) as f32,
p,
output,
scratch,
total,
);
}
}
}
}
}
}
impl Motion {
pub(crate) fn root_delta(
&self,
from: f32,
to: f32,
looping: bool,
p: &[AnimationParameter],
skeleton: &Skeleton,
root: usize,
) -> (Vec3, f32) {
let mut sum = (Vec3::ZERO, 0.);
self.add_root_delta(from, to, looping, 1., p, skeleton, root, &mut sum);
sum
}
#[allow(clippy::too_many_arguments)]
fn add_root_delta(
&self,
from: f32,
to: f32,
looping: bool,
weight: f32,
p: &[AnimationParameter],
skeleton: &Skeleton,
root: usize,
sum: &mut (Vec3, f32),
) {
if weight <= 0. {
return;
}
let child = |c: &MotionChild, w: f32, sum: &mut (Vec3, f32)| {
c.motion.add_root_delta(
from * c.time_scale,
to * c.time_scale,
looping,
w,
p,
skeleton,
root,
sum,
);
};
match self {
Self::Clip(c) => {
let at = |t: f32| {
let pose = c.sample_joint(t, skeleton, root);
(pose.position * Vec3::new(1., 0., 1.), yaw(pose.rotation))
};
let d = c.duration();
let between = |a: f32, b: f32| {
let (pa, ya) = at(a);
let (pb, yb) = at(b);
(pb - pa, angle_delta(ya, yb))
};
let (position, angle) = if looping && d > 0. {
let loops = (to / d).floor() - (from / d).floor();
let (lp, ly) = between(0., d);
let (pp, py) = between(from.rem_euclid(d), to.rem_euclid(d));
(pp + lp * loops, py + ly * loops)
} else {
between(from, to)
};
sum.0 += position * weight;
sum.1 += angle * weight;
}
Self::Blend1D { parameter, children } => {
let v = float_parameter(p, *parameter);
let hi = children.partition_point(|(t, _)| *t <= v);
if hi == 0 {
child(&children[0].1, weight, sum);
} else if hi == children.len() {
child(&children[hi - 1].1, weight, sum);
} else {
let f = (v - children[hi - 1].0) / (children[hi].0 - children[hi - 1].0);
child(&children[hi - 1].1, weight * (1. - f), sum);
child(&children[hi].1, weight * f, sum);
}
}
Self::Blend2D { x, y, children } => {
let v = Vec2::new(float_parameter(p, *x), float_parameter(p, *y));
if let Some((_, c)) = children.iter().find(|(point, _)| *point == v) {
child(c, weight, sum);
} else {
let distance = |point: Vec2| {
let dx = f64::from(point.x) - f64::from(v.x);
let dy = f64::from(point.y) - f64::from(v.y);
dx * dx + dy * dy
};
let total: f64 = children.iter().map(|(point, _)| 1. / distance(*point)).sum();
for (point, c) in children {
child(c, weight * ((1. / distance(*point)) / total) as f32, sum);
}
}
}
}
}
}
impl MotionChild {
fn validate(&self, p: &[AnimationParameter]) {
assert!(
self.time_scale.is_finite() && self.time_scale > 0.,
"AnimationGraph: invalid child time scale"
);
self.motion.validate(p);
}
#[allow(clippy::too_many_arguments)]
fn sample(
&self,
time: f32,
looping: bool,
weight: f32,
p: &[AnimationParameter],
output: &mut Skeleton,
scratch: &mut Skeleton,
total: &mut f32,
) {
self.motion
.sample(time * self.time_scale, looping, weight, p, output, scratch, total);
}
}
fn mix(output: &mut [Transform3D], source: &[Transform3D], weight: f32) {
for (a, b) in output.iter_mut().zip(source) {
a.position = a.position.lerp(b.position, weight);
a.scale = a.scale.lerp(b.scale, weight);
a.rotation = a.rotation.slerp(b.rotation, weight);
}
}
#[derive(Debug, Clone)]
struct Fade {
source: Option<(usize, f32)>,
elapsed: f32,
duration: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerBlend {
Override,
Additive,
}
#[derive(Debug, Clone)]
pub struct JointMask(Vec<usize>);
impl JointMask {
pub fn new(mut joints: Vec<usize>) -> Self {
joints.sort_unstable();
joints.dedup();
Self(joints)
}
pub fn descendants(skeleton: &Skeleton, name: &str) -> Option<Self> {
let root = skeleton.find(name)?;
let mut joints = vec![root];
for (i, joint) in skeleton.joints().iter().enumerate().skip(root + 1) {
if joint.parent.is_some_and(|p| joints.binary_search(&p).is_ok()) {
joints.push(i);
}
}
Some(Self(joints))
}
pub fn contains(&self, joint: usize) -> bool {
self.0.binary_search(&joint).is_ok()
}
}
#[derive(Debug, Clone)]
pub struct AnimationLayer {
pub animator: Box<Animator>,
pub weight: f32,
pub mode: LayerBlend,
pub mask: Option<JointMask>,
pub reference_pose: Option<Vec<Transform3D>>,
buffers: Option<(Skeleton, Skeleton, Skeleton)>,
}
impl AnimationLayer {
pub fn new(graph: Arc<AnimationGraph>) -> Self {
Self {
animator: Box::new(Animator::new(graph)),
weight: 1.,
mode: LayerBlend::Override,
mask: None,
reference_pose: None,
buffers: None,
}
}
fn apply(&mut self, dt: f32, skeleton: &mut Skeleton) {
if self
.buffers
.as_ref()
.is_none_or(|(s, _, _)| !s.shares_joints(skeleton.shared_joints()))
{
self.buffers = Some((skeleton.clone(), skeleton.clone(), skeleton.clone()));
}
let (pose, reference, scratch) = self.buffers.as_mut().unwrap();
self.animator.update(dt, pose);
let w = if self.weight.is_nan() {
0.
} else {
self.weight.clamp(0., 1.)
};
if w == 0. {
return;
}
if self.mode == LayerBlend::Additive && self.reference_pose.is_none() {
reference.reset_pose();
self.animator.graph.states[self.animator.state].motion.sample(
0.,
false,
1.,
&self.animator.parameters,
reference,
scratch,
&mut 0.,
);
}
skeleton
.morphs
.layer(&pose.morphs, w, self.mode == LayerBlend::Additive);
let joints = skeleton.shared_joints().clone();
for (i, (a, b)) in skeleton.pose.iter_mut().zip(&pose.pose).enumerate() {
if self.mask.as_ref().is_some_and(|m| !m.contains(i)) {
continue;
}
match self.mode {
LayerBlend::Override => {
a.position = a.position.lerp(b.position, w);
a.rotation = a.rotation.slerp(b.rotation, w);
a.scale = a.scale.lerp(b.scale, w);
}
LayerBlend::Additive => {
let r = self
.reference_pose
.as_ref()
.map_or_else(|| reference.pose.get(i), |p| p.get(i))
.unwrap_or(&joints[i].rest);
a.position += (b.position - r.position) * w;
a.scale += (b.scale - r.scale) * w;
a.rotation = (a.rotation * Quat::IDENTITY.slerp(r.rotation.inverse() * b.rotation, w)).normalize();
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RootMotion {
pub position: Vec3,
pub rotation: Quat,
}
impl Default for RootMotion {
fn default() -> Self {
Self {
position: Vec3::ZERO,
rotation: Quat::IDENTITY,
}
}
}
fn yaw(q: Quat) -> f32 {
let forward = q * Vec3::Z;
forward.x.atan2(forward.z)
}
fn angle_delta(a: f32, b: f32) -> f32 {
(b - a + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU) - std::f32::consts::PI
}
#[derive(Debug, Clone)]
pub struct Animator {
graph: Arc<AnimationGraph>,
parameters: Vec<AnimationParameter>,
state: usize,
time: f32,
fade: Option<Fade>,
pending: Option<(usize, f32, f32)>,
buffers: Option<(Skeleton, Skeleton, Skeleton)>,
events: Vec<Arc<str>>,
layers: Vec<AnimationLayer>,
root_delta: RootMotion,
pub extract_root_motion: bool,
pub root_joint: usize,
pub apply_root_motion: bool,
pub manual: bool,
}
impl Animator {
pub fn new(graph: Arc<AnimationGraph>) -> Self {
Self {
parameters: graph.parameters.clone(),
state: graph.initial,
graph,
time: 0.,
fade: None,
pending: None,
buffers: None,
events: Vec::with_capacity(64),
layers: Vec::new(),
root_delta: RootMotion::default(),
extract_root_motion: false,
root_joint: 0,
apply_root_motion: false,
manual: false,
}
}
pub fn add_layer(&mut self, layer: AnimationLayer) -> usize {
let index = self.layers.len();
self.layers.push(layer);
index
}
pub fn layer_mut(&mut self, index: usize) -> &mut AnimationLayer {
&mut self.layers[index]
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn root_motion(&self) -> RootMotion {
self.root_delta
}
pub fn set_float(&mut self, parameter: usize, value: f32) {
assert!(value.is_finite(), "Animator: non-finite float");
match self.parameters.get_mut(parameter) {
Some(AnimationParameter::Float(v)) => *v = value,
_ => panic!("Animator: expected float parameter"),
}
}
pub fn set_bool(&mut self, parameter: usize, value: bool) {
match self.parameters.get_mut(parameter) {
Some(AnimationParameter::Bool(v)) => *v = value,
_ => panic!("Animator: expected bool parameter"),
}
}
pub fn set_trigger(&mut self, parameter: usize, value: bool) {
match self.parameters.get_mut(parameter) {
Some(AnimationParameter::Trigger(v)) => *v = value,
_ => panic!("Animator: expected trigger parameter"),
}
}
pub fn state(&self) -> usize {
self.state
}
pub fn normalized_time(&self) -> f32 {
let d = self.graph.states[self.state].motion.duration();
if d > 0. { self.time / d } else { 0. }
}
pub fn fade_weight(&self) -> f32 {
self.fade.as_ref().map_or(1., |f| (f.elapsed / f.duration).min(1.))
}
pub fn crossfade(&mut self, state: usize, duration: f32, normalized_offset: f32) {
assert!(
state < self.graph.states.len()
&& duration.is_finite()
&& duration >= 0.
&& normalized_offset.is_finite()
&& normalized_offset >= 0.,
"Animator: invalid playback command"
);
self.pending = Some((state, duration, normalized_offset));
}
pub fn play(&mut self, state: usize, normalized_time: f32) {
self.crossfade(state, 0., normalized_time);
}
pub fn next_event(&mut self) -> Option<Arc<str>> {
(!self.events.is_empty()).then(|| self.events.remove(0))
}
pub fn drain_events(&mut self) -> impl Iterator<Item = Arc<str>> + '_ {
self.events.drain(..)
}
fn advance(&self, state: usize, time: f32, dt: f32) -> f32 {
let s = &self.graph.states[state];
let speed = s.speed
* s.speed_parameter
.map_or(1., |p| float_parameter(&self.parameters, p).max(0.));
let next = (time + dt * speed).min(f32::MAX);
if s.looping { next } else { next.min(s.motion.duration()) }
}
fn matches(&self, c: &AnimationCondition) -> bool {
match *c {
AnimationCondition::Greater(i, v) => float_parameter(&self.parameters, i) > v,
AnimationCondition::Less(i, v) => float_parameter(&self.parameters, i) < v,
AnimationCondition::Bool(i, v) => matches!(self.parameters[i],AnimationParameter::Bool(b) if b==v),
AnimationCondition::Trigger(i) => matches!(self.parameters[i], AnimationParameter::Trigger(true)),
}
}
pub fn update(&mut self, dt: f32, skeleton: &mut Skeleton) {
self.update_base(dt, skeleton);
for layer in &mut self.layers {
layer.apply(dt, skeleton);
}
if self.extract_root_motion || self.apply_root_motion {
let rest = skeleton.joints().get(self.root_joint).map(|j| j.rest);
if let (Some(rest), Some(pose)) = (rest, skeleton.pose.get_mut(self.root_joint)) {
pose.position.x = rest.position.x;
pose.position.z = rest.position.z;
pose.rotation = Quat::from_rotation_y(yaw(rest.rotation) - yaw(pose.rotation)) * pose.rotation;
}
}
}
fn update_base(&mut self, dt: f32, skeleton: &mut Skeleton) {
assert!(
dt.is_finite() && dt >= 0.,
"Animator: dt must be finite and nonnegative"
);
if self
.buffers
.as_ref()
.is_none_or(|(s, _, _)| !s.shares_joints(skeleton.shared_joints()))
{
self.buffers = Some((skeleton.clone(), skeleton.clone(), skeleton.clone()));
}
let command = self.pending.take();
let selected = if command.is_none() {
self.graph.transitions.iter().position(|t| {
t.from.map_or(t.to != self.state, |s| s == self.state)
&& (t.from.is_none()
|| t.exit_time.is_none_or(|exit| {
let d = self.graph.states[self.state].motion.duration();
d > 0. && self.advance(self.state, self.time, dt) / d >= exit
}))
&& t.conditions.iter().all(|c| self.matches(c))
})
} else {
None
};
let command = command.or_else(|| {
selected.map(|i| {
let t = &self.graph.transitions[i];
for c in &t.conditions {
if let AnimationCondition::Trigger(p) = *c {
self.parameters[p] = AnimationParameter::Trigger(false);
}
}
(t.to, t.duration, t.offset)
})
});
if let Some((state, duration, offset)) = command {
let source = if self.fade.is_some() {
None
} else {
Some((self.state, self.time))
};
let (_, _, snapshot) = self.buffers.as_mut().unwrap();
snapshot.pose.clone_from(&skeleton.pose);
snapshot.morphs.clone_from(&skeleton.morphs);
self.fade = if duration > 0. {
Some(Fade {
source,
elapsed: 0.,
duration,
})
} else {
None
};
self.state = state;
let s = &self.graph.states[state];
self.time = offset * s.motion.duration();
if !s.looping {
self.time = self.time.min(s.motion.duration());
}
}
let old = self.time;
self.time = self.advance(self.state, self.time, dt);
self.collect_events(old, self.time);
self.root_delta = RootMotion::default();
if (self.extract_root_motion || self.apply_root_motion) && self.root_joint < skeleton.joints().len() {
let state = &self.graph.states[self.state];
let mut delta = state.motion.root_delta(
old,
self.time,
state.looping,
&self.parameters,
skeleton,
self.root_joint,
);
if let Some(fade) = &self.fade {
let w = ((fade.elapsed + dt) / fade.duration).min(1.);
let source = fade.source.map_or((Vec3::ZERO, 0.), |(i, time)| {
let s = &self.graph.states[i];
s.motion.root_delta(
time,
self.advance(i, time, dt),
s.looping,
&self.parameters,
skeleton,
self.root_joint,
)
});
delta = (source.0.lerp(delta.0, w), source.1 + (delta.1 - source.1) * w);
}
self.root_delta = RootMotion {
position: delta.0,
rotation: Quat::from_rotation_y(delta.1),
};
}
if let Some(mut fade) = self.fade.take() {
fade.elapsed = (fade.elapsed + dt).min(fade.duration);
if let Some((state, time)) = fade.source {
fade.source = Some((state, self.advance(state, time, dt)));
}
if fade.elapsed < fade.duration {
self.fade = Some(fade);
}
}
let (output, scratch, snapshot) = self.buffers.as_mut().unwrap();
let state = &self.graph.states[self.state];
output.reset_pose();
state
.motion
.sample(self.time, state.looping, 1., &self.parameters, output, scratch, &mut 0.);
if let Some(f) = &self.fade {
if let Some((i, time)) = f.source {
let s = &self.graph.states[i];
snapshot.reset_pose();
s.motion
.sample(time, s.looping, 1., &self.parameters, snapshot, scratch, &mut 0.);
}
mix(&mut output.pose, &snapshot.pose, 1. - f.elapsed / f.duration);
output.morphs.mix(&snapshot.morphs, 1. - f.elapsed / f.duration);
}
skeleton.pose.clone_from(&output.pose);
skeleton.morphs.clone_from(&output.morphs);
}
fn collect_events(&mut self, old: f32, new: f32) {
let state = &self.graph.states[self.state];
let d = state.motion.duration();
if d <= 0. || new <= old || state.events.is_empty() {
return;
}
let start = f64::from(old) / f64::from(d);
let end = f64::from(new) / f64::from(d);
let mut cycle = if state.looping { start.floor() } else { 0. };
while cycle <= end.floor() && self.events.len() < 64 {
for e in &state.events {
let at = cycle + f64::from(e.time);
if at > start && at <= end && self.events.len() < 64 {
self.events.push(e.name.clone());
}
}
if !state.looping {
break;
}
let next = cycle + 1.;
if next == cycle {
break;
}
cycle = next;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AnimationTrack, Interpolation, Joint, JointAnimation, Transform3D, Vec4};
fn skeleton() -> Skeleton {
Skeleton::new(vec![Joint {
name: "root".into(),
parent: None,
rest: Transform3D::default(),
}])
}
fn clip(s: &Skeleton, x: f32) -> AnimationClip {
AnimationClip::new(
"pose",
s,
vec![JointAnimation {
joint: 0,
name: "root".into(),
translation: Some(AnimationTrack::new(
vec![0., 1.],
vec![Vec4::new(x, 0., 0., 0.); 2],
Interpolation::Linear,
)),
rotation: None,
scale: None,
}],
)
}
fn near(a: f32, b: f32) {
assert!((a - b).abs() < 1e-5, "{a} != {b}");
}
#[test]
fn layers_weight_mask_and_additive_rotation() {
let mut s = Skeleton::new(vec![
Joint {
name: "root".into(),
parent: None,
rest: Transform3D::default(),
},
Joint {
name: "arm".into(),
parent: Some(0),
rest: Transform3D::default(),
},
]);
let mut a = animator(&s, Motion::Clip(clip(&s, 2.)), vec![]);
let overlay = animator(&s, Motion::Clip(clip(&s, 10.)), vec![]);
let mut layer = AnimationLayer::new(overlay.graph.clone());
layer.mask = Some(JointMask::descendants(&s, "arm").unwrap());
let i = a.add_layer(layer);
a.update(0., &mut s);
near(s.pose[0].position.x, 2.);
a.layer_mut(i).mask = None;
a.layer_mut(i).weight = 0.;
a.update(0., &mut s);
near(s.pose[0].position.x, 2.);
a.layer_mut(i).weight = 1.;
a.update(0., &mut s);
near(s.pose[0].position.x, 10.);
let rotation = AnimationClip::new(
"add",
&s,
vec![JointAnimation {
joint: 0,
name: "root".into(),
translation: None,
scale: None,
rotation: Some(AnimationTrack::new(
vec![0., 1.],
vec![Vec4::W, Vec4::from_array(glam::Quat::from_rotation_z(0.2).to_array())],
Interpolation::Linear,
)),
}],
);
let mut layer = AnimationLayer::new(animator(&s, Motion::Clip(rotation), vec![]).graph);
layer.mode = LayerBlend::Additive;
layer.animator.graph = AnimationGraph::new(
vec![],
vec![{
let mut state = layer.animator.graph.states[0].clone();
state.looping = false;
state
}],
vec![],
0,
);
a.add_layer(layer);
a.update(1., &mut s);
assert!(s.pose[0].rotation.abs_diff_eq(glam::Quat::from_rotation_z(0.2), 1e-5));
}
fn walk(s: &Skeleton, distance: f32) -> AnimationClip {
AnimationClip::new(
"walk",
s,
vec![JointAnimation {
joint: 0,
name: "root".into(),
translation: Some(AnimationTrack::new(
vec![0., 1.],
vec![Vec4::ZERO, Vec4::new(distance, 0., 0., 0.)],
Interpolation::Linear,
)),
rotation: None,
scale: None,
}],
)
}
#[test]
fn root_motion_wrap_speed_and_crossfade() {
let mut s = skeleton();
let mut fast = AnimationState::new(Motion::Clip(walk(&s, 2.)));
fast.speed = 2.;
let mut a = Animator::new(AnimationGraph::new(
vec![],
vec![AnimationState::new(Motion::Clip(walk(&s, 1.))), fast],
vec![],
0,
));
a.extract_root_motion = true;
let mut total = 0.;
for _ in 0..10 {
a.update(0.3, &mut s);
total += a.root_motion().position.x;
near(s.pose[0].position.x, 0.);
}
near(total, 3.);
a.crossfade(1, 1., 0.);
a.update(0.5, &mut s);
near(a.root_motion().position.x, 1.25);
a.update(2., &mut s);
near(a.root_motion().position.x, 8.);
}
#[test]
fn idle_walk_run_use_ordered_transitions_and_fixed_time_weights() {
let mut s = skeleton();
let states = [0., 1., 2.]
.map(|x| AnimationState::new(Motion::Clip(clip(&s, x))))
.to_vec();
let transition = |from, to, condition| AnimationTransition {
from: Some(from),
to,
conditions: vec![condition],
duration: 0.12,
exit_time: None,
offset: 0.,
};
let g = AnimationGraph::new(
vec![AnimationParameter::Float(0.)],
states,
vec![
transition(0, 1, AnimationCondition::Greater(0, 0.05)),
transition(1, 2, AnimationCondition::Greater(0, 1.4)),
],
0,
);
let mut a = Animator::new(g);
a.update(0., &mut s);
a.set_float(0, 1.);
a.update(0.03, &mut s);
assert_eq!(a.state(), 1);
near(a.fade_weight(), 0.25);
near(s.pose[0].position.x, 0.25);
a.update(0.09, &mut s);
near(a.fade_weight(), 1.);
a.set_float(0, 2.);
a.update(0.06, &mut s);
assert_eq!(a.state(), 2);
near(s.pose[0].position.x, 1.5);
}
fn animator(_s: &Skeleton, motion: Motion, parameters: Vec<AnimationParameter>) -> Animator {
Animator::new(AnimationGraph::new(
parameters,
vec![AnimationState::new(motion)],
vec![],
0,
))
}
fn child(s: &Skeleton, x: f32) -> MotionChild {
MotionChild {
motion: Motion::Clip(clip(s, x)),
time_scale: 1.,
}
}
#[test]
fn one_dimensional_blends_match_thresholds_and_interpolate_neighbours() {
let mut s = skeleton();
let tree = Motion::Blend1D {
parameter: 0,
children: vec![(0., child(&s, 0.)), (1., child(&s, 10.)), (2., child(&s, 30.))],
};
let mut a = animator(&s, tree, vec![AnimationParameter::Float(0.)]);
for (v, want) in [
(-1., 0.),
(0., 0.),
(0.5, 5.),
(1., 10.),
(1.5, 20.),
(2., 30.),
(3., 30.),
] {
a.set_float(0, v);
a.update(0., &mut s);
near(s.pose[0].position.x, want);
}
}
#[test]
fn two_dimensional_blends_select_cardinals_exactly_and_support_nested_trees() {
let mut s = skeleton();
let points = [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y];
let children = points
.into_iter()
.enumerate()
.map(|(i, p)| (p, child(&s, i as f32)))
.collect();
let tree = Motion::Blend2D { x: 0, y: 1, children };
let nested = Motion::Blend1D {
parameter: 0,
children: vec![(
0.,
MotionChild {
motion: tree,
time_scale: 1.,
},
)],
};
let mut a = animator(&s, nested, vec![AnimationParameter::Float(0.); 2]);
for (i, p) in points.into_iter().enumerate() {
a.set_float(0, p.x);
a.set_float(1, p.y);
a.update(0., &mut s);
near(s.pose[0].position.x, i as f32);
}
a.set_float(0, 0.);
a.set_float(1, 0.);
a.update(0., &mut s);
near(s.pose[0].position.x, 1.5);
}
fn transition(from: Option<usize>, to: usize, conditions: Vec<AnimationCondition>) -> AnimationTransition {
AnimationTransition {
from,
to,
conditions,
exit_time: None,
duration: 0.,
offset: 0.,
}
}
#[test]
fn selected_transition_consumes_triggers_but_failed_conditions_do_not() {
let mut s = skeleton();
let states = vec![AnimationState::new(Motion::Clip(clip(&s, 0.))); 2];
let transitions = vec![
transition(
Some(0),
1,
vec![AnimationCondition::Trigger(0), AnimationCondition::Bool(1, true)],
),
transition(Some(1), 0, vec![AnimationCondition::Trigger(0)]),
];
let mut a = Animator::new(AnimationGraph::new(
vec![AnimationParameter::Trigger(false), AnimationParameter::Bool(false)],
states,
transitions,
0,
));
a.set_trigger(0, true);
a.update(0., &mut s);
assert_eq!(a.state(), 0);
a.set_bool(1, true);
a.update(0., &mut s);
assert_eq!(a.state(), 1);
a.update(0., &mut s);
assert_eq!(a.state(), 1);
a.set_trigger(0, true);
a.update(0., &mut s);
assert_eq!(a.state(), 0);
}
#[test]
fn exit_time_waits_until_normalized_threshold_and_any_state_ignores_it() {
let mut s = skeleton();
let states = vec![AnimationState::new(Motion::Clip(clip(&s, 0.))); 2];
let mut t = transition(Some(0), 1, vec![]);
t.exit_time = Some(0.75);
let mut a = Animator::new(AnimationGraph::new(vec![], states.clone(), vec![t.clone()], 0));
a.update(0.5, &mut s);
assert_eq!(a.state(), 0);
a.update(0.25, &mut s);
assert_eq!(a.state(), 1);
t.from = None;
t.exit_time = Some(100.);
let mut a = Animator::new(AnimationGraph::new(vec![], states, vec![t], 0));
a.update(0., &mut s);
assert_eq!(a.state(), 1);
}
#[test]
fn interrupted_crossfade_starts_at_the_last_mixed_pose() {
let mut s = skeleton();
let states = [0., 10., 20.]
.map(|x| AnimationState::new(Motion::Clip(clip(&s, x))))
.to_vec();
let mut a = Animator::new(AnimationGraph::new(vec![], states, vec![], 0));
a.update(0., &mut s);
a.crossfade(1, 1., 0.);
a.update(0.5, &mut s);
near(s.pose[0].position.x, 5.);
a.crossfade(2, 1., 0.);
a.update(0., &mut s);
near(s.pose[0].position.x, 5.);
a.update(0.5, &mut s);
near(s.pose[0].position.x, 12.5);
a.play(0, 0.);
a.update(0., &mut s);
near(s.pose[0].position.x, 0.);
}
#[test]
fn events_cross_loop_wrap_in_time_order_and_reuse_buffer() {
let mut s = skeleton();
let mut state = AnimationState::new(Motion::Clip(clip(&s, 0.)));
state.events = vec![
AnimationEvent {
time: 0.9,
name: "end".into(),
},
AnimationEvent {
time: 0.1,
name: "start".into(),
},
];
let mut a = Animator::new(AnimationGraph::new(vec![], vec![state], vec![], 0));
a.play(0, 0.8);
a.update(0.4, &mut s);
assert_eq!(
a.drain_events().map(|s| s.to_string()).collect::<Vec<_>>(),
["end", "start"]
);
a.update(0., &mut s);
assert_eq!(a.drain_events().count(), 0);
a.update(100., &mut s);
assert_eq!(a.drain_events().count(), 64);
}
#[test]
fn child_time_scale_and_parameter_speed_multiply_and_clamping_holds_end() {
let mut s = skeleton();
let c = AnimationClip::new(
"ramp",
&s,
vec![JointAnimation {
joint: 0,
name: "root".into(),
translation: Some(AnimationTrack::new(
vec![0., 1.],
vec![Vec4::ZERO, Vec4::X],
Interpolation::Linear,
)),
rotation: None,
scale: None,
}],
);
let mut state = AnimationState::new(Motion::Blend1D {
parameter: 0,
children: vec![(
0.,
MotionChild {
motion: Motion::Clip(c),
time_scale: 2.,
},
)],
});
state.speed = 2.;
state.speed_parameter = Some(0);
state.looping = false;
let mut a = Animator::new(AnimationGraph::new(
vec![AnimationParameter::Float(0.5)],
vec![state],
vec![],
0,
));
a.update(0.125, &mut s);
near(s.pose[0].position.x, 0.25);
near(a.normalized_time(), 0.25);
a.update(2., &mut s);
near(s.pose[0].position.x, 1.);
near(a.normalized_time(), 1.);
a.set_float(0, -1.);
a.play(0, 0.);
a.update(1., &mut s);
near(s.pose[0].position.x, 0.);
}
#[test]
#[should_panic(expected = "AnimationGraph: invalid 1D thresholds")]
fn duplicate_thresholds_are_rejected() {
let s = skeleton();
animator(
&s,
Motion::Blend1D {
parameter: 0,
children: vec![(0., child(&s, 0.)), (0., child(&s, 1.))],
},
vec![AnimationParameter::Float(0.)],
);
}
#[test]
fn automatic_updates_run_without_a_renderer_and_manual_animators_are_skipped() {
let mut ctx = crate::Ctx::new(0.25, Vec2::ONE);
let s = skeleton();
let c = clip(&s, 3.);
let graph = AnimationGraph::new(vec![], vec![AnimationState::new(Motion::Clip(c))], vec![], 0);
let automatic = ctx.world.spawn((Animator::new(graph.clone()), s.clone()));
let mut manual = Animator::new(graph);
manual.manual = true;
let manual = ctx.world.spawn((manual, s));
ctx.update_animators();
near(ctx.world.get::<Skeleton>(automatic).unwrap().pose[0].position.x, 3.);
near(ctx.world.get::<Skeleton>(manual).unwrap().pose[0].position.x, 0.);
}
#[test]
fn first_matching_transition_wins_and_target_offset_is_applied() {
let mut s = skeleton();
let states = vec![AnimationState::new(Motion::Clip(clip(&s, 0.))); 3];
let mut t = transition(Some(0), 1, vec![AnimationCondition::Less(0, 1.)]);
t.offset = 0.5;
let mut a = Animator::new(AnimationGraph::new(
vec![AnimationParameter::Float(0.)],
states,
vec![t, transition(None, 2, vec![])],
0,
));
a.update(0.1, &mut s);
assert_eq!(a.state(), 1);
near(a.normalized_time(), 0.6);
}
#[test]
fn ordinary_fades_keep_advancing_the_outgoing_clip() {
let mut s = skeleton();
let ramp = AnimationClip::new(
"ramp",
&s,
vec![JointAnimation {
joint: 0,
name: "root".into(),
translation: Some(AnimationTrack::new(
vec![0., 1.],
vec![Vec4::ZERO, Vec4::X],
Interpolation::Linear,
)),
rotation: None,
scale: None,
}],
);
let mut a = Animator::new(AnimationGraph::new(
vec![],
vec![
AnimationState::new(Motion::Clip(ramp)),
AnimationState::new(Motion::Clip(clip(&s, 2.))),
],
vec![],
0,
));
a.update(0.25, &mut s);
a.crossfade(1, 1., 0.);
a.update(0.25, &mut s);
near(s.pose[0].position.x, 0.875);
}
}