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:
After:
---------
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