From 713d91b7218b2ec77cac6f35e6eda6942f41b4c5 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:20:38 -0500 Subject: [PATCH 1/8] Improve Bloom 3D lighting (#11981) # Objective - With the recent lighting changes, the default configuration in the `bloom_3d` example is less clear what bloom actually does - See [this screenshot](https://github.com/bevyengine/bevy-website/pull/1023/files/4fdb1455d5a3371d69db32b036e38731342b48de#r1494648414) for a comparison. - `bloom_3d` additionally uses a for-loop to spawn the spheres, which can be turned into `commands::spawn_batch` call. - The text is black, which is difficult to see on the gray background. ## Solution - Increase emmisive values of materials. - Set text to white. ## Showcase Before: before After: image --------- Co-authored-by: Alice Cecile --- crates/bevy_core_pipeline/src/bloom/settings.rs | 2 ++ examples/3d/bloom_3d.rs | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 69c789933c686..d42cc412f262d 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -106,6 +106,8 @@ pub struct BloomSettings { impl BloomSettings { /// The default bloom preset. + /// + /// This uses the [`EnergyConserving`](BloomCompositeMode::EnergyConserving) composite mode. pub const NATURAL: Self = Self { intensity: 0.15, low_frequency_boost: 0.7, diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index 34101d35ffd55..b2fd467942997 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -36,19 +36,20 @@ fn setup_scene( transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }, - BloomSettings::default(), // 3. Enable bloom for the camera + // 3. Enable bloom for the camera + BloomSettings::NATURAL, )); let material_emissive1 = materials.add(StandardMaterial { - emissive: Color::linear_rgb(2300.0, 900.0, 300.0), // 4. Put something bright in a dark environment to see the effect + emissive: Color::linear_rgb(23000.0, 9000.0, 3000.0), // 4. Put something bright in a dark environment to see the effect ..default() }); let material_emissive2 = materials.add(StandardMaterial { - emissive: Color::linear_rgb(300.0, 2300.0, 900.0), + emissive: Color::linear_rgb(3000.0, 23000.0, 9000.0), ..default() }); let material_emissive3 = materials.add(StandardMaterial { - emissive: Color::linear_rgb(900.0, 300.0, 2300.0), + emissive: Color::linear_rgb(9000.0, 3000.0, 23000.0), ..default() }); let material_non_emissive = materials.add(StandardMaterial { @@ -60,6 +61,8 @@ fn setup_scene( for x in -5..5 { for z in -5..5 { + // This generates a pseudo-random integer between `[0, 6)`, but deterministically so + // the same spheres are always the same colors. let mut hasher = DefaultHasher::new(); (x, z).hash(&mut hasher); let rand = (hasher.finish() - 2) % 6; @@ -90,7 +93,7 @@ fn setup_scene( "", TextStyle { font_size: 20.0, - color: Color::BLACK, + color: Color::WHITE, ..default() }, ) @@ -219,7 +222,7 @@ fn update_bloom_settings( *text = "Bloom: Off (Toggle: Space)".to_string(); if keycode.just_pressed(KeyCode::Space) { - commands.entity(entity).insert(BloomSettings::default()); + commands.entity(entity).insert(BloomSettings::NATURAL); } } } From dfdf2b9ea4d26daff697b6851806cf342fadd8e3 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 7 Mar 2024 12:22:42 -0800 Subject: [PATCH 2/8] Implement the `AnimationGraph`, allowing for multiple animations to be blended together. (#11989) This is an implementation of RFC #51: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md Note that the implementation strategy is different from the one outlined in that RFC, because two-phase animation has now landed. # Objective Bevy needs animation blending. The RFC for this is [RFC 51]. ## Solution This is an implementation of the RFC. Note that the implementation strategy is different from the one outlined there, because two-phase animation has now landed. This is just a draft to get the conversation started. Currently we're missing a few things: - [x] A fully-fleshed-out mechanism for transitions - [x] A serialization format for `AnimationGraph`s - [x] Examples are broken, other than `animated_fox` - [x] Documentation --- ## Changelog ### Added * The `AnimationPlayer` has been reworked to support blending multiple animations together through an `AnimationGraph`, and as such will no longer function unless a `Handle` has been added to the entity containing the player. See [RFC 51] for more details. * Transition functionality has moved from the `AnimationPlayer` to a new component, `AnimationTransitions`, which works in tandem with the `AnimationGraph`. ## Migration Guide * `AnimationPlayer`s can no longer play animations by themselves and need to be paired with a `Handle`. Code that was using `AnimationPlayer` to play animations will need to create an `AnimationGraph` asset first, add a node for the clip (or clips) you want to play, and then supply the index of that node to the `AnimationPlayer`'s `play` method. * The `AnimationPlayer::play_with_transition()` method has been removed and replaced with the `AnimationTransitions` component. If you were previously using `AnimationPlayer::play_with_transition()`, add all animations that you were playing to the `AnimationGraph`, and create an `AnimationTransitions` component to manage the blending between them. [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md --------- Co-authored-by: Rob Parrett --- Cargo.toml | 11 + assets/animation_graphs/Fox.animgraph.ron | 35 + crates/bevy_animation/Cargo.toml | 9 + crates/bevy_animation/src/graph.rs | 400 +++++++++ crates/bevy_animation/src/lib.rs | 774 +++++++++++------- crates/bevy_animation/src/transition.rs | 132 +++ crates/bevy_asset/src/id.rs | 3 +- crates/bevy_reflect/Cargo.toml | 2 + crates/bevy_reflect/src/impls/petgraph.rs | 15 + crates/bevy_reflect/src/lib.rs | 3 +- examples/3d/irradiance_volumes.rs | 28 +- examples/README.md | 1 + examples/animation/animated_fox.rs | 131 ++- examples/animation/animated_transform.rs | 9 +- examples/animation/animation_graph.rs | 576 +++++++++++++ examples/animation/morph_targets.rs | 12 +- examples/stress_tests/many_foxes.rs | 61 +- .../tools/scene_viewer/animation_plugin.rs | 51 +- 18 files changed, 1865 insertions(+), 388 deletions(-) create mode 100644 assets/animation_graphs/Fox.animgraph.ron create mode 100644 crates/bevy_animation/src/graph.rs create mode 100644 crates/bevy_animation/src/transition.rs create mode 100644 crates/bevy_reflect/src/impls/petgraph.rs create mode 100644 examples/animation/animation_graph.rs diff --git a/Cargo.toml b/Cargo.toml index 6de2dccefc702..4f365f59f2ede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -980,6 +980,17 @@ description = "Plays an animation from a skinned glTF" category = "Animation" wasm = true +[[example]] +name = "animation_graph" +path = "examples/animation/animation_graph.rs" +doc-scrape-examples = true + +[package.metadata.example.animation_graph] +name = "Animation Graph" +description = "Blends multiple animations together with a graph" +category = "Animation" +wasm = true + [[example]] name = "morph_targets" path = "examples/animation/morph_targets.rs" diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron new file mode 100644 index 0000000000000..a1b21f1254172 --- /dev/null +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -0,0 +1,35 @@ +( + graph: ( + nodes: [ + ( + clip: None, + weight: 1.0, + ), + ( + clip: None, + weight: 0.5, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation0")), + weight: 1.0, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation1")), + weight: 1.0, + ), + ( + clip: Some(AssetPath("models/animated/Fox.glb#Animation2")), + weight: 1.0, + ), + ], + node_holes: [], + edge_property: directed, + edges: [ + Some((0, 1, ())), + Some((0, 2, ())), + Some((1, 3, ())), + Some((1, 4, ())), + ], + ), + root: 0, +) \ No newline at end of file diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index e47fefb987848..c6cc5275f5255 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -13,9 +13,12 @@ keywords = ["bevy"] bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" } bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } bevy_math = { path = "../bevy_math", version = "0.14.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [ "bevy", + "petgraph", ] } bevy_render = { path = "../bevy_render", version = "0.14.0-dev" } bevy_time = { path = "../bevy_time", version = "0.14.0-dev" } @@ -25,7 +28,13 @@ bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } # other +fixedbitset = "0.4" +petgraph = { version = "0.6", features = ["serde-1"] } +ron = "0.8" +serde = "1" sha1_smol = { version = "1.0" } +thiserror = "1" +thread_local = "1" uuid = { version = "1.7", features = ["v4"] } [lints] diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs new file mode 100644 index 0000000000000..aba53f1d7adfa --- /dev/null +++ b/crates/bevy_animation/src/graph.rs @@ -0,0 +1,400 @@ +//! The animation graph, which allows animations to be blended together. + +use std::io::{self, Write}; +use std::ops::{Index, IndexMut}; + +use bevy_asset::io::Reader; +use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, AsyncReadExt as _, Handle, LoadContext}; +use bevy_reflect::{Reflect, ReflectSerialize}; +use bevy_utils::BoxedFuture; +use petgraph::graph::{DiGraph, NodeIndex}; +use ron::de::SpannedError; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::AnimationClip; + +/// A graph structure that describes how animation clips are to be blended +/// together. +/// +/// Applications frequently want to be able to play multiple animations at once +/// and to fine-tune the influence that animations have on a skinned mesh. Bevy +/// uses an *animation graph* to store this information. Animation graphs are a +/// directed acyclic graph (DAG) that describes how animations are to be +/// weighted and combined together. Every frame, Bevy evaluates the graph from +/// the root and blends the animations together in a bottom-up fashion to +/// produce the final pose. +/// +/// There are two types of nodes: *blend nodes* and *clip nodes*, both of which +/// can have an associated weight. Blend nodes have no associated animation clip +/// and simply affect the weights of all their descendant nodes. Clip nodes +/// specify an animation clip to play. When a graph is created, it starts with +/// only a single blend node, the root node. +/// +/// For example, consider the following graph: +/// +/// ```text +/// ┌────────────┐ +/// │ │ +/// │ Idle ├─────────────────────┐ +/// │ │ │ +/// └────────────┘ │ +/// │ +/// ┌────────────┐ │ ┌────────────┐ +/// │ │ │ │ │ +/// │ Run ├──┐ ├──┤ Root │ +/// │ │ │ ┌────────────┐ │ │ │ +/// └────────────┘ │ │ Blend │ │ └────────────┘ +/// ├──┤ ├──┘ +/// ┌────────────┐ │ │ 0.5 │ +/// │ │ │ └────────────┘ +/// │ Walk ├──┘ +/// │ │ +/// └────────────┘ +/// ``` +/// +/// In this case, assuming that Idle, Run, and Walk are all playing with weight +/// 1.0, the Run and Walk animations will be equally blended together, then +/// their weights will be halved and finally blended with the Idle animation. +/// Thus the weight of Run and Walk are effectively half of the weight of Idle. +/// +/// Animation graphs are assets and can be serialized to and loaded from [RON] +/// files. Canonically, such files have an `.animgraph.ron` extension. +/// +/// The animation graph implements [RFC 51]. See that document for more +/// information. +/// +/// [RON]: https://github.com/ron-rs/ron +/// +/// [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md +#[derive(Asset, Reflect, Clone, Debug, Serialize)] +#[reflect(Serialize, Debug)] +#[serde(into = "SerializedAnimationGraph")] +pub struct AnimationGraph { + /// The `petgraph` data structure that defines the animation graph. + pub graph: AnimationDiGraph, + /// The index of the root node in the animation graph. + pub root: NodeIndex, +} + +/// A type alias for the `petgraph` data structure that defines the animation +/// graph. +pub type AnimationDiGraph = DiGraph; + +/// The index of either an animation or blend node in the animation graph. +/// +/// These indices are the way that [`crate::AnimationPlayer`]s identify +/// particular animations. +pub type AnimationNodeIndex = NodeIndex; + +/// An individual node within an animation graph. +/// +/// If `clip` is present, this is a *clip node*. Otherwise, it's a *blend node*. +/// Both clip and blend nodes can have weights, and those weights are propagated +/// down to descendants. +#[derive(Clone, Reflect, Debug)] +pub struct AnimationGraphNode { + /// The animation clip associated with this node, if any. + /// + /// If the clip is present, this node is an *animation clip node*. + /// Otherwise, this node is a *blend node*. + pub clip: Option>, + + /// The weight of this node. + /// + /// Weights are propagated down to descendants. Thus if an animation clip + /// has weight 0.3 and its parent blend node has weight 0.6, the computed + /// weight of the animation clip is 0.18. + pub weight: f32, +} + +/// An [`AssetLoader`] that can load [`AnimationGraph`]s as assets. +/// +/// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain +/// `.animgraph` is supported as well. +#[derive(Default)] +pub struct AnimationGraphAssetLoader; + +/// Various errors that can occur when serializing or deserializing animation +/// graphs to and from RON, respectively. +#[derive(Error, Debug)] +pub enum AnimationGraphLoadError { + /// An I/O error occurred. + #[error("I/O")] + Io(#[from] io::Error), + /// An error occurred in RON serialization or deserialization. + #[error("RON serialization")] + Ron(#[from] ron::Error), + /// An error occurred in RON deserialization, and the location of the error + /// is supplied. + #[error("RON serialization")] + SpannedRon(#[from] SpannedError), +} + +/// A version of [`AnimationGraph`] suitable for serializing as an asset. +/// +/// Animation nodes can refer to external animation clips, and the [`AssetId`] +/// is typically not sufficient to identify the clips, since the +/// [`bevy_asset::AssetServer`] assigns IDs in unpredictable ways. That fact +/// motivates this type, which replaces the `Handle` with an +/// asset path. Loading an animation graph via the [`bevy_asset::AssetServer`] +/// actually loads a serialized instance of this type, as does serializing an +/// [`AnimationGraph`] through `serde`. +#[derive(Serialize, Deserialize)] +pub struct SerializedAnimationGraph { + /// Corresponds to the `graph` field on [`AnimationGraph`]. + pub graph: DiGraph, + /// Corresponds to the `root` field on [`AnimationGraph`]. + pub root: NodeIndex, +} + +/// A version of [`AnimationGraphNode`] suitable for serializing as an asset. +/// +/// See the comments in [`SerializedAnimationGraph`] for more information. +#[derive(Serialize, Deserialize)] +pub struct SerializedAnimationGraphNode { + /// Corresponds to the `clip` field on [`AnimationGraphNode`]. + pub clip: Option, + /// Corresponds to the `weight` field on [`AnimationGraphNode`]. + pub weight: f32, +} + +/// A version of `Handle` suitable for serializing as an asset. +/// +/// This replaces any handle that has a path with an [`AssetPath`]. Failing +/// that, the asset ID is serialized directly. +#[derive(Serialize, Deserialize)] +pub enum SerializedAnimationClip { + /// Records an asset path. + AssetPath(AssetPath<'static>), + /// The fallback that records an asset ID. + /// + /// Because asset IDs can change, this should not be relied upon. Prefer to + /// use asset paths where possible. + AssetId(AssetId), +} + +impl AnimationGraph { + /// Creates a new animation graph with a root node and no other nodes. + pub fn new() -> Self { + let mut graph = DiGraph::default(); + let root = graph.add_node(AnimationGraphNode::default()); + Self { graph, root } + } + + /// A convenience function for creating an [`AnimationGraph`] from a single + /// [`AnimationClip`]. + /// + /// The clip will be a direct child of the root with weight 1.0. Both the + /// graph and the index of the added node are returned as a tuple. + pub fn from_clip(clip: Handle) -> (Self, AnimationNodeIndex) { + let mut graph = Self::new(); + let node_index = graph.add_clip(clip, 1.0, graph.root); + (graph, node_index) + } + + /// Adds an [`AnimationClip`] to the animation graph with the given weight + /// and returns its index. + /// + /// The animation clip will be the child of the given parent. + pub fn add_clip( + &mut self, + clip: Handle, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: Some(clip), + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// A convenience method to add multiple [`AnimationClip`]s to the animation + /// graph. + /// + /// All of the animation clips will have the same weight and will be + /// parented to the same node. + /// + /// Returns the indices of the new nodes. + pub fn add_clips<'a, I>( + &'a mut self, + clips: I, + weight: f32, + parent: AnimationNodeIndex, + ) -> impl Iterator + 'a + where + I: IntoIterator>, + ::IntoIter: 'a, + { + clips + .into_iter() + .map(move |clip| self.add_clip(clip, weight, parent)) + } + + /// Adds a blend node to the animation graph with the given weight and + /// returns its index. + /// + /// The blend node will be placed under the supplied `parent` node. During + /// animation evaluation, the descendants of this blend node will have their + /// weights multiplied by the weight of the blend. + pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex { + let node_index = self + .graph + .add_node(AnimationGraphNode { clip: None, weight }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an edge from the edge `from` to `to`, making `to` a child of + /// `from`. + /// + /// The behavior is unspecified if adding this produces a cycle in the + /// graph. + pub fn add_edge(&mut self, from: NodeIndex, to: NodeIndex) { + self.graph.add_edge(from, to, ()); + } + + /// Removes an edge between `from` and `to` if it exists. + /// + /// Returns true if the edge was successfully removed or false if no such + /// edge existed. + pub fn remove_edge(&mut self, from: NodeIndex, to: NodeIndex) -> bool { + self.graph + .find_edge(from, to) + .map(|edge| self.graph.remove_edge(edge)) + .is_some() + } + + /// Returns the [`AnimationGraphNode`] associated with the given index. + /// + /// If no node with the given index exists, returns `None`. + pub fn get(&self, animation: AnimationNodeIndex) -> Option<&AnimationGraphNode> { + self.graph.node_weight(animation) + } + + /// Returns a mutable reference to the [`AnimationGraphNode`] associated + /// with the given index. + /// + /// If no node with the given index exists, returns `None`. + pub fn get_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut AnimationGraphNode> { + self.graph.node_weight_mut(animation) + } + + /// Returns an iterator over the [`AnimationGraphNode`]s in this graph. + pub fn nodes(&self) -> impl Iterator { + self.graph.node_indices() + } + + /// Serializes the animation graph to the given [`Write`]r in RON format. + /// + /// If writing to a file, it can later be loaded with the + /// [`AnimationGraphAssetLoader`] to reconstruct the graph. + pub fn save(&self, writer: &mut W) -> Result<(), AnimationGraphLoadError> + where + W: Write, + { + let mut ron_serializer = ron::ser::Serializer::new(writer, None)?; + Ok(self.serialize(&mut ron_serializer)?) + } +} + +impl Index for AnimationGraph { + type Output = AnimationGraphNode; + + fn index(&self, index: AnimationNodeIndex) -> &Self::Output { + &self.graph[index] + } +} + +impl IndexMut for AnimationGraph { + fn index_mut(&mut self, index: AnimationNodeIndex) -> &mut Self::Output { + &mut self.graph[index] + } +} + +impl Default for AnimationGraphNode { + fn default() -> Self { + Self { + clip: None, + weight: 1.0, + } + } +} + +impl Default for AnimationGraph { + fn default() -> Self { + Self::new() + } +} + +impl AssetLoader for AnimationGraphAssetLoader { + type Asset = AnimationGraph; + + type Settings = (); + + type Error = AnimationGraphLoadError; + + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _: &'a Self::Settings, + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize a `SerializedAnimationGraph` directly, so that we can + // get the list of the animation clips it refers to and load them. + let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?; + let serialized_animation_graph = + SerializedAnimationGraph::deserialize(&mut deserializer) + .map_err(|err| deserializer.span_error(err))?; + + // Load all `AssetPath`s to convert from a + // `SerializedAnimationGraph` to a real `AnimationGraph`. + Ok(AnimationGraph { + graph: serialized_animation_graph.graph.map( + |_, serialized_node| AnimationGraphNode { + clip: serialized_node.clip.as_ref().map(|clip| match clip { + SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), + SerializedAnimationClip::AssetPath(asset_path) => { + load_context.load(asset_path) + } + }), + weight: serialized_node.weight, + }, + |_, _| (), + ), + root: serialized_animation_graph.root, + }) + }) + } + + fn extensions(&self) -> &[&str] { + &["animgraph", "animgraph.ron"] + } +} + +impl From for SerializedAnimationGraph { + fn from(animation_graph: AnimationGraph) -> Self { + // If any of the animation clips have paths, then serialize them as + // `SerializedAnimationClip::AssetPath` so that the + // `AnimationGraphAssetLoader` can load them. + Self { + graph: animation_graph.graph.map( + |_, node| SerializedAnimationGraphNode { + weight: node.weight, + clip: node.clip.as_ref().map(|clip| match clip.path() { + Some(path) => SerializedAnimationClip::AssetPath(path.clone()), + None => SerializedAnimationClip::AssetId(clip.id()), + }), + }, + |_, _| (), + ), + root: animation_graph.root, + } + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 08f10a167dfd2..d3dbed8406193 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,12 +1,15 @@ //! Animation for the game engine Bevy mod animatable; +mod graph; +mod transition; mod util; +use std::cell::RefCell; +use std::collections::BTreeMap; use std::hash::{Hash, Hasher}; use std::iter; use std::ops::{Add, Mul}; -use std::time::Duration; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -20,19 +23,30 @@ use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; use bevy_utils::hashbrown::HashMap; -use bevy_utils::{tracing::error, NoOpHash}; +use bevy_utils::{ + tracing::{error, trace}, + NoOpHash, +}; +use fixedbitset::FixedBitSet; +use graph::{AnimationGraph, AnimationNodeIndex}; +use petgraph::graph::NodeIndex; +use petgraph::Direction; +use prelude::{AnimationGraphAssetLoader, AnimationTransitions}; use sha1_smol::Sha1; +use thread_local::ThreadLocal; use uuid::Uuid; #[allow(missing_docs)] pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, Interpolation, Keyframes, - VariableCurve, + animatable::*, graph::*, transition::*, AnimationClip, AnimationPlayer, AnimationPlugin, + Interpolation, Keyframes, VariableCurve, }; } +use crate::transition::{advance_transitions, expire_completed_transitions}; + /// 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) @@ -279,8 +293,17 @@ pub enum RepeatAnimation { Forever, } +/// An animation that an [`AnimationPlayer`] is currently either playing or was +/// playing, but is presently paused. +/// +/// An stopped animation is considered no longer active. #[derive(Debug, Reflect)] -struct PlayingAnimation { +pub struct ActiveAnimation { + /// The factor by which the weight from the [`AnimationGraph`] is multiplied. + weight: f32, + /// The actual weight of this animation this frame, taking the + /// [`AnimationGraph`] into account. + computed_weight: f32, repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -291,26 +314,28 @@ struct PlayingAnimation { /// /// Note: This will always be in the range [0.0, animation clip duration] seek_time: f32, - animation_clip: Handle, /// Number of times the animation has completed. /// If the animation is playing in reverse, this increments when the animation passes the start. completions: u32, + paused: bool, } -impl Default for PlayingAnimation { +impl Default for ActiveAnimation { fn default() -> Self { Self { + weight: 1.0, + computed_weight: 1.0, repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, seek_time: 0.0, - animation_clip: Default::default(), completions: 0, + paused: false, } } } -impl PlayingAnimation { +impl ActiveAnimation { /// Check if the animation has finished, based on its repetition behavior and the number of times it has repeated. /// /// Note: An animation with `RepeatAnimation::Forever` will never finish. @@ -353,38 +378,112 @@ impl PlayingAnimation { } /// Reset back to the initial state as if no time has elapsed. - fn replay(&mut self) { + pub fn replay(&mut self) { self.completions = 0; self.elapsed = 0.0; self.seek_time = 0.0; } -} -/// 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, + /// Returns the current weight of this animation. + pub fn weight(&self) -> f32 { + self.weight + } + + /// Sets the weight of this animation. + pub fn set_weight(&mut self, weight: f32) { + self.weight = weight; + } + + /// Pause the animation. + pub fn pause(&mut self) -> &mut Self { + self.paused = true; + self + } + + /// Unpause the animation. + pub fn resume(&mut self) -> &mut Self { + self.paused = false; + self + } + + /// Returns true if this animation is currently paused. + /// + /// Note that paused animations are still [`ActiveAnimation`]s. + #[inline] + pub fn is_paused(&self) -> bool { + self.paused + } + + /// Sets the repeat mode for this playing animation. + pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { + self.repeat = repeat; + self + } + + /// Marks this animation as repeating forever. + pub fn repeat(&mut self) -> &mut Self { + self.set_repeat(RepeatAnimation::Forever) + } + + /// Returns the repeat mode assigned to this active animation. + pub fn repeat_mode(&self) -> RepeatAnimation { + self.repeat + } + + /// Returns the number of times this animation has completed. + pub fn completions(&self) -> u32 { + self.completions + } + + /// Returns true if the animation is playing in reverse. + pub fn is_playback_reversed(&self) -> bool { + self.speed < 0.0 + } + + /// Returns the speed of the animation playback. + pub fn speed(&self) -> f32 { + self.speed + } + + /// Sets the speed of the animation playback. + pub fn set_speed(&mut self, speed: f32) -> &mut Self { + self.speed = speed; + self + } + + /// Returns the amount of time the animation has been playing. + pub fn elapsed(&self) -> f32 { + self.elapsed + } + + /// Returns the seek time of the animation. + /// + /// This is nonnegative and no more than the clip duration. + pub fn seek_time(&self) -> f32 { + self.seek_time + } + + /// Seeks to a specific time in the animation. + pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { + self.seek_time = seek_time; + self + } + + /// Seeks to the beginning of the animation. + pub fn rewind(&mut self) -> &mut Self { + self.seek_time = 0.0; + self + } } /// 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, + /// We use a `BTreeMap` instead of a `HashMap` here to ensure a consistent + /// ordering when applying the animations. + active_animations: BTreeMap, + blend_weights: HashMap, } /// The components that we might need to read or write during animation of each @@ -397,157 +496,160 @@ struct AnimationTargetContext<'a> { morph_weights: Option>, } -impl AnimationPlayer { - /// 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(&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 - } +/// Information needed during the traversal of the animation graph in +/// [`advance_animations`]. +#[derive(Default)] +pub struct AnimationGraphEvaluator { + /// The stack used for the depth-first search of the graph. + dfs_stack: Vec, + /// The list of visited nodes during the depth-first traversal. + dfs_visited: FixedBitSet, + /// Accumulated weights for each node. + weights: Vec, +} - /// 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: Duration, - ) -> &mut Self { - let mut animation = PlayingAnimation { - animation_clip: handle, - ..Default::default() - }; - std::mem::swap(&mut animation, &mut self.animation); - - // 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, - }); +thread_local! { + /// A cached per-thread copy of the graph evaluator. + /// + /// Caching the evaluator lets us save allocation traffic from frame to + /// frame. + static ANIMATION_GRAPH_EVALUATOR: RefCell = + RefCell::new(AnimationGraphEvaluator::default()); +} - self +impl AnimationPlayer { + /// Start playing an animation, restarting it if necessary. + pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { + self.active_animations.entry(animation).or_default() } - /// 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.is_playing_clip(&handle) || self.is_paused() { - self.start(handle); - } - self + /// Start playing an animation, unless the requested animation is already playing. + pub fn play(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { + let playing_animation = self.active_animations.entry(animation).or_default(); + playing_animation.weight = 1.0; + playing_animation } - /// 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: Duration, - ) -> &mut Self { - if !self.is_playing_clip(&handle) || self.is_paused() { - self.start_with_transition(handle, transition_duration); - } + /// Stops playing the given animation, removing it from the list of playing + /// animations. + pub fn stop(&mut self, animation: AnimationNodeIndex) -> &mut Self { + self.active_animations.remove(&animation); self } - /// Handle to the animation clip being played. - pub fn animation_clip(&self) -> &Handle { - &self.animation.animation_clip - } - - /// Check if the given animation clip is being played. - pub fn is_playing_clip(&self, handle: &Handle) -> bool { - self.animation_clip() == handle - } - - /// Check if the playing animation has finished, according to the repetition behavior. - pub fn is_finished(&self) -> bool { - self.animation.is_finished() - } - - /// Sets repeat to [`RepeatAnimation::Forever`]. - /// - /// See also [`Self::set_repeat`]. - pub fn repeat(&mut self) -> &mut Self { - self.animation.repeat = RepeatAnimation::Forever; + /// Stops all currently-playing animations. + pub fn stop_all(&mut self) -> &mut Self { + self.active_animations.clear(); self } - /// Set the repetition behaviour of the animation. - pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { - self.animation.repeat = repeat; - self + /// Iterates through all animations that this [`AnimationPlayer`] is + /// currently playing. + pub fn playing_animations( + &self, + ) -> impl Iterator { + self.active_animations.iter() } - /// Repetition behavior of the animation. - pub fn repeat_mode(&self) -> RepeatAnimation { - self.animation.repeat + /// Iterates through all animations that this [`AnimationPlayer`] is + /// currently playing, mutably. + pub fn playing_animations_mut( + &mut self, + ) -> impl Iterator { + self.active_animations.iter_mut() } - /// Number of times the animation has completed. - pub fn completions(&self) -> u32 { - self.animation.completions + /// Check if the given animation node is being played. + pub fn is_playing_animation(&self, animation: AnimationNodeIndex) -> bool { + self.active_animations.contains_key(&animation) } - /// Check if the animation is playing in reverse. - pub fn is_playback_reversed(&self) -> bool { - self.animation.speed < 0.0 + /// Check if all playing animations have finished, according to the repetition behavior. + pub fn all_finished(&self) -> bool { + self.active_animations + .values() + .all(|playing_animation| playing_animation.is_finished()) } - /// Pause the animation - pub fn pause(&mut self) { - self.paused = true; + /// Check if all playing animations are paused. + #[doc(alias = "is_paused")] + pub fn all_paused(&self) -> bool { + self.active_animations + .values() + .all(|playing_animation| playing_animation.is_paused()) } - /// Unpause the animation - pub fn resume(&mut self) { - self.paused = false; + /// Resume all playing animations. + #[doc(alias = "pause")] + pub fn pause_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.pause(); + } + self } - /// Is the animation paused - pub fn is_paused(&self) -> bool { - self.paused + /// Resume all active animations. + #[doc(alias = "resume")] + pub fn resume_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.resume(); + } + self } - /// Speed of the animation playback - pub fn speed(&self) -> f32 { - self.animation.speed + /// Rewinds all active animations. + #[doc(alias = "rewind")] + pub fn rewind_all(&mut self) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + playing_animation.rewind(); + } + self } - /// Set the speed of the animation playback - pub fn set_speed(&mut self, speed: f32) -> &mut Self { - self.animation.speed = speed; + /// Multiplies the speed of all active animations by the given factor. + #[doc(alias = "set_speed")] + pub fn adjust_speeds(&mut self, factor: f32) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + let new_speed = playing_animation.speed() * factor; + playing_animation.set_speed(new_speed); + } self } - /// Time elapsed playing the animation - pub fn elapsed(&self) -> f32 { - self.animation.elapsed + /// Seeks all active animations forward or backward by the same amount. + /// + /// To seek forward, pass a positive value; to seek negative, pass a + /// negative value. Values below 0.0 or beyond the end of the animation clip + /// are clamped appropriately. + #[doc(alias = "seek_to")] + pub fn seek_all_by(&mut self, amount: f32) -> &mut Self { + for (_, playing_animation) in self.playing_animations_mut() { + let new_time = playing_animation.seek_time(); + playing_animation.seek_to(new_time + amount); + } + self } - /// Seek time inside of the animation. Always within the range [0.0, clip duration]. - pub fn seek_time(&self) -> f32 { - self.animation.seek_time + /// Returns the [`ActiveAnimation`] associated with the given animation + /// node if it's currently playing. + /// + /// If the animation isn't currently active, returns `None`. + pub fn animation(&self, animation: AnimationNodeIndex) -> Option<&ActiveAnimation> { + self.active_animations.get(&animation) } - /// Seek to a specific time in the animation. - pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { - self.animation.seek_time = seek_time; - self + /// Returns a mutable reference to the [`ActiveAnimation`] associated with + /// the given animation node if it's currently active. + /// + /// If the animation isn't currently active, returns `None`. + pub fn animation_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut ActiveAnimation> { + self.active_animations.get_mut(&animation) } - /// Reset the animation to its initial state, as if no time has elapsed. - pub fn replay(&mut self) { - self.animation.replay(); + /// Returns true if the animation is currently playing or paused, or false + /// if the animation is stopped. + pub fn animation_is_playing(&self, animation: AnimationNodeIndex) -> bool { + self.active_animations.contains_key(&animation) } } @@ -555,46 +657,87 @@ impl AnimationPlayer { pub fn advance_animations( time: Res