From 1e1d93d1bb4930ac003b49927659553727db279b Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sun, 11 Dec 2022 18:07:25 -0800 Subject: [PATCH] Animation Blending --- crates/bevy_animation/src/lib.rs | 360 ++++++++++++++++------- examples/animation/animated_fox.rs | 4 +- examples/animation/animated_transform.rs | 2 +- examples/stress_tests/many_foxes.rs | 4 +- examples/tools/scene_viewer.rs | 7 +- 5 files changed, 265 insertions(+), 112 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 0ab68da7c8788..7707c3388af5b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -3,12 +3,13 @@ #![warn(missing_docs)] use std::ops::Deref; +use std::time::Duration; use bevy_app::{App, CoreStage, Plugin}; use bevy_asset::{AddAsset, Assets, Handle}; use bevy_core::Name; use bevy_ecs::{ - change_detection::DetectChanges, + change_detection::{DetectChanges, Mut}, entity::Entity, prelude::Component, reflect::ReflectComponent, @@ -90,21 +91,17 @@ impl AnimationClip { } } -/// Animation controls -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct AnimationPlayer { - paused: bool, +#[derive(Reflect)] +struct PlayingAnimation { repeat: bool, speed: f32, elapsed: f32, animation_clip: Handle, } -impl Default for AnimationPlayer { +impl Default for PlayingAnimation { fn default() -> Self { Self { - paused: false, repeat: false, speed: 1.0, elapsed: 0.0, @@ -113,33 +110,87 @@ impl Default for AnimationPlayer { } } +/// An animation that is being faded out as part of a transition +struct AnimationTransition { + /// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out. + current_weight: f32, + /// How much to decrease `current_weight` per second + weight_decline_per_sec: f32, + /// The animation that is being faded out + animation: PlayingAnimation, +} + +/// Animation controls +#[derive(Component, Default, Reflect)] +#[reflect(Component)] +pub struct AnimationPlayer { + paused: bool, + + animation: PlayingAnimation, + + // List of previous animations we're currently transitioning away from. + // Usually this is empty, when transitioning between animations, there is + // one entry. When another animation transition happens while a transition + // is still ongoing, then there can be more than one entry. + // Once a transition is finished, it will be automatically removed from the list + #[reflect(ignore)] + transitions: Vec, +} + impl AnimationPlayer { /// Start playing an animation, resetting state of the player - pub fn start(&mut self, handle: Handle) -> &mut Self { - *self = Self { + /// If `transition_duration` is set, this will use a linear blending + /// between the previous and the new animation to make a smooth transition + pub fn start( + &mut self, + handle: Handle, + transition_duration: Option, + ) -> &mut Self { + let mut animation = PlayingAnimation { animation_clip: handle, ..Default::default() }; + std::mem::swap(&mut animation, &mut self.animation); + if let Some(transition_duration) = transition_duration { + // Add the current transition. If other transitions are still ongoing, + // this will keep those transitions running and cause a transition between + // the output of that previous transition to the new animation. + self.transitions.push(AnimationTransition { + current_weight: 1.0, + weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(), + animation, + }); + } else { + // We want a hard transition. + // In case any previous transitions are still playing, stop them + self.transitions.clear(); + } self } /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. - pub fn play(&mut self, handle: Handle) -> &mut Self { - if self.animation_clip != handle || self.is_paused() { - self.start(handle); + /// If `transition_duration` is set, this will use a linear blending + /// between the previous and the new animation to make a smooth transition + pub fn play( + &mut self, + handle: Handle, + transition_duration: Option, + ) -> &mut Self { + if self.animation.animation_clip != handle || self.is_paused() { + self.start(handle, transition_duration); } self } /// Set the animation to repeat pub fn repeat(&mut self) -> &mut Self { - self.repeat = true; + self.animation.repeat = true; self } /// Stop the animation from repeating pub fn stop_repeating(&mut self) -> &mut Self { - self.repeat = false; + self.animation.repeat = false; self } @@ -160,23 +211,23 @@ impl AnimationPlayer { /// Speed of the animation playback pub fn speed(&self) -> f32 { - self.speed + self.animation.speed } /// Set the speed of the animation playback pub fn set_speed(&mut self, speed: f32) -> &mut Self { - self.speed = speed; + self.animation.speed = speed; self } /// Time elapsed playing the animation pub fn elapsed(&self) -> f32 { - self.elapsed + self.animation.elapsed } /// Seek to a specific time in the animation pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self { - self.elapsed = elapsed; + self.animation.elapsed = elapsed; self } } @@ -192,100 +243,161 @@ pub fn animation_player( children: Query<&Children>, ) { for (entity, mut player) in &mut animation_players { - if let Some(animation_clip) = animations.get(&player.animation_clip) { - // Continue if paused unless the `AnimationPlayer` was changed - // This allow the animation to still be updated if the player.elapsed field was manually updated in pause - if player.paused && !player.is_changed() { - continue; - } - if !player.paused { - player.elapsed += time.delta_seconds() * player.speed; - } - let mut elapsed = player.elapsed; - if player.repeat { - elapsed %= animation_clip.duration; - } - if elapsed < 0.0 { - elapsed += animation_clip.duration; - } - 'entity: for (path, curves) in &animation_clip.curves { - // PERF: finding the target entity can be optimised - let mut current_entity = entity; - // Ignore the first name, it is the root node which we already have - for part in path.parts.iter().skip(1) { - let mut found = false; - if let Ok(children) = children.get(current_entity) { - for child in children.deref() { - if let Ok(name) = names.get(*child) { - if name == part { - // Found a children with the right name, continue to the next part - current_entity = *child; - found = true; - break; - } - } - } - } - if !found { - warn!("Entity not found for path {:?} on part {:?}", path, part); - continue 'entity; - } - } - if let Ok(mut transform) = transforms.get_mut(current_entity) { - for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - match &curve.keyframes { - Keyframes::Rotation(keyframes) => transform.rotation = keyframes[0], - Keyframes::Translation(keyframes) => { - transform.translation = keyframes[0]; - } - Keyframes::Scale(keyframes) => transform.scale = keyframes[0], - } - continue; - } + _update_transitions(&mut player, &time); + _run_animation_player( + entity, + player, + &time, + &animations, + &names, + &mut transforms, + &children, + ); + } +} - // Find the current keyframe - // PERF: finding the current keyframe can be optimised - let step_start = match curve - .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap()) - { - Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished - Ok(i) => i, - Err(0) => continue, // this curve isn't started yet - Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished - Err(i) => i - 1, - }; - let ts_start = curve.keyframe_timestamps[step_start]; - let ts_end = curve.keyframe_timestamps[step_start + 1]; - let lerp = (elapsed - ts_start) / (ts_end - ts_start); - - // Apply the keyframe +fn _run_animation_player( + entity: Entity, + mut player: Mut, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &mut Query<&mut Transform>, + children: &Query<&Children>, +) { + let paused = player.paused; + // Continue if paused unless the `AnimationPlayer` was changed + // This allow the animation to still be updated if the player.elapsed field was manually updated in pause + if paused && !player.is_changed() { + return; + } + + // Apply the main animation + _apply_animation( + 1.0, + &mut player.animation, + paused, + entity, + time, + animations, + names, + transforms, + children, + ); + + // Apply any potential fade-out transitions from previous animations + for AnimationTransition { + current_weight, + animation, + .. + } in &mut player.transitions + { + _apply_animation( + *current_weight, + animation, + paused, + entity, + time, + animations, + names, + transforms, + children, + ); + } +} + +#[allow(clippy::too_many_arguments)] +fn _apply_animation( + weight: f32, + animation: &mut PlayingAnimation, + paused: bool, + entity: Entity, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &mut Query<&mut Transform>, + children: &Query<&Children>, +) { + if let Some(animation_clip) = animations.get(&animation.animation_clip) { + if !paused { + animation.elapsed += time.delta_seconds() * animation.speed; + } + let mut elapsed = animation.elapsed; + if animation.repeat { + elapsed %= animation_clip.duration; + } + if elapsed < 0.0 { + elapsed += animation_clip.duration; + } + for (path, curves) in &animation_clip.curves { + let Some(current_entity) = _find_entity(entity, path, children, names) else { + continue; + }; + if let Ok(mut transform) = transforms.get_mut(current_entity) { + for curve in curves { + // Some curves have only one keyframe used to set a transform + if curve.keyframe_timestamps.len() == 1 { match &curve.keyframes { Keyframes::Rotation(keyframes) => { - let rot_start = keyframes[step_start]; - let mut rot_end = keyframes[step_start + 1]; - // Choose the smallest angle for the rotation - if rot_end.dot(rot_start) < 0.0 { - rot_end = -rot_end; - } - // Rotations are using a spherical linear interpolation - transform.rotation = - rot_start.normalize().slerp(rot_end.normalize(), lerp); + transform.rotation = transform.rotation.slerp(keyframes[0], weight); } Keyframes::Translation(keyframes) => { - let translation_start = keyframes[step_start]; - let translation_end = keyframes[step_start + 1]; - let result = translation_start.lerp(translation_end, lerp); - transform.translation = result; + transform.translation = + transform.translation.lerp(keyframes[0], weight); } Keyframes::Scale(keyframes) => { - let scale_start = keyframes[step_start]; - let scale_end = keyframes[step_start + 1]; - let result = scale_start.lerp(scale_end, lerp); - transform.scale = result; + transform.scale = transform.scale.lerp(keyframes[0], weight); + } + } + continue; + } + + // Find the current keyframe + // PERF: finding the current keyframe can be optimised + let step_start = match curve + .keyframe_timestamps + .binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap()) + { + Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => { + // This curve is finished. Freeze the last frame. + curve.keyframe_timestamps.len() - 2 + } + Ok(i) => i, + Err(0) => continue, // this curve isn't started yet + Err(n) if n > curve.keyframe_timestamps.len() - 1 => { + // This curve is finished. Freeze the last frame. + curve.keyframe_timestamps.len() - 2 + } + Err(i) => i - 1, + }; + let ts_start = curve.keyframe_timestamps[step_start]; + let ts_end = curve.keyframe_timestamps[step_start + 1]; + let lerp = (elapsed.min(ts_end) - ts_start) / (ts_end - ts_start); + + // Apply the keyframe + match &curve.keyframes { + Keyframes::Rotation(keyframes) => { + let rot_start = keyframes[step_start]; + let mut rot_end = keyframes[step_start + 1]; + // Choose the smallest angle for the rotation + if rot_end.dot(rot_start) < 0.0 { + rot_end = -rot_end; } + // Rotations are using a spherical linear interpolation + let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp); + transform.rotation = transform.rotation.slerp(rot, weight); + } + Keyframes::Translation(keyframes) => { + let translation_start = keyframes[step_start]; + let translation_end = keyframes[step_start + 1]; + let result = translation_start.lerp(translation_end, lerp); + transform.translation = transform.translation.lerp(result, weight); + } + Keyframes::Scale(keyframes) => { + let scale_start = keyframes[step_start]; + let scale_end = keyframes[step_start + 1]; + let result = scale_start.lerp(scale_end, lerp); + transform.scale = transform.scale.lerp(result, weight); } } } @@ -294,6 +406,44 @@ pub fn animation_player( } } +fn _find_entity( + entity: Entity, + path: &EntityPath, + children: &Query<&Children>, + names: &Query<&Name>, +) -> Option { + // PERF: finding the target entity can be optimised + let mut current_entity = entity; + // Ignore the first name, it is the root node which we already have + for part in path.parts.iter().skip(1) { + let mut found = false; + if let Ok(children) = children.get(current_entity) { + for child in children.deref() { + if let Ok(name) = names.get(*child) { + if name == part { + // Found a children with the right name, continue to the next part + current_entity = *child; + found = true; + break; + } + } + } + } + if !found { + warn!("Entity not found for path {:?} on part {:?}", path, part); + return None; + } + } + Some(current_entity) +} + +fn _update_transitions(player: &mut AnimationPlayer, time: &Time) { + player.transitions.retain_mut(|animation| { + animation.current_weight -= animation.weight_decline_per_sec * time.delta_seconds(); + animation.current_weight > 0.0 + }); +} + /// Adds animation support to an app #[derive(Default)] pub struct AnimationPlugin {} diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index c19fa97df34d2..c7a1cae86994c 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -78,7 +78,7 @@ fn setup_scene_once_loaded( ) { if !*done { if let Ok(mut player) = player.get_single_mut() { - player.play(animations.0[0].clone_weak()).repeat(); + player.play(animations.0[0].clone_weak(), None).repeat(); *done = true; } } @@ -122,7 +122,7 @@ fn keyboard_animation_control( if keyboard_input.just_pressed(KeyCode::Return) { *current_animation = (*current_animation + 1) % animations.0.len(); player - .play(animations.0[*current_animation].clone_weak()) + .play(animations.0[*current_animation].clone_weak(), None) .repeat(); } } diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index faa090be04c64..dd90a26559198 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -111,7 +111,7 @@ fn setup( // Create the animation player, and set it to repeat let mut player = AnimationPlayer::default(); - player.play(animations.add(animation)).repeat(); + player.play(animations.add(animation), None).repeat(); // Create the scene that will be animated // First entity is the planet diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 4011ddec374f5..0794699e8461e 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -190,7 +190,7 @@ fn setup_scene_once_loaded( ) { if !*done && player.iter().len() == foxes.count { for mut player in &mut player { - player.play(animations.0[0].clone_weak()).repeat(); + player.play(animations.0[0].clone_weak(), None).repeat(); } *done = true; } @@ -266,7 +266,7 @@ fn keyboard_animation_control( if keyboard_input.just_pressed(KeyCode::Return) { player - .play(animations.0[*current_animation].clone_weak()) + .play(animations.0[*current_animation].clone_weak(), None) .repeat(); } } diff --git a/examples/tools/scene_viewer.rs b/examples/tools/scene_viewer.rs index 5537437af1fc8..d9ed426b14ab2 100644 --- a/examples/tools/scene_viewer.rs +++ b/examples/tools/scene_viewer.rs @@ -201,7 +201,7 @@ fn start_animation( if !*done { if let Ok(mut player) = player.get_single_mut() { if let Some(animation) = scene_handle.animations.first() { - player.play(animation.clone_weak()).repeat(); + player.play(animation.clone_weak(), None).repeat(); *done = true; } } @@ -233,7 +233,10 @@ fn keyboard_animation_control( // change the animation the frame after return was pressed *current_animation = (*current_animation + 1) % scene_handle.animations.len(); player - .play(scene_handle.animations[*current_animation].clone_weak()) + .play( + scene_handle.animations[*current_animation].clone_weak(), + None, + ) .repeat(); *changing = false; }