Merge ff886e60f2
into 6792cebfd0
This commit is contained in:
commit
77b6ec908f
@ -83,6 +83,7 @@ use crate::{AnimationClip, AnimationTargetId};
|
||||
/// mask corresponds to a *mask group*, which is a set of animation targets
|
||||
/// (bones). An animation target can belong to any number of mask groups within
|
||||
/// the context of an animation graph.
|
||||
/// Note - Avoid using 0 as a *mask* as that is the default mask for non masked nodes.
|
||||
///
|
||||
/// When the appropriate bit is set in a node's mask, neither the node nor its
|
||||
/// descendants will animate any animation targets belonging to that mask group.
|
||||
|
@ -24,7 +24,7 @@ use core::{
|
||||
iter, slice,
|
||||
};
|
||||
use graph::AnimationNodeType;
|
||||
use prelude::AnimationCurveEvaluator;
|
||||
use prelude::{handle_node_transition, AnimationCurveEvaluator};
|
||||
|
||||
use crate::{
|
||||
graph::{AnimationGraphHandle, ThreadedAnimationGraphs},
|
||||
@ -60,10 +60,53 @@ pub mod prelude {
|
||||
use crate::{
|
||||
animation_curves::AnimationCurve,
|
||||
graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex},
|
||||
transition::{advance_transitions, expire_completed_transitions, AnimationTransitions},
|
||||
transition::{expire_completed_transitions, AnimationTransitions},
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
|
||||
/// Adds animation support to an app
|
||||
#[derive(Default)]
|
||||
pub struct AnimationPlugin;
|
||||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset::<AnimationClip>()
|
||||
.init_asset::<AnimationGraph>()
|
||||
.init_asset_loader::<AnimationGraphAssetLoader>()
|
||||
.register_asset_reflect::<AnimationClip>()
|
||||
.register_asset_reflect::<AnimationGraph>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.register_type::<AnimationTarget>()
|
||||
.register_type::<AnimationTransitions>()
|
||||
.register_type::<AnimationGraphHandle>()
|
||||
.register_type::<NodeIndex>()
|
||||
.register_type::<ThreadedAnimationGraphs>()
|
||||
.init_resource::<ThreadedAnimationGraphs>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
graph::thread_animation_graphs.before(AssetEventSystems),
|
||||
handle_node_transition,
|
||||
advance_animations,
|
||||
// TODO: `animate_targets` can animate anything, so
|
||||
// ambiguity testing currently considers it ambiguous with
|
||||
// every other system in `PostUpdate`. We may want to move
|
||||
// it to its own system set after `Update` but before
|
||||
// `PostUpdate`. For now, we just disable ambiguity testing
|
||||
// for this system.
|
||||
animate_targets
|
||||
.before(bevy_render::mesh::inherit_weights)
|
||||
.ambiguous_with_all(),
|
||||
trigger_untargeted_animation_events,
|
||||
expire_completed_transitions,
|
||||
)
|
||||
.chain()
|
||||
.in_set(AnimationSystems)
|
||||
.before(TransformSystems::Propagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The [UUID namespace] of animation targets (e.g. bones).
|
||||
///
|
||||
/// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based)
|
||||
@ -1223,49 +1266,6 @@ pub fn animate_targets(
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds animation support to an app
|
||||
#[derive(Default)]
|
||||
pub struct AnimationPlugin;
|
||||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset::<AnimationClip>()
|
||||
.init_asset::<AnimationGraph>()
|
||||
.init_asset_loader::<AnimationGraphAssetLoader>()
|
||||
.register_asset_reflect::<AnimationClip>()
|
||||
.register_asset_reflect::<AnimationGraph>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.register_type::<AnimationTarget>()
|
||||
.register_type::<AnimationTransitions>()
|
||||
.register_type::<AnimationGraphHandle>()
|
||||
.register_type::<NodeIndex>()
|
||||
.register_type::<ThreadedAnimationGraphs>()
|
||||
.init_resource::<ThreadedAnimationGraphs>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
graph::thread_animation_graphs.before(AssetEventSystems),
|
||||
advance_transitions,
|
||||
advance_animations,
|
||||
// TODO: `animate_targets` can animate anything, so
|
||||
// ambiguity testing currently considers it ambiguous with
|
||||
// every other system in `PostUpdate`. We may want to move
|
||||
// it to its own system set after `Update` but before
|
||||
// `PostUpdate`. For now, we just disable ambiguity testing
|
||||
// for this system.
|
||||
animate_targets
|
||||
.before(bevy_render::mesh::inherit_weights)
|
||||
.ambiguous_with_all(),
|
||||
trigger_untargeted_animation_events,
|
||||
expire_completed_transitions,
|
||||
)
|
||||
.chain()
|
||||
.in_set(AnimationSystems)
|
||||
.before(TransformSystems::Propagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationTargetId {
|
||||
/// Creates a new [`AnimationTargetId`] by hashing a list of names.
|
||||
///
|
||||
|
@ -1,8 +1,11 @@
|
||||
//! Animation transitions.
|
||||
//!
|
||||
//! Please note that this is an unstable temporary API. It may be replaced by a
|
||||
//! state machine in the future.
|
||||
//! Animation Transitioning logic goes here!
|
||||
//! This struct should in the later run be responsible for handling multi-state Animation Graph nodes.
|
||||
|
||||
use crate::{
|
||||
graph::{AnimationGraphHandle, AnimationNodeIndex},
|
||||
ActiveAnimation, AnimationPlayer,
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
reflect::ReflectComponent,
|
||||
@ -10,151 +13,181 @@ use bevy_ecs::{
|
||||
};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_time::Time;
|
||||
use core::time::Duration;
|
||||
use core::{f32, time::Duration};
|
||||
|
||||
use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer};
|
||||
|
||||
/// Manages fade-out of animation blend factors, allowing for smooth transitions
|
||||
/// between animations.
|
||||
/// Component responsible for managing transitions between multiple nodes or states.
|
||||
///
|
||||
/// To use this component, place it on the same entity as the
|
||||
/// [`AnimationPlayer`] and [`AnimationGraphHandle`](crate::AnimationGraphHandle). It'll take
|
||||
/// responsibility for adjusting the weight on the [`ActiveAnimation`] in order
|
||||
/// to fade out animations smoothly.
|
||||
/// It supports multiple independent "flows", where each flow represents a distinct active
|
||||
/// animation or state machine. A flow tracks the transition between two states over time.
|
||||
///
|
||||
/// When using an [`AnimationTransitions`] component, you should play all
|
||||
/// animations through the [`AnimationTransitions::play`] method, rather than by
|
||||
/// directly manipulating the [`AnimationPlayer`]. Playing animations through
|
||||
/// the [`AnimationPlayer`] directly will cause the [`AnimationTransitions`]
|
||||
/// component to get confused about which animation is the "main" animation, and
|
||||
/// transitions will usually be incorrect as a result.
|
||||
#[derive(Component, Default, Reflect)]
|
||||
#[reflect(Component, Default, Clone)]
|
||||
/// In the simplest case, `flow_amount` should be set to `1`, indicating a single flow.
|
||||
/// However, if multiple state machines or simultaneous animations are needed, `flow_amount`
|
||||
/// should be increased accordingly.
|
||||
///
|
||||
/// Is worth mentioning, that when using `AnimationTransitions`, you should avoid messing around with player!
|
||||
/// As he will do the heavy lifting for you! All you need to worry about is transitioning your flows!
|
||||
/// If you do play an additional animation directly via player this WILL BREAK!
|
||||
///
|
||||
/// It is also the user's responsibility to track which flow they are currently operating on
|
||||
/// when triggering transitions.
|
||||
/// Ex: Flow 0 - Plays idle,walks and so on. Affect whole body
|
||||
/// Flow 1 - Plays close hands - Affects only hand bones.
|
||||
#[derive(Component, Default, Reflect, Deref, DerefMut)]
|
||||
#[reflect(Component, Default)]
|
||||
#[require(AnimationGraphHandle, AnimationPlayer)]
|
||||
pub struct AnimationTransitions {
|
||||
main_animation: Option<AnimationNodeIndex>,
|
||||
#[deref]
|
||||
transitions: Vec<AnimationTransition>,
|
||||
/// Flows represent sequences of animation states.
|
||||
/// For example, in cases such as masked or additive animation scenarios, a user can easily define transitions between previous and new states.
|
||||
/// This concept is similar to "main" animations, but instead of one sole `ActiveAnimation`, we might have multiple active animation being controlled!
|
||||
flows: Vec<Option<AnimationNodeIndex>>,
|
||||
}
|
||||
|
||||
// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`.
|
||||
impl Clone for AnimationTransitions {
|
||||
fn clone(&self) -> Self {
|
||||
/// An animation node that is being faded out as part of a transition, note this controls the animations being played!
|
||||
#[derive(Debug, Reflect, Clone)]
|
||||
pub struct AnimationTransition {
|
||||
/// How much weight we will decrease according to the given user value
|
||||
duration: Duration,
|
||||
/// Node to transition from
|
||||
old_node: AnimationNodeIndex,
|
||||
/// Node to transition into
|
||||
new_node: AnimationNodeIndex,
|
||||
/// Acts similarly to a local variable, tracks how far into the transition are we, should start from 1. and go to 0
|
||||
weight: f32,
|
||||
}
|
||||
impl AnimationTransitions {
|
||||
/// Initializes the [`AnimationTransitions`] component, with ONE SINGLE flow meaning. It is expected to play only one animation at once
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
main_animation: self.main_animation,
|
||||
transitions: self.transitions.clone(),
|
||||
flows: vec![None; 1_usize],
|
||||
transitions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_from(&mut self, source: &Self) {
|
||||
self.main_animation = source.main_animation;
|
||||
self.transitions.clone_from(&source.transitions);
|
||||
}
|
||||
}
|
||||
|
||||
/// An animation that is being faded out as part of a transition
|
||||
#[derive(Debug, Clone, Copy, Reflect)]
|
||||
#[reflect(Clone)]
|
||||
pub 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: AnimationNodeIndex,
|
||||
}
|
||||
|
||||
impl AnimationTransitions {
|
||||
/// Creates a new [`AnimationTransitions`] component, ready to be added to
|
||||
/// an entity with an [`AnimationPlayer`].
|
||||
pub fn new() -> AnimationTransitions {
|
||||
AnimationTransitions::default()
|
||||
/// Define your flow amount and initializes your component, renember `flow_amount` is the amount of animation you want to be playing at once
|
||||
pub fn new_with_flow(flow_amount: usize) -> Self {
|
||||
Self {
|
||||
flows: vec![None; flow_amount],
|
||||
// Default transitions are instantaniously cleared
|
||||
transitions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays a new animation on the given [`AnimationPlayer`], fading out any
|
||||
/// existing animations that were already playing over the
|
||||
/// `transition_duration`.
|
||||
/// Transitions the specified flow from its current node to a new node over a given duration.
|
||||
///
|
||||
/// Pass [`Duration::ZERO`] to instantly switch to a new animation, avoiding
|
||||
/// any transition.
|
||||
pub fn play<'p>(
|
||||
/// This method manages transitions within one single flow.
|
||||
pub fn transition<'p>(
|
||||
&mut self,
|
||||
player: &'p mut AnimationPlayer,
|
||||
new_animation: AnimationNodeIndex,
|
||||
transition_duration: Duration,
|
||||
new_node: AnimationNodeIndex,
|
||||
duration: Duration,
|
||||
) -> &'p mut ActiveAnimation {
|
||||
if let Some(old_animation_index) = self.main_animation.replace(new_animation) {
|
||||
if let Some(old_animation) = player.animation_mut(old_animation_index) {
|
||||
if !old_animation.is_paused() {
|
||||
self.transitions.push(AnimationTransition {
|
||||
current_weight: old_animation.weight,
|
||||
weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(),
|
||||
animation: old_animation_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(old_node) = self.flows.get_mut(0) {
|
||||
// If is first time playing, just say old node equals new node
|
||||
let previous_node = old_node.unwrap_or(new_node);
|
||||
self.transitions.push(AnimationTransition {
|
||||
duration,
|
||||
old_node: previous_node,
|
||||
new_node,
|
||||
weight: 1.0,
|
||||
});
|
||||
*old_node = Some(new_node);
|
||||
|
||||
// Starts new animation, note we wont clear AnimationPlayer active animation hashmap here!
|
||||
player.start(new_node)
|
||||
} else {
|
||||
panic!("Flow position 0 is out of bounds!");
|
||||
}
|
||||
|
||||
// If already transitioning away from this animation, cancel the transition.
|
||||
// Otherwise the transition ending would incorrectly stop the new animation.
|
||||
self.transitions
|
||||
.retain(|transition| transition.animation != new_animation);
|
||||
|
||||
player.start(new_animation)
|
||||
}
|
||||
|
||||
/// Obtain the currently playing main animation.
|
||||
pub fn get_main_animation(&self) -> Option<AnimationNodeIndex> {
|
||||
self.main_animation
|
||||
/// Transitions the specified flow from its current node to a new node over a given duration.
|
||||
///
|
||||
/// This method manages transitions within a specific flow, allowing multiple independent
|
||||
/// state machines or animation layers to transition separately. If the flow has no previous node,
|
||||
/// it will treat the `new_node` as both the old and new node during the transition.
|
||||
pub fn transition_flows<'p>(
|
||||
&mut self,
|
||||
player: &'p mut AnimationPlayer,
|
||||
new_node: AnimationNodeIndex,
|
||||
flow_position: usize,
|
||||
duration: Duration,
|
||||
) -> &'p mut ActiveAnimation {
|
||||
// Check if flow exists
|
||||
if let Some(old_node) = self.flows.get_mut(flow_position) {
|
||||
// If is first time playing, just say old node equals new node
|
||||
let previous_node = old_node.unwrap_or(new_node);
|
||||
self.transitions.push(AnimationTransition {
|
||||
duration,
|
||||
old_node: previous_node,
|
||||
new_node,
|
||||
weight: 1.0,
|
||||
});
|
||||
*old_node = Some(new_node);
|
||||
|
||||
// Starts new animation, note we wont clear AnimationPlayer active animation hashmap here!
|
||||
player.start(new_node)
|
||||
} else {
|
||||
panic!("Flow position {flow_position} is out of bounds!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that alters the weight of currently-playing transitions based on
|
||||
/// the current time and decline amount.
|
||||
pub fn advance_transitions(
|
||||
/// System responsible for handling [`AnimationTransitions`] transitioning nodes among each other. According to the pacing defined by user.
|
||||
pub fn handle_node_transition(
|
||||
mut query: Query<(&mut AnimationTransitions, &mut AnimationPlayer)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
// We use a "greedy layer" system here. The top layer (most recent
|
||||
// transition) gets as much as weight as it wants, and the remaining amount
|
||||
// is divided between all the other layers, eventually culminating in the
|
||||
// currently-playing animation receiving whatever's left. This results in a
|
||||
// nicely normalized weight.
|
||||
for (mut animation_transitions, mut player) in query.iter_mut() {
|
||||
for (mut animation_transitions, mut animation_player) in query.iter_mut() {
|
||||
let mut remaining_weight = 1.0;
|
||||
|
||||
for transition in &mut animation_transitions.transitions.iter_mut().rev() {
|
||||
// Decrease weight.
|
||||
transition.current_weight = (transition.current_weight
|
||||
- transition.weight_decline_per_sec * time.delta_secs())
|
||||
for transition in animation_transitions.iter_mut() {
|
||||
// How much to transition per tick!
|
||||
transition.weight = (transition.weight
|
||||
- 1. / transition.duration.as_secs_f32() * time.delta_secs())
|
||||
.max(0.0);
|
||||
|
||||
// Update weight.
|
||||
let Some(ref mut animation) = player.animation_mut(transition.animation) else {
|
||||
continue;
|
||||
};
|
||||
animation.weight = transition.current_weight * remaining_weight;
|
||||
remaining_weight -= animation.weight;
|
||||
}
|
||||
// Handles edge case when duration is zero
|
||||
if transition.duration == Duration::ZERO {
|
||||
transition.weight = 0.0;
|
||||
}
|
||||
|
||||
if let Some(main_animation_index) = animation_transitions.main_animation {
|
||||
if let Some(ref mut animation) = player.animation_mut(main_animation_index) {
|
||||
animation.weight = remaining_weight;
|
||||
// Handles edge case first flow
|
||||
if transition.old_node.eq(&transition.new_node) {
|
||||
if let Some(old_node) = animation_player.animation_mut(transition.old_node) {
|
||||
remaining_weight -= transition.weight * remaining_weight;
|
||||
old_node.weight = remaining_weight;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(old_node) = animation_player.animation_mut(transition.old_node) {
|
||||
old_node.weight = transition.weight * remaining_weight;
|
||||
remaining_weight -= old_node.weight;
|
||||
}
|
||||
|
||||
if let Some(new_node) = animation_player.animation_mut(transition.new_node) {
|
||||
new_node.weight = remaining_weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that removed transitions that have completed from the
|
||||
/// A system that removes transitions that have completed from the
|
||||
/// [`AnimationTransitions`] object.
|
||||
pub fn expire_completed_transitions(
|
||||
mut query: Query<(&mut AnimationTransitions, &mut AnimationPlayer)>,
|
||||
) {
|
||||
for (mut animation_transitions, mut player) in query.iter_mut() {
|
||||
animation_transitions.transitions.retain(|transition| {
|
||||
let expire = transition.current_weight <= 0.0;
|
||||
if expire {
|
||||
player.stop(transition.animation);
|
||||
for (mut animation_transitions, mut animation_player) in query.iter_mut() {
|
||||
animation_transitions.retain(|transition| {
|
||||
let should_expire = transition.weight <= 0.0;
|
||||
let first_init = transition.new_node.eq(&transition.old_node);
|
||||
//We need to handle the edge case of first initialization, we shouldnt stop in that case!
|
||||
if should_expire && !first_init {
|
||||
animation_player.stop(transition.old_node);
|
||||
false
|
||||
} else {
|
||||
!(should_expire && first_init)
|
||||
}
|
||||
!expire
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
|
||||
use bevy::{animation::RepeatAnimation, pbr::CascadeShadowConfigBuilder, prelude::*};
|
||||
|
||||
const FOX_PATH: &str = "models/animated/Fox.glb";
|
||||
|
||||
fn main() {
|
||||
@ -110,12 +109,9 @@ fn setup_scene_once_loaded(
|
||||
for (entity, mut player) in &mut players {
|
||||
let mut transitions = AnimationTransitions::new();
|
||||
|
||||
// Make sure to start the animation via the `AnimationTransitions`
|
||||
// component. The `AnimationTransitions` component wants to manage all
|
||||
// the animations and will get confused if the animations are started
|
||||
// directly via the `AnimationPlayer`.
|
||||
// Use only transitions to control your animation player!
|
||||
transitions
|
||||
.play(&mut player, animations.animations[0], Duration::ZERO)
|
||||
.transition(&mut player, animations.animations[0], Duration::ZERO)
|
||||
.repeat();
|
||||
|
||||
commands
|
||||
@ -173,10 +169,11 @@ fn keyboard_control(
|
||||
*current_animation = (*current_animation + 1) % animations.animations.len();
|
||||
|
||||
transitions
|
||||
.play(
|
||||
.transition_flows(
|
||||
&mut player,
|
||||
animations.animations[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
0,
|
||||
Duration::from_millis(200),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
|
@ -169,17 +169,13 @@ fn setup_scene_once_loaded(
|
||||
running_animation.add_event_to_target(feet.back_left, 0.0, OnStep);
|
||||
running_animation.add_event_to_target(feet.back_right, 0.125, OnStep);
|
||||
|
||||
// Start the animation
|
||||
|
||||
// Boot up your transition flows
|
||||
let mut transitions = AnimationTransitions::new();
|
||||
|
||||
// Make sure to start the animation via the `AnimationTransitions`
|
||||
// component. The `AnimationTransitions` component wants to manage all
|
||||
// the animations and will get confused if the animations are started
|
||||
// directly via the `AnimationPlayer`.
|
||||
transitions
|
||||
.play(&mut player, animations.index, Duration::ZERO)
|
||||
.repeat();
|
||||
// Make sure to play your new animation!
|
||||
transitions.transition_flows(&mut player, animations.index, 0, Duration::ZERO);
|
||||
|
||||
player.play(animations.index).repeat();
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
|
@ -318,7 +318,7 @@ fn keyboard_animation_control(
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
transitions
|
||||
.play(
|
||||
.transition(
|
||||
&mut player,
|
||||
animations.node_indices[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
|
@ -285,7 +285,7 @@ mod animation {
|
||||
if let Ok((entity, mut player)) = players.get_mut(child) {
|
||||
let mut transitions = AnimationTransitions::new();
|
||||
transitions
|
||||
.play(&mut player, animation.animation, Duration::ZERO)
|
||||
.transition(&mut player, animation.animation, Duration::from_millis(200))
|
||||
.seek_to(0.5)
|
||||
.pause();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user