From 0404c1bc7805e491d9ff8b4a572feb9126680987 Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sun, 11 Dec 2022 18:07:25 -0800 Subject: [PATCH 1/5] Animation Blending --- crates/bevy_animation/src/lib.rs | 226 +++++++++++++++++++---- examples/animation/animated_fox.rs | 4 +- examples/animation/animated_transform.rs | 2 +- examples/stress_tests/many_foxes.rs | 4 +- 4 files changed, 190 insertions(+), 46 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9b9e8de991af9..78e42674c45a5 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, query::With, @@ -114,11 +115,8 @@ impl AnimationClip { } } -/// Animation controls -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct AnimationPlayer { - paused: bool, +#[derive(Reflect)] +struct PlayingAnimation { repeat: bool, speed: f32, elapsed: f32, @@ -126,10 +124,9 @@ pub struct AnimationPlayer { path_cache: Vec>>, } -impl Default for AnimationPlayer { +impl Default for PlayingAnimation { fn default() -> Self { Self { - paused: false, repeat: false, speed: 1.0, elapsed: 0.0, @@ -139,33 +136,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 } @@ -186,23 +237,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 } } @@ -283,37 +334,118 @@ pub fn animation_player( mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>, ) { animation_players.par_for_each_mut(10, |(root, maybe_parent, mut player)| { - let Some(animation_clip) = animations.get(&player.animation_clip) else { return }; - // 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() { - return; - } - if !player.paused { - player.elapsed += time.delta_seconds() * player.speed; + _update_transitions(&mut player, &time); + _run_animation_player( + root, + player, + &time, + &animations, + &names, + &transforms, + maybe_parent, + &parents, + &children, + ); + }); +} + +fn _run_animation_player( + root: Entity, + mut player: Mut, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &Query<&mut Transform>, + maybe_parent: Option<&Parent>, + parents: &Query<(Option>, Option<&Parent>)>, + 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, + root, + time, + animations, + names, + transforms, + maybe_parent, + parents, + 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, + root, + time, + animations, + names, + transforms, + maybe_parent, + parents, + children, + ); + } +} + +#[allow(clippy::too_many_arguments)] +fn _apply_animation( + weight: f32, + animation: &mut PlayingAnimation, + paused: bool, + root: Entity, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &Query<&mut Transform>, + maybe_parent: Option<&Parent>, + parents: &Query<(Option>, Option<&Parent>)>, + 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 = player.elapsed; - if player.repeat { + let mut elapsed = animation.elapsed; + if animation.repeat { elapsed %= animation_clip.duration; } if elapsed < 0.0 { elapsed += animation_clip.duration; } - if player.path_cache.len() != animation_clip.paths.len() { - player.path_cache = vec![Vec::new(); animation_clip.paths.len()]; + if animation.path_cache.len() != animation_clip.paths.len() { + animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; } if !verify_no_ancestor_player(maybe_parent, &parents) { warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root); return; } + for (path, bone_id) in &animation_clip.paths { - let cached_path = &mut player.path_cache[*bone_id]; + let cached_path = &mut animation.path_cache[*bone_id]; let curves = animation_clip.get_curves(*bone_id).unwrap(); let Some(target) = find_bone(root, path, &children, &names, cached_path) else { continue }; // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias // any of their descendant Transforms. - // - // The system scheduler prevents any other system from mutating Transforms at the same time, + // + // The system scheduler prevents any other system from mutating Transforms at the same time, // so the only way this fetch can alias is if two AnimationPlayers are targetting the same bone. // This can only happen if there are two or more AnimationPlayers are ancestors to the same // entities. By verifying that there is no other AnimationPlayer in the ancestors of a @@ -327,11 +459,16 @@ pub fn animation_player( // 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::Rotation(keyframes) => { + transform.rotation = transform.rotation.slerp(keyframes[0], weight); + } Keyframes::Translation(keyframes) => { - transform.translation = keyframes[0]; + transform.translation = + transform.translation.lerp(keyframes[0], weight); + } + Keyframes::Scale(keyframes) => { + transform.scale = transform.scale.lerp(keyframes[0], weight); } - Keyframes::Scale(keyframes) => transform.scale = keyframes[0], } continue; } @@ -362,24 +499,31 @@ pub fn animation_player( rot_end = -rot_end; } // Rotations are using a spherical linear interpolation - transform.rotation = - rot_start.normalize().slerp(rot_end.normalize(), lerp); + 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 = result; + 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 = result; + transform.scale = transform.scale.lerp(result, weight); } } } } + } +} + +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 }); } 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 ce8d9c7bc468d..a692710d6a9eb 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 9d5ae3d398b8c..390b71ddccd2c 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(); } } From 4150bd1dfd04209df306f67f460bcfee74a74603 Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sat, 17 Dec 2022 04:16:20 +0100 Subject: [PATCH 2/5] No underscore for private functions --- crates/bevy_animation/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 78e42674c45a5..87a7146d96670 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -334,8 +334,8 @@ pub fn animation_player( mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>, ) { animation_players.par_for_each_mut(10, |(root, maybe_parent, mut player)| { - _update_transitions(&mut player, &time); - _run_animation_player( + update_transitions(&mut player, &time); + run_animation_player( root, player, &time, @@ -349,7 +349,7 @@ pub fn animation_player( }); } -fn _run_animation_player( +fn run_animation_player( root: Entity, mut player: Mut, time: &Time, @@ -368,7 +368,7 @@ fn _run_animation_player( } // Apply the main animation - _apply_animation( + apply_animation( 1.0, &mut player.animation, paused, @@ -389,7 +389,7 @@ fn _run_animation_player( .. } in &mut player.transitions { - _apply_animation( + apply_animation( *current_weight, animation, paused, @@ -406,7 +406,7 @@ fn _run_animation_player( } #[allow(clippy::too_many_arguments)] -fn _apply_animation( +fn apply_animation( weight: f32, animation: &mut PlayingAnimation, paused: bool, @@ -520,7 +520,7 @@ fn _apply_animation( } } -fn _update_transitions(player: &mut AnimationPlayer, time: &Time) { +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 From dd1c92d8a14a9f634ea557abf66e1336fcedbfa3 Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sat, 17 Dec 2022 04:17:29 +0100 Subject: [PATCH 3/5] Add example for transition --- examples/animation/animated_fox.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index c7a1cae86994c..746c1b806d999 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -1,6 +1,7 @@ //! Plays animations from a skinned glTF. use std::f32::consts::PI; +use std::time::Duration; use bevy::prelude::*; @@ -122,7 +123,10 @@ 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(), None) + .play( + animations.0[*current_animation].clone_weak(), + Some(Duration::from_millis(250)), + ) .repeat(); } } From aa307ec50fff008c047db65151697d520cc29485 Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sat, 17 Dec 2022 04:30:54 +0100 Subject: [PATCH 4/5] Use {start,play}_with_transition instead of changing {start,play} API) --- crates/bevy_animation/src/lib.rs | 61 ++++++++++++++++-------- examples/animation/animated_fox.rs | 6 +-- examples/animation/animated_transform.rs | 2 +- examples/stress_tests/many_foxes.rs | 8 +++- 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 87a7146d96670..fc1815c002ed7 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -165,45 +165,64 @@ pub struct AnimationPlayer { impl AnimationPlayer { /// Start playing an animation, resetting state of the player - /// 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( + /// 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) -> &mut Self { + self.animation = PlayingAnimation { + animation_clip: handle, + ..Default::default() + }; + + // 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 + /// This will use a linear blending between the previous and the new animation to make a smooth transition + pub fn start_with_transition( &mut self, handle: Handle, - transition_duration: Option, + transition_duration: Duration, ) -> &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(); - } + + // 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, + }); + self } /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. /// 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( + pub fn play(&mut self, handle: Handle) -> &mut Self { + if self.animation.animation_clip != handle || self.is_paused() { + self.start(handle); + } + self + } + + /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. + /// This will use a linear blending between the previous and the new animation to make a smooth transition + pub fn play_with_transition( &mut self, handle: Handle, - transition_duration: Option, + transition_duration: Duration, ) -> &mut Self { if self.animation.animation_clip != handle || self.is_paused() { - self.start(handle, transition_duration); + self.start_with_transition(handle, transition_duration); } self } diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 746c1b806d999..1f1232d79d03a 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -79,7 +79,7 @@ fn setup_scene_once_loaded( ) { if !*done { if let Ok(mut player) = player.get_single_mut() { - player.play(animations.0[0].clone_weak(), None).repeat(); + player.play(animations.0[0].clone_weak()).repeat(); *done = true; } } @@ -123,9 +123,9 @@ fn keyboard_animation_control( if keyboard_input.just_pressed(KeyCode::Return) { *current_animation = (*current_animation + 1) % animations.0.len(); player - .play( + .play_with_transition( animations.0[*current_animation].clone_weak(), - Some(Duration::from_millis(250)), + Duration::from_millis(250), ) .repeat(); } diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index a692710d6a9eb..ce8d9c7bc468d 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), None).repeat(); + player.play(animations.add(animation)).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 390b71ddccd2c..58fcc5cc41646 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -2,6 +2,7 @@ //! animation to stress test skinned meshes. use std::f32::consts::PI; +use std::time::Duration; use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, @@ -190,7 +191,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(), None).repeat(); + player.play(animations.0[0].clone_weak()).repeat(); } *done = true; } @@ -266,7 +267,10 @@ fn keyboard_animation_control( if keyboard_input.just_pressed(KeyCode::Return) { player - .play(animations.0[*current_animation].clone_weak(), None) + .play_with_transition( + animations.0[*current_animation].clone_weak(), + Duration::from_millis(250), + ) .repeat(); } } From 22cf15f25a1b05c088e00841cb51034162231cca Mon Sep 17 00:00:00 2001 From: Sebastian Messmer Date: Sun, 8 Jan 2023 11:26:58 +0100 Subject: [PATCH 5/5] Fix clippy warnings --- crates/bevy_animation/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index fc1815c002ed7..6e098f0e24e50 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -368,6 +368,7 @@ pub fn animation_player( }); } +#[allow(clippy::too_many_arguments)] fn run_animation_player( root: Entity, mut player: Mut, @@ -452,7 +453,7 @@ fn apply_animation( if animation.path_cache.len() != animation_clip.paths.len() { animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; } - if !verify_no_ancestor_player(maybe_parent, &parents) { + if !verify_no_ancestor_player(maybe_parent, parents) { warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root); return; } @@ -460,7 +461,7 @@ fn apply_animation( for (path, bone_id) in &animation_clip.paths { let cached_path = &mut animation.path_cache[*bone_id]; let curves = animation_clip.get_curves(*bone_id).unwrap(); - let Some(target) = find_bone(root, path, &children, &names, cached_path) else { continue }; + let Some(target) = find_bone(root, path, children, names, cached_path) else { continue }; // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias // any of their descendant Transforms. //