Skip to content

Commit fa15b31

Browse files
committed
Smooth Transition between Animations (#6922)
# Objective - Fixes #6338 This PR allows for smooth transitions between different animations. ## Solution - This PR uses very simple linear blending of animations. - When starting a new animation, you can give it a duration, and throughout that duration, the previous and the new animation are being linearly blended, until only the new animation is running. - I'm aware of bevyengine/rfcs#49 and bevyengine/rfcs#51, which are more complete solutions to this problem, but they seem still far from being implemented. Until they're ready, this PR allows for the most basic use case of blending, i.e. smoothly transitioning between different animations. ## Migration Guide - no bc breaking changes
1 parent a41e869 commit fa15b31

File tree

3 files changed

+214
-42
lines changed

3 files changed

+214
-42
lines changed

crates/bevy_animation/src/lib.rs

+204-40
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
#![warn(missing_docs)]
44

55
use std::ops::Deref;
6+
use std::time::Duration;
67

78
use bevy_app::{App, CoreStage, Plugin};
89
use bevy_asset::{AddAsset, Assets, Handle};
910
use bevy_core::Name;
1011
use bevy_ecs::{
11-
change_detection::DetectChanges,
12+
change_detection::{DetectChanges, Mut},
1213
entity::Entity,
1314
prelude::Component,
1415
query::With,
@@ -114,22 +115,18 @@ impl AnimationClip {
114115
}
115116
}
116117

117-
/// Animation controls
118-
#[derive(Component, Reflect)]
119-
#[reflect(Component)]
120-
pub struct AnimationPlayer {
121-
paused: bool,
118+
#[derive(Reflect)]
119+
struct PlayingAnimation {
122120
repeat: bool,
123121
speed: f32,
124122
elapsed: f32,
125123
animation_clip: Handle<AnimationClip>,
126124
path_cache: Vec<Vec<Option<Entity>>>,
127125
}
128126

129-
impl Default for AnimationPlayer {
127+
impl Default for PlayingAnimation {
130128
fn default() -> Self {
131129
Self {
132-
paused: false,
133130
repeat: false,
134131
speed: 1.0,
135132
elapsed: 0.0,
@@ -139,33 +136,106 @@ impl Default for AnimationPlayer {
139136
}
140137
}
141138

139+
/// An animation that is being faded out as part of a transition
140+
struct AnimationTransition {
141+
/// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out.
142+
current_weight: f32,
143+
/// How much to decrease `current_weight` per second
144+
weight_decline_per_sec: f32,
145+
/// The animation that is being faded out
146+
animation: PlayingAnimation,
147+
}
148+
149+
/// Animation controls
150+
#[derive(Component, Default, Reflect)]
151+
#[reflect(Component)]
152+
pub struct AnimationPlayer {
153+
paused: bool,
154+
155+
animation: PlayingAnimation,
156+
157+
// List of previous animations we're currently transitioning away from.
158+
// Usually this is empty, when transitioning between animations, there is
159+
// one entry. When another animation transition happens while a transition
160+
// is still ongoing, then there can be more than one entry.
161+
// Once a transition is finished, it will be automatically removed from the list
162+
#[reflect(ignore)]
163+
transitions: Vec<AnimationTransition>,
164+
}
165+
142166
impl AnimationPlayer {
143167
/// Start playing an animation, resetting state of the player
168+
/// This will use a linear blending between the previous and the new animation to make a smooth transition
144169
pub fn start(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
145-
*self = Self {
170+
self.animation = PlayingAnimation {
146171
animation_clip: handle,
147172
..Default::default()
148173
};
174+
175+
// We want a hard transition.
176+
// In case any previous transitions are still playing, stop them
177+
self.transitions.clear();
178+
179+
self
180+
}
181+
182+
/// Start playing an animation, resetting state of the player
183+
/// This will use a linear blending between the previous and the new animation to make a smooth transition
184+
pub fn start_with_transition(
185+
&mut self,
186+
handle: Handle<AnimationClip>,
187+
transition_duration: Duration,
188+
) -> &mut Self {
189+
let mut animation = PlayingAnimation {
190+
animation_clip: handle,
191+
..Default::default()
192+
};
193+
std::mem::swap(&mut animation, &mut self.animation);
194+
195+
// Add the current transition. If other transitions are still ongoing,
196+
// this will keep those transitions running and cause a transition between
197+
// the output of that previous transition to the new animation.
198+
self.transitions.push(AnimationTransition {
199+
current_weight: 1.0,
200+
weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(),
201+
animation,
202+
});
203+
149204
self
150205
}
151206

152207
/// Start playing an animation, resetting state of the player, unless the requested animation is already playing.
208+
/// If `transition_duration` is set, this will use a linear blending
209+
/// between the previous and the new animation to make a smooth transition
153210
pub fn play(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
154-
if self.animation_clip != handle || self.is_paused() {
211+
if self.animation.animation_clip != handle || self.is_paused() {
155212
self.start(handle);
156213
}
157214
self
158215
}
159216

217+
/// Start playing an animation, resetting state of the player, unless the requested animation is already playing.
218+
/// This will use a linear blending between the previous and the new animation to make a smooth transition
219+
pub fn play_with_transition(
220+
&mut self,
221+
handle: Handle<AnimationClip>,
222+
transition_duration: Duration,
223+
) -> &mut Self {
224+
if self.animation.animation_clip != handle || self.is_paused() {
225+
self.start_with_transition(handle, transition_duration);
226+
}
227+
self
228+
}
229+
160230
/// Set the animation to repeat
161231
pub fn repeat(&mut self) -> &mut Self {
162-
self.repeat = true;
232+
self.animation.repeat = true;
163233
self
164234
}
165235

166236
/// Stop the animation from repeating
167237
pub fn stop_repeating(&mut self) -> &mut Self {
168-
self.repeat = false;
238+
self.animation.repeat = false;
169239
self
170240
}
171241

@@ -186,23 +256,23 @@ impl AnimationPlayer {
186256

187257
/// Speed of the animation playback
188258
pub fn speed(&self) -> f32 {
189-
self.speed
259+
self.animation.speed
190260
}
191261

192262
/// Set the speed of the animation playback
193263
pub fn set_speed(&mut self, speed: f32) -> &mut Self {
194-
self.speed = speed;
264+
self.animation.speed = speed;
195265
self
196266
}
197267

198268
/// Time elapsed playing the animation
199269
pub fn elapsed(&self) -> f32 {
200-
self.elapsed
270+
self.animation.elapsed
201271
}
202272

203273
/// Seek to a specific time in the animation
204274
pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self {
205-
self.elapsed = elapsed;
275+
self.animation.elapsed = elapsed;
206276
self
207277
}
208278
}
@@ -283,37 +353,119 @@ pub fn animation_player(
283353
mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>,
284354
) {
285355
animation_players.par_for_each_mut(10, |(root, maybe_parent, mut player)| {
286-
let Some(animation_clip) = animations.get(&player.animation_clip) else { return };
287-
// Continue if paused unless the `AnimationPlayer` was changed
288-
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
289-
if player.paused && !player.is_changed() {
290-
return;
291-
}
292-
if !player.paused {
293-
player.elapsed += time.delta_seconds() * player.speed;
356+
update_transitions(&mut player, &time);
357+
run_animation_player(
358+
root,
359+
player,
360+
&time,
361+
&animations,
362+
&names,
363+
&transforms,
364+
maybe_parent,
365+
&parents,
366+
&children,
367+
);
368+
});
369+
}
370+
371+
#[allow(clippy::too_many_arguments)]
372+
fn run_animation_player(
373+
root: Entity,
374+
mut player: Mut<AnimationPlayer>,
375+
time: &Time,
376+
animations: &Assets<AnimationClip>,
377+
names: &Query<&Name>,
378+
transforms: &Query<&mut Transform>,
379+
maybe_parent: Option<&Parent>,
380+
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
381+
children: &Query<&Children>,
382+
) {
383+
let paused = player.paused;
384+
// Continue if paused unless the `AnimationPlayer` was changed
385+
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
386+
if paused && !player.is_changed() {
387+
return;
388+
}
389+
390+
// Apply the main animation
391+
apply_animation(
392+
1.0,
393+
&mut player.animation,
394+
paused,
395+
root,
396+
time,
397+
animations,
398+
names,
399+
transforms,
400+
maybe_parent,
401+
parents,
402+
children,
403+
);
404+
405+
// Apply any potential fade-out transitions from previous animations
406+
for AnimationTransition {
407+
current_weight,
408+
animation,
409+
..
410+
} in &mut player.transitions
411+
{
412+
apply_animation(
413+
*current_weight,
414+
animation,
415+
paused,
416+
root,
417+
time,
418+
animations,
419+
names,
420+
transforms,
421+
maybe_parent,
422+
parents,
423+
children,
424+
);
425+
}
426+
}
427+
428+
#[allow(clippy::too_many_arguments)]
429+
fn apply_animation(
430+
weight: f32,
431+
animation: &mut PlayingAnimation,
432+
paused: bool,
433+
root: Entity,
434+
time: &Time,
435+
animations: &Assets<AnimationClip>,
436+
names: &Query<&Name>,
437+
transforms: &Query<&mut Transform>,
438+
maybe_parent: Option<&Parent>,
439+
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
440+
children: &Query<&Children>,
441+
) {
442+
if let Some(animation_clip) = animations.get(&animation.animation_clip) {
443+
if !paused {
444+
animation.elapsed += time.delta_seconds() * animation.speed;
294445
}
295-
let mut elapsed = player.elapsed;
296-
if player.repeat {
446+
let mut elapsed = animation.elapsed;
447+
if animation.repeat {
297448
elapsed %= animation_clip.duration;
298449
}
299450
if elapsed < 0.0 {
300451
elapsed += animation_clip.duration;
301452
}
302-
if player.path_cache.len() != animation_clip.paths.len() {
303-
player.path_cache = vec![Vec::new(); animation_clip.paths.len()];
453+
if animation.path_cache.len() != animation_clip.paths.len() {
454+
animation.path_cache = vec![Vec::new(); animation_clip.paths.len()];
304455
}
305-
if !verify_no_ancestor_player(maybe_parent, &parents) {
456+
if !verify_no_ancestor_player(maybe_parent, parents) {
306457
warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root);
307458
return;
308459
}
460+
309461
for (path, bone_id) in &animation_clip.paths {
310-
let cached_path = &mut player.path_cache[*bone_id];
462+
let cached_path = &mut animation.path_cache[*bone_id];
311463
let curves = animation_clip.get_curves(*bone_id).unwrap();
312-
let Some(target) = find_bone(root, path, &children, &names, cached_path) else { continue };
464+
let Some(target) = find_bone(root, path, children, names, cached_path) else { continue };
313465
// SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias
314466
// any of their descendant Transforms.
315-
//
316-
// The system scheduler prevents any other system from mutating Transforms at the same time,
467+
//
468+
// The system scheduler prevents any other system from mutating Transforms at the same time,
317469
// so the only way this fetch can alias is if two AnimationPlayers are targetting the same bone.
318470
// This can only happen if there are two or more AnimationPlayers are ancestors to the same
319471
// entities. By verifying that there is no other AnimationPlayer in the ancestors of a
@@ -327,11 +479,16 @@ pub fn animation_player(
327479
// Some curves have only one keyframe used to set a transform
328480
if curve.keyframe_timestamps.len() == 1 {
329481
match &curve.keyframes {
330-
Keyframes::Rotation(keyframes) => transform.rotation = keyframes[0],
482+
Keyframes::Rotation(keyframes) => {
483+
transform.rotation = transform.rotation.slerp(keyframes[0], weight);
484+
}
331485
Keyframes::Translation(keyframes) => {
332-
transform.translation = keyframes[0];
486+
transform.translation =
487+
transform.translation.lerp(keyframes[0], weight);
488+
}
489+
Keyframes::Scale(keyframes) => {
490+
transform.scale = transform.scale.lerp(keyframes[0], weight);
333491
}
334-
Keyframes::Scale(keyframes) => transform.scale = keyframes[0],
335492
}
336493
continue;
337494
}
@@ -362,24 +519,31 @@ pub fn animation_player(
362519
rot_end = -rot_end;
363520
}
364521
// Rotations are using a spherical linear interpolation
365-
transform.rotation =
366-
rot_start.normalize().slerp(rot_end.normalize(), lerp);
522+
let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp);
523+
transform.rotation = transform.rotation.slerp(rot, weight);
367524
}
368525
Keyframes::Translation(keyframes) => {
369526
let translation_start = keyframes[step_start];
370527
let translation_end = keyframes[step_start + 1];
371528
let result = translation_start.lerp(translation_end, lerp);
372-
transform.translation = result;
529+
transform.translation = transform.translation.lerp(result, weight);
373530
}
374531
Keyframes::Scale(keyframes) => {
375532
let scale_start = keyframes[step_start];
376533
let scale_end = keyframes[step_start + 1];
377534
let result = scale_start.lerp(scale_end, lerp);
378-
transform.scale = result;
535+
transform.scale = transform.scale.lerp(result, weight);
379536
}
380537
}
381538
}
382539
}
540+
}
541+
}
542+
543+
fn update_transitions(player: &mut AnimationPlayer, time: &Time) {
544+
player.transitions.retain_mut(|animation| {
545+
animation.current_weight -= animation.weight_decline_per_sec * time.delta_seconds();
546+
animation.current_weight > 0.0
383547
});
384548
}
385549

examples/animation/animated_fox.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Plays animations from a skinned glTF.
22
33
use std::f32::consts::PI;
4+
use std::time::Duration;
45

56
use bevy::prelude::*;
67

@@ -122,7 +123,10 @@ fn keyboard_animation_control(
122123
if keyboard_input.just_pressed(KeyCode::Return) {
123124
*current_animation = (*current_animation + 1) % animations.0.len();
124125
player
125-
.play(animations.0[*current_animation].clone_weak())
126+
.play_with_transition(
127+
animations.0[*current_animation].clone_weak(),
128+
Duration::from_millis(250),
129+
)
126130
.repeat();
127131
}
128132
}

0 commit comments

Comments
 (0)