mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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<AnimationGraph>` 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<AnimationGraph>`. 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 <robparrett@gmail.com>
This commit is contained in:
parent
713d91b721
commit
dfdf2b9ea4
18 changed files with 2045 additions and 568 deletions
11
Cargo.toml
11
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"
|
||||
|
|
35
assets/animation_graphs/Fox.animgraph.ron
Normal file
35
assets/animation_graphs/Fox.animgraph.ron
Normal file
|
@ -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,
|
||||
)
|
|
@ -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]
|
||||
|
|
400
crates/bevy_animation/src/graph.rs
Normal file
400
crates/bevy_animation/src/graph.rs
Normal file
|
@ -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<AnimationGraphNode, (), u32>;
|
||||
|
||||
/// 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<u32>;
|
||||
|
||||
/// 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<Handle<AnimationClip>>,
|
||||
|
||||
/// 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<AnimationClip>` 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<SerializedAnimationGraphNode, (), u32>,
|
||||
/// 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<SerializedAnimationClip>,
|
||||
/// Corresponds to the `weight` field on [`AnimationGraphNode`].
|
||||
pub weight: f32,
|
||||
}
|
||||
|
||||
/// A version of `Handle<AnimationClip>` 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<AnimationClip>),
|
||||
}
|
||||
|
||||
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<AnimationClip>) -> (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<AnimationClip>,
|
||||
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<Item = AnimationNodeIndex> + 'a
|
||||
where
|
||||
I: IntoIterator<Item = Handle<AnimationClip>>,
|
||||
<I as std::iter::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<Item = AnimationNodeIndex> {
|
||||
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<W>(&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<AnimationNodeIndex> for AnimationGraph {
|
||||
type Output = AnimationGraphNode;
|
||||
|
||||
fn index(&self, index: AnimationNodeIndex) -> &Self::Output {
|
||||
&self.graph[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<AnimationNodeIndex> 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<Self::Asset, Self::Error>> {
|
||||
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<AnimationGraph> 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,
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
132
crates/bevy_animation/src/transition.rs
Normal file
132
crates/bevy_animation/src/transition.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
//! Animation transitions.
|
||||
//!
|
||||
//! Please note that this is an unstable temporary API. It may be replaced by a
|
||||
//! state machine in the future.
|
||||
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
system::{Query, Res},
|
||||
};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_time::Time;
|
||||
use bevy_utils::Duration;
|
||||
|
||||
use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer};
|
||||
|
||||
/// Manages fade-out of animation blend factors, allowing for smooth transitions
|
||||
/// between animations.
|
||||
///
|
||||
/// To use this component, place it on the same entity as the
|
||||
/// [`AnimationPlayer`] and [`bevy_asset::Handle<AnimationGraph>`]. It'll take
|
||||
/// responsibility for adjusting the weight on the [`ActiveAnimation`] in order
|
||||
/// to fade out animations smoothly.
|
||||
///
|
||||
/// 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)]
|
||||
pub struct AnimationTransitions {
|
||||
main_animation: Option<AnimationNodeIndex>,
|
||||
transitions: Vec<AnimationTransition>,
|
||||
}
|
||||
|
||||
/// An animation that is being faded out as part of a transition
|
||||
#[derive(Debug, Reflect)]
|
||||
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()
|
||||
}
|
||||
|
||||
/// Plays a new animation on the given [`AnimationPlayer`], fading out any
|
||||
/// existing animations that were already playing over the
|
||||
/// `transition_duration`.
|
||||
///
|
||||
/// Pass [`Duration::ZERO`] to instantly switch to a new animation, avoiding
|
||||
/// any transition.
|
||||
pub fn play<'p>(
|
||||
&mut self,
|
||||
player: &'p mut AnimationPlayer,
|
||||
new_animation: AnimationNodeIndex,
|
||||
transition_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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.main_animation = Some(new_animation);
|
||||
player.start(new_animation)
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that alters the weight of currently-playing transitions based on
|
||||
/// the current time and decline amount.
|
||||
pub fn advance_transitions(
|
||||
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.
|
||||
let mut remaining_weight = 1.0;
|
||||
for (mut animation_transitions, mut player) in query.iter_mut() {
|
||||
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_seconds())
|
||||
.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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that removed 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);
|
||||
}
|
||||
!expire
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{Asset, AssetIndex};
|
||||
use bevy_reflect::Reflect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::{
|
||||
|
@ -16,7 +17,7 @@ use thiserror::Error;
|
|||
/// For an identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`).
|
||||
///
|
||||
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
|
||||
#[derive(Reflect)]
|
||||
#[derive(Reflect, Serialize, Deserialize)]
|
||||
pub enum AssetId<A: Asset> {
|
||||
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
|
||||
/// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are
|
||||
|
|
|
@ -15,6 +15,7 @@ default = ["smallvec"]
|
|||
bevy = ["smallvec", "bevy_math", "smol_str"]
|
||||
glam = ["dep:glam"]
|
||||
bevy_math = ["glam", "dep:bevy_math"]
|
||||
petgraph = ["dep:petgraph"]
|
||||
smallvec = ["dep:smallvec"]
|
||||
uuid = ["dep:uuid"]
|
||||
# When enabled, allows documentation comments to be accessed via reflection
|
||||
|
@ -37,6 +38,7 @@ serde = "1"
|
|||
smallvec = { version = "1.11", optional = true }
|
||||
|
||||
glam = { version = "0.25", features = ["serde"], optional = true }
|
||||
petgraph = { version = "0.6", features = ["serde-1"], optional = true }
|
||||
smol_str = { version = "0.2.0", optional = true }
|
||||
uuid = { version = "1.0", optional = true, features = ["v4", "serde"] }
|
||||
|
||||
|
|
15
crates/bevy_reflect/src/impls/petgraph.rs
Normal file
15
crates/bevy_reflect/src/impls/petgraph.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::{
|
||||
self as bevy_reflect, impl_reflect_value, prelude::ReflectDefault, ReflectDeserialize,
|
||||
ReflectSerialize,
|
||||
};
|
||||
|
||||
impl_reflect_value!(::petgraph::graph::NodeIndex(
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize
|
||||
));
|
||||
impl_reflect_value!(::petgraph::graph::DiGraph<
|
||||
N: ::std::clone::Clone,
|
||||
E: ::std::clone::Clone,
|
||||
Ix: ::petgraph::graph::IndexType
|
||||
>());
|
|
@ -493,7 +493,8 @@ mod impls {
|
|||
mod primitives3d;
|
||||
mod rect;
|
||||
}
|
||||
|
||||
#[cfg(feature = "petgraph")]
|
||||
mod petgraph;
|
||||
#[cfg(feature = "smallvec")]
|
||||
mod smallvec;
|
||||
#[cfg(feature = "smol_str")]
|
||||
|
|
|
@ -97,8 +97,11 @@ struct ExampleAssets {
|
|||
// The glTF scene containing the animated fox.
|
||||
fox: Handle<Scene>,
|
||||
|
||||
// The animation that the fox will play.
|
||||
fox_animation: Handle<AnimationClip>,
|
||||
// The graph containing the animation that the fox will play.
|
||||
fox_animation_graph: Handle<AnimationGraph>,
|
||||
|
||||
// The node within the animation graph containing the animation.
|
||||
fox_animation_node: AnimationNodeIndex,
|
||||
|
||||
// The voxel cube mesh.
|
||||
voxel_cube: Handle<Mesh>,
|
||||
|
@ -513,6 +516,10 @@ fn handle_mouse_clicks(
|
|||
|
||||
impl FromWorld for ExampleAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let fox_animation = world.load_asset("models/animated/Fox.glb#Animation1");
|
||||
let (fox_animation_graph, fox_animation_node) =
|
||||
AnimationGraph::from_clip(fox_animation.clone());
|
||||
|
||||
ExampleAssets {
|
||||
main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)),
|
||||
fox: world.load_asset("models/animated/Fox.glb#Scene0"),
|
||||
|
@ -520,7 +527,8 @@ impl FromWorld for ExampleAssets {
|
|||
main_scene: world
|
||||
.load_asset("models/IrradianceVolumeExample/IrradianceVolumeExample.glb#Scene0"),
|
||||
irradiance_volume: world.load_asset("irradiance_volumes/Example.vxgi.ktx2"),
|
||||
fox_animation: world.load_asset("models/animated/Fox.glb#Animation1"),
|
||||
fox_animation_graph: world.add_asset(fox_animation_graph),
|
||||
fox_animation_node,
|
||||
voxel_cube: world.add_asset(Cuboid::default()),
|
||||
// Just use a specular map for the skybox since it's not too blurry.
|
||||
// In reality you wouldn't do this--you'd use a real skybox texture--but
|
||||
|
@ -531,10 +539,16 @@ impl FromWorld for ExampleAssets {
|
|||
}
|
||||
|
||||
// Plays the animation on the fox.
|
||||
fn play_animations(assets: Res<ExampleAssets>, mut players: Query<&mut AnimationPlayer>) {
|
||||
for mut player in players.iter_mut() {
|
||||
// This will safely do nothing if the animation is already playing.
|
||||
player.play(assets.fox_animation.clone()).repeat();
|
||||
fn play_animations(
|
||||
mut commands: Commands,
|
||||
assets: Res<ExampleAssets>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Without<Handle<AnimationGraph>>>,
|
||||
) {
|
||||
for (entity, mut player) in players.iter_mut() {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(assets.fox_animation_graph.clone());
|
||||
player.play(assets.fox_animation_node).repeat();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ Example | Description
|
|||
--- | ---
|
||||
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
|
||||
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
|
||||
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
|
||||
[Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve
|
||||
[Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code
|
||||
[Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
use std::f32::consts::PI;
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::{animation::RepeatAnimation, pbr::CascadeShadowConfigBuilder, prelude::*};
|
||||
use bevy::{
|
||||
animation::{animate_targets, RepeatAnimation},
|
||||
pbr::CascadeShadowConfigBuilder,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
|
@ -13,28 +17,47 @@ fn main() {
|
|||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
(setup_scene_once_loaded, keyboard_animation_control),
|
||||
)
|
||||
.add_systems(Update, setup_scene_once_loaded.before(animate_targets))
|
||||
.add_systems(Update, keyboard_animation_control)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Animations(Vec<Handle<AnimationClip>>);
|
||||
struct Animations {
|
||||
animations: Vec<AnimationNodeIndex>,
|
||||
#[allow(dead_code)]
|
||||
graph: Handle<AnimationGraph>,
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
// Build the animation graph
|
||||
let mut graph = AnimationGraph::new();
|
||||
let animations = graph
|
||||
.add_clips(
|
||||
[
|
||||
"models/animated/Fox.glb#Animation2",
|
||||
"models/animated/Fox.glb#Animation1",
|
||||
"models/animated/Fox.glb#Animation0",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|path| asset_server.load(path)),
|
||||
1.0,
|
||||
graph.root,
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Insert a resource with the current scene information
|
||||
commands.insert_resource(Animations(vec![
|
||||
asset_server.load("models/animated/Fox.glb#Animation2"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
||||
]));
|
||||
let graph = graphs.add(graph);
|
||||
commands.insert_resource(Animations {
|
||||
animations,
|
||||
graph: graph.clone(),
|
||||
});
|
||||
|
||||
// Camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
|
@ -83,76 +106,108 @@ fn setup(
|
|||
|
||||
// Once the scene is loaded, start the animation
|
||||
fn setup_scene_once_loaded(
|
||||
mut commands: Commands,
|
||||
animations: Res<Animations>,
|
||||
mut players: Query<&mut AnimationPlayer, Added<AnimationPlayer>>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
) {
|
||||
for mut player in &mut players {
|
||||
player.play(animations.0[0].clone_weak()).repeat();
|
||||
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`.
|
||||
transitions
|
||||
.play(&mut player, animations.animations[0], Duration::ZERO)
|
||||
.repeat();
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(animations.graph.clone())
|
||||
.insert(transitions);
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_animation_control(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut animation_players: Query<&mut AnimationPlayer>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
animations: Res<Animations>,
|
||||
mut current_animation: Local<usize>,
|
||||
) {
|
||||
for mut player in &mut animation_players {
|
||||
for (mut player, mut transitions) in &mut animation_players {
|
||||
let Some((&playing_animation_index, _)) = player.playing_animations().next() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
if player.is_paused() {
|
||||
player.resume();
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
if playing_animation.is_paused() {
|
||||
playing_animation.resume();
|
||||
} else {
|
||||
player.pause();
|
||||
playing_animation.pause();
|
||||
}
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 1.2);
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 1.2);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 0.8);
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 0.8);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||
let elapsed = player.seek_time();
|
||||
player.seek_to(elapsed - 0.1);
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed - 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||
let elapsed = player.seek_time();
|
||||
player.seek_to(elapsed + 0.1);
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed + 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
*current_animation = (*current_animation + 1) % animations.0.len();
|
||||
player
|
||||
.play_with_transition(
|
||||
animations.0[*current_animation].clone_weak(),
|
||||
*current_animation = (*current_animation + 1) % animations.animations.len();
|
||||
|
||||
transitions
|
||||
.play(
|
||||
&mut player,
|
||||
animations.animations[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit1) {
|
||||
player.set_repeat(RepeatAnimation::Count(1));
|
||||
player.replay();
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(1))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit3) {
|
||||
player.set_repeat(RepeatAnimation::Count(3));
|
||||
player.replay();
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(3))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit5) {
|
||||
player.set_repeat(RepeatAnimation::Count(5));
|
||||
player.replay();
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(5))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::KeyL) {
|
||||
player.set_repeat(RepeatAnimation::Forever);
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation.set_repeat(RepeatAnimation::Forever);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ fn setup(
|
|||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut animations: ResMut<Assets<AnimationClip>>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
// Camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
|
@ -125,9 +126,12 @@ fn setup(
|
|||
},
|
||||
);
|
||||
|
||||
// Create the animation graph
|
||||
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
|
||||
|
||||
// Create the animation player, and set it to repeat
|
||||
let mut player = AnimationPlayer::default();
|
||||
player.play(animations.add(animation)).repeat();
|
||||
player.play(animation_index).repeat();
|
||||
|
||||
// Create the scene that will be animated
|
||||
// First entity is the planet
|
||||
|
@ -138,8 +142,9 @@ fn setup(
|
|||
material: materials.add(Color::srgb(0.8, 0.7, 0.6)),
|
||||
..default()
|
||||
},
|
||||
// Add the animation player
|
||||
// Add the animation graph and player
|
||||
planet,
|
||||
graphs.add(graph),
|
||||
player,
|
||||
))
|
||||
.id();
|
||||
|
|
576
examples/animation/animation_graph.rs
Normal file
576
examples/animation/animation_graph.rs
Normal file
|
@ -0,0 +1,576 @@
|
|||
//! Demonstrates animation blending with animation graphs.
|
||||
//!
|
||||
//! The animation graph is shown on screen. You can change the weights of the
|
||||
//! playing animations by clicking and dragging left or right within the nodes.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
use bevy::{
|
||||
animation::animate_targets,
|
||||
color::palettes::{
|
||||
basic::WHITE,
|
||||
css::{ANTIQUE_WHITE, DARK_GREEN},
|
||||
},
|
||||
prelude::*,
|
||||
ui::RelativeCursorPosition,
|
||||
};
|
||||
|
||||
use argh::FromArgs;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy::asset::io::file::FileAssetReader;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy::tasks::IoTaskPool;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use ron::ser::PrettyConfig;
|
||||
|
||||
/// Where to find the serialized animation graph.
|
||||
static ANIMATION_GRAPH_PATH: &str = "animation_graphs/Fox.animgraph.ron";
|
||||
|
||||
/// The indices of the nodes containing animation clips in the graph.
|
||||
static CLIP_NODE_INDICES: [u32; 3] = [2, 3, 4];
|
||||
|
||||
/// The help text in the upper left corner.
|
||||
static HELP_TEXT: &str = "Click and drag an animation clip node to change its weight";
|
||||
|
||||
/// The node widgets in the UI.
|
||||
static NODE_TYPES: [NodeType; 5] = [
|
||||
NodeType::Clip(ClipNode::new("Idle", 0)),
|
||||
NodeType::Clip(ClipNode::new("Walk", 1)),
|
||||
NodeType::Blend("Root"),
|
||||
NodeType::Blend("Blend\n0.5"),
|
||||
NodeType::Clip(ClipNode::new("Run", 2)),
|
||||
];
|
||||
|
||||
/// The positions of the node widgets in the UI.
|
||||
///
|
||||
/// These are in the same order as [`NODE_TYPES`] above.
|
||||
static NODE_RECTS: [NodeRect; 5] = [
|
||||
NodeRect::new(10.00, 10.00, 97.64, 48.41),
|
||||
NodeRect::new(10.00, 78.41, 97.64, 48.41),
|
||||
NodeRect::new(286.08, 78.41, 97.64, 48.41),
|
||||
NodeRect::new(148.04, 44.20, 97.64, 48.41),
|
||||
NodeRect::new(10.00, 146.82, 97.64, 48.41),
|
||||
];
|
||||
|
||||
/// The positions of the horizontal lines in the UI.
|
||||
static HORIZONTAL_LINES: [Line; 6] = [
|
||||
Line::new(107.64, 34.21, 20.20),
|
||||
Line::new(107.64, 102.61, 20.20),
|
||||
Line::new(107.64, 171.02, 158.24),
|
||||
Line::new(127.84, 68.41, 20.20),
|
||||
Line::new(245.68, 68.41, 20.20),
|
||||
Line::new(265.88, 102.61, 20.20),
|
||||
];
|
||||
|
||||
/// The positions of the vertical lines in the UI.
|
||||
static VERTICAL_LINES: [Line; 2] = [
|
||||
Line::new(127.83, 34.21, 68.40),
|
||||
Line::new(265.88, 68.41, 102.61),
|
||||
];
|
||||
|
||||
/// Initializes the app.
|
||||
fn main() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let args: Args = argh::from_env();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let args = Args::from_args(&[], &[]).unwrap();
|
||||
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Bevy Animation Graph Example".into(),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}))
|
||||
.add_systems(Startup, (setup_assets, setup_scene, setup_ui))
|
||||
.add_systems(Update, init_animations.before(animate_targets))
|
||||
.add_systems(
|
||||
Update,
|
||||
(handle_weight_drag, update_ui, sync_weights).chain(),
|
||||
)
|
||||
.insert_resource(args)
|
||||
.insert_resource(AmbientLight {
|
||||
color: WHITE.into(),
|
||||
brightness: 100.0,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Demonstrates animation blending with animation graphs
|
||||
#[derive(FromArgs, Resource)]
|
||||
struct Args {
|
||||
/// disables loading of the animation graph asset from disk
|
||||
#[argh(switch)]
|
||||
no_load: bool,
|
||||
/// regenerates the asset file; implies `--no-load`
|
||||
#[argh(switch)]
|
||||
save: bool,
|
||||
}
|
||||
|
||||
/// The [`AnimationGraph`] asset, which specifies how the animations are to
|
||||
/// be blended together.
|
||||
#[derive(Clone, Resource)]
|
||||
struct ExampleAnimationGraph(Handle<AnimationGraph>);
|
||||
|
||||
/// The current weights of the three playing animations.
|
||||
#[derive(Component)]
|
||||
struct ExampleAnimationWeights {
|
||||
/// The weights of the three playing animations.
|
||||
weights: [f32; 3],
|
||||
}
|
||||
|
||||
/// Initializes the scene.
|
||||
fn setup_assets(
|
||||
mut commands: Commands,
|
||||
mut asset_server: ResMut<AssetServer>,
|
||||
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||
args: Res<Args>,
|
||||
) {
|
||||
// Create or load the assets.
|
||||
if args.no_load || args.save {
|
||||
setup_assets_programmatically(
|
||||
&mut commands,
|
||||
&mut asset_server,
|
||||
&mut animation_graphs,
|
||||
args.save,
|
||||
);
|
||||
} else {
|
||||
setup_assets_via_serialized_animation_graph(&mut commands, &mut asset_server);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_ui(mut commands: Commands) {
|
||||
setup_help_text(&mut commands);
|
||||
setup_node_rects(&mut commands);
|
||||
setup_node_lines(&mut commands);
|
||||
}
|
||||
|
||||
/// Creates the assets programmatically, including the animation graph.
|
||||
/// Optionally saves them to disk if `save` is present (corresponding to the
|
||||
/// `--save` option).
|
||||
fn setup_assets_programmatically(
|
||||
commands: &mut Commands,
|
||||
asset_server: &mut AssetServer,
|
||||
animation_graphs: &mut Assets<AnimationGraph>,
|
||||
_save: bool,
|
||||
) {
|
||||
// Create the nodes.
|
||||
let mut animation_graph = AnimationGraph::new();
|
||||
let blend_node = animation_graph.add_blend(0.5, animation_graph.root);
|
||||
animation_graph.add_clip(
|
||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
||||
1.0,
|
||||
animation_graph.root,
|
||||
);
|
||||
animation_graph.add_clip(
|
||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
||||
1.0,
|
||||
blend_node,
|
||||
);
|
||||
animation_graph.add_clip(
|
||||
asset_server.load("models/animated/Fox.glb#Animation2"),
|
||||
1.0,
|
||||
blend_node,
|
||||
);
|
||||
|
||||
// If asked to save, do so.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if _save {
|
||||
let animation_graph = animation_graph.clone();
|
||||
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
let mut animation_graph_writer = File::create(Path::join(
|
||||
&FileAssetReader::get_base_path(),
|
||||
Path::join(Path::new("assets"), Path::new(ANIMATION_GRAPH_PATH)),
|
||||
))
|
||||
.expect("Failed to open the animation graph asset");
|
||||
ron::ser::to_writer_pretty(
|
||||
&mut animation_graph_writer,
|
||||
&animation_graph,
|
||||
PrettyConfig::default(),
|
||||
)
|
||||
.expect("Failed to serialize the animation graph");
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Add the graph.
|
||||
let handle = animation_graphs.add(animation_graph);
|
||||
|
||||
// Save the assets in a resource.
|
||||
commands.insert_resource(ExampleAnimationGraph(handle));
|
||||
}
|
||||
|
||||
fn setup_assets_via_serialized_animation_graph(
|
||||
commands: &mut Commands,
|
||||
asset_server: &mut AssetServer,
|
||||
) {
|
||||
commands.insert_resource(ExampleAnimationGraph(
|
||||
asset_server.load(ANIMATION_GRAPH_PATH),
|
||||
));
|
||||
}
|
||||
|
||||
/// Spawns the animated fox.
|
||||
fn setup_scene(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-10.0, 5.0, 13.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
intensity: 10_000_000.0,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(-4.0, 8.0, 13.0),
|
||||
..default()
|
||||
});
|
||||
|
||||
commands.spawn(SceneBundle {
|
||||
scene: asset_server.load("models/animated/Fox.glb#Scene0"),
|
||||
transform: Transform::from_scale(Vec3::splat(0.07)),
|
||||
..default()
|
||||
});
|
||||
|
||||
// Ground
|
||||
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Circle::new(7.0)),
|
||||
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
|
||||
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Places the help text at the top left of the window.
|
||||
fn setup_help_text(commands: &mut Commands) {
|
||||
commands.spawn(TextBundle {
|
||||
text: Text::from_section(
|
||||
HELP_TEXT,
|
||||
TextStyle {
|
||||
font_size: 20.0,
|
||||
..default()
|
||||
},
|
||||
),
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(12.0),
|
||||
left: Val::Px(12.0),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Initializes the node UI widgets.
|
||||
fn setup_node_rects(commands: &mut Commands) {
|
||||
for (node_rect, node_type) in NODE_RECTS.iter().zip(NODE_TYPES.iter()) {
|
||||
let node_string = match *node_type {
|
||||
NodeType::Clip(ref clip) => clip.text,
|
||||
NodeType::Blend(text) => text,
|
||||
};
|
||||
|
||||
let text = commands
|
||||
.spawn(TextBundle {
|
||||
text: Text::from_section(
|
||||
node_string,
|
||||
TextStyle {
|
||||
font_size: 16.0,
|
||||
color: ANTIQUE_WHITE.into(),
|
||||
..default()
|
||||
},
|
||||
)
|
||||
.with_justify(JustifyText::Center),
|
||||
..default()
|
||||
})
|
||||
.id();
|
||||
|
||||
let container = {
|
||||
let mut container = commands.spawn((
|
||||
NodeBundle {
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
bottom: Val::Px(node_rect.bottom),
|
||||
left: Val::Px(node_rect.left),
|
||||
height: Val::Px(node_rect.height),
|
||||
width: Val::Px(node_rect.width),
|
||||
align_items: AlignItems::Center,
|
||||
justify_items: JustifyItems::Center,
|
||||
align_content: AlignContent::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
border_color: WHITE.into(),
|
||||
..default()
|
||||
},
|
||||
Outline::new(Val::Px(1.), Val::ZERO, Color::WHITE),
|
||||
));
|
||||
|
||||
if let NodeType::Clip(ref clip) = node_type {
|
||||
container.insert((
|
||||
Interaction::None,
|
||||
RelativeCursorPosition::default(),
|
||||
(*clip).clone(),
|
||||
));
|
||||
}
|
||||
|
||||
container.id()
|
||||
};
|
||||
|
||||
// Create the background color.
|
||||
if let NodeType::Clip(_) = node_type {
|
||||
let background = commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(0.),
|
||||
left: Val::Px(0.),
|
||||
height: Val::Px(node_rect.height),
|
||||
width: Val::Px(node_rect.width),
|
||||
..default()
|
||||
},
|
||||
background_color: DARK_GREEN.into(),
|
||||
..default()
|
||||
})
|
||||
.id();
|
||||
|
||||
commands.entity(container).add_child(background);
|
||||
}
|
||||
|
||||
commands.entity(container).add_child(text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates boxes for the horizontal and vertical lines.
|
||||
///
|
||||
/// This is a bit hacky: it uses 1-pixel-wide and 1-pixel-high boxes to draw
|
||||
/// vertical and horizontal lines, respectively.
|
||||
fn setup_node_lines(commands: &mut Commands) {
|
||||
for line in &HORIZONTAL_LINES {
|
||||
commands.spawn(NodeBundle {
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
bottom: Val::Px(line.bottom),
|
||||
left: Val::Px(line.left),
|
||||
height: Val::Px(0.0),
|
||||
width: Val::Px(line.length),
|
||||
border: UiRect::bottom(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
border_color: WHITE.into(),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
for line in &VERTICAL_LINES {
|
||||
commands.spawn(NodeBundle {
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
bottom: Val::Px(line.bottom),
|
||||
left: Val::Px(line.left),
|
||||
height: Val::Px(line.length),
|
||||
width: Val::Px(0.0),
|
||||
border: UiRect::left(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
border_color: WHITE.into(),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the animation graph to the scene, and plays all three animations.
|
||||
fn init_animations(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &mut AnimationPlayer)>,
|
||||
animation_graph: Res<ExampleAnimationGraph>,
|
||||
mut done: Local<bool>,
|
||||
) {
|
||||
if *done {
|
||||
return;
|
||||
}
|
||||
|
||||
for (entity, mut player) in query.iter_mut() {
|
||||
commands.entity(entity).insert((
|
||||
animation_graph.0.clone(),
|
||||
ExampleAnimationWeights::default(),
|
||||
));
|
||||
for &node_index in &CLIP_NODE_INDICES {
|
||||
player.play(node_index.into()).repeat();
|
||||
}
|
||||
|
||||
*done = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Read cursor position relative to clip nodes, allowing the user to change weights
|
||||
/// when dragging the node UI widgets.
|
||||
fn handle_weight_drag(
|
||||
mut interaction_query: Query<(&Interaction, &RelativeCursorPosition, &ClipNode)>,
|
||||
mut animation_weights_query: Query<&mut ExampleAnimationWeights>,
|
||||
) {
|
||||
for (interaction, relative_cursor, clip_node) in &mut interaction_query {
|
||||
if !matches!(*interaction, Interaction::Pressed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(pos) = relative_cursor.normalized else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for mut animation_weights in animation_weights_query.iter_mut() {
|
||||
animation_weights.weights[clip_node.index] = pos.x.clamp(0., 1.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the UI based on the weights that the user has chosen.
|
||||
fn update_ui(
|
||||
mut text_query: Query<&mut Text>,
|
||||
mut background_query: Query<&mut Style, Without<Text>>,
|
||||
container_query: Query<(&Children, &ClipNode)>,
|
||||
animation_weights_query: Query<&ExampleAnimationWeights, Changed<ExampleAnimationWeights>>,
|
||||
) {
|
||||
for animation_weights in animation_weights_query.iter() {
|
||||
for (children, clip_node) in &container_query {
|
||||
// Draw the green background color to visually indicate the weight.
|
||||
let mut bg_iter = background_query.iter_many_mut(children);
|
||||
if let Some(mut style) = bg_iter.fetch_next() {
|
||||
// All nodes are the same width, so `NODE_RECTS[0]` is as good as any other.
|
||||
style.width =
|
||||
Val::Px(NODE_RECTS[0].width * animation_weights.weights[clip_node.index]);
|
||||
}
|
||||
|
||||
// Update the node labels with the current weights.
|
||||
let mut text_iter = text_query.iter_many_mut(children);
|
||||
if let Some(mut text) = text_iter.fetch_next() {
|
||||
text.sections[0].value = format!(
|
||||
"{}\n{:.2}",
|
||||
clip_node.text, animation_weights.weights[clip_node.index]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the weights that were set in the UI and assigns them to the actual
|
||||
/// playing animation.
|
||||
fn sync_weights(mut query: Query<(&mut AnimationPlayer, &ExampleAnimationWeights)>) {
|
||||
for (mut animation_player, animation_weights) in query.iter_mut() {
|
||||
for (&animation_node_index, &animation_weight) in CLIP_NODE_INDICES
|
||||
.iter()
|
||||
.zip(animation_weights.weights.iter())
|
||||
{
|
||||
// If the animation happens to be no longer active, restart it.
|
||||
if !animation_player.animation_is_playing(animation_node_index.into()) {
|
||||
animation_player.play(animation_node_index.into());
|
||||
}
|
||||
|
||||
// Set the weight.
|
||||
if let Some(active_animation) =
|
||||
animation_player.animation_mut(animation_node_index.into())
|
||||
{
|
||||
active_animation.set_weight(animation_weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An on-screen representation of a node.
|
||||
#[derive(Debug)]
|
||||
struct NodeRect {
|
||||
/// The number of pixels that this rectangle is from the left edge of the
|
||||
/// window.
|
||||
left: f32,
|
||||
/// The number of pixels that this rectangle is from the bottom edge of the
|
||||
/// window.
|
||||
bottom: f32,
|
||||
/// The width of this rectangle in pixels.
|
||||
width: f32,
|
||||
/// The height of this rectangle in pixels.
|
||||
height: f32,
|
||||
}
|
||||
|
||||
/// Either a straight horizontal or a straight vertical line on screen.
|
||||
///
|
||||
/// The line starts at (`left`, `bottom`) and goes either right (if the line is
|
||||
/// horizontal) or down (if the line is vertical).
|
||||
struct Line {
|
||||
/// The number of pixels that the start of this line is from the left edge
|
||||
/// of the screen.
|
||||
left: f32,
|
||||
/// The number of pixels that the start of this line is from the bottom edge
|
||||
/// of the screen.
|
||||
bottom: f32,
|
||||
/// The length of the line.
|
||||
length: f32,
|
||||
}
|
||||
|
||||
/// The type of each node in the UI: either a clip node or a blend node.
|
||||
enum NodeType {
|
||||
/// A clip node, which specifies an animation.
|
||||
Clip(ClipNode),
|
||||
/// A blend node with no animation and a string label.
|
||||
Blend(&'static str),
|
||||
}
|
||||
|
||||
/// The label for the UI representation of a clip node.
|
||||
#[derive(Clone, Component)]
|
||||
struct ClipNode {
|
||||
/// The string label of the node.
|
||||
text: &'static str,
|
||||
/// Which of the three animations this UI widget represents.
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Default for ExampleAnimationWeights {
|
||||
fn default() -> Self {
|
||||
Self { weights: [1.0; 3] }
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipNode {
|
||||
/// Creates a new [`ClipNodeText`] from a label and the animation index.
|
||||
const fn new(text: &'static str, index: usize) -> Self {
|
||||
Self { text, index }
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeRect {
|
||||
/// Creates a new [`NodeRect`] from the lower-left corner and size.
|
||||
///
|
||||
/// Note that node rectangles are anchored in the *lower*-left corner. The
|
||||
/// `bottom` parameter specifies vertical distance from the *bottom* of the
|
||||
/// window.
|
||||
const fn new(left: f32, bottom: f32, width: f32, height: f32) -> NodeRect {
|
||||
NodeRect {
|
||||
left,
|
||||
bottom,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// Creates a new [`Line`], either horizontal or vertical.
|
||||
///
|
||||
/// Note that the line's start point is anchored in the lower-*left* corner,
|
||||
/// and that the `length` extends either to the right or downward.
|
||||
const fn new(left: f32, bottom: f32, length: f32) -> Self {
|
||||
Self {
|
||||
left,
|
||||
bottom,
|
||||
length,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -56,18 +56,24 @@ fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
|
|||
/// Plays an [`AnimationClip`] from the loaded [`Gltf`] on the [`AnimationPlayer`] created by the spawned scene.
|
||||
fn setup_animations(
|
||||
mut has_setup: Local<bool>,
|
||||
mut players: Query<(&Name, &mut AnimationPlayer)>,
|
||||
mut commands: Commands,
|
||||
mut players: Query<(Entity, &Name, &mut AnimationPlayer)>,
|
||||
morph_data: Res<MorphData>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
if *has_setup {
|
||||
return;
|
||||
}
|
||||
for (name, mut player) in &mut players {
|
||||
for (entity, name, mut player) in &mut players {
|
||||
// The name of the entity in the GLTF scene containing the AnimationPlayer for our morph targets is "Main"
|
||||
if name.as_str() != "Main" {
|
||||
continue;
|
||||
}
|
||||
player.play(morph_data.the_wave.clone()).repeat();
|
||||
|
||||
let (graph, animation) = AnimationGraph::from_clip(morph_data.the_wave.clone());
|
||||
commands.entity(entity).insert(graphs.add(graph));
|
||||
|
||||
player.play(animation).repeat();
|
||||
*has_setup = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use bevy::{
|
|||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||
pbr::CascadeShadowConfigBuilder,
|
||||
prelude::*,
|
||||
window::{PresentMode, WindowPlugin, WindowResolution},
|
||||
window::{PresentMode, WindowResolution},
|
||||
winit::{UpdateMode, WinitSettings},
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,10 @@ fn main() {
|
|||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Animations(Vec<Handle<AnimationClip>>);
|
||||
struct Animations {
|
||||
node_indices: Vec<AnimationNodeIndex>,
|
||||
graph: Handle<AnimationGraph>,
|
||||
}
|
||||
|
||||
const RING_SPACING: f32 = 2.0;
|
||||
const FOX_SPACING: f32 = 2.0;
|
||||
|
@ -108,16 +111,25 @@ fn setup(
|
|||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||
foxes: Res<Foxes>,
|
||||
) {
|
||||
warn!(include_str!("warning_string.txt"));
|
||||
|
||||
// Insert a resource with the current scene information
|
||||
commands.insert_resource(Animations(vec![
|
||||
let animation_clips = [
|
||||
asset_server.load("models/animated/Fox.glb#Animation2"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
||||
]));
|
||||
];
|
||||
let mut animation_graph = AnimationGraph::new();
|
||||
let node_indices = animation_graph
|
||||
.add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root)
|
||||
.collect();
|
||||
commands.insert_resource(Animations {
|
||||
node_indices,
|
||||
graph: animation_graphs.add(animation_graph),
|
||||
});
|
||||
|
||||
// Foxes
|
||||
// Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
|
||||
|
@ -222,14 +234,20 @@ fn setup(
|
|||
fn setup_scene_once_loaded(
|
||||
animations: Res<Animations>,
|
||||
foxes: Res<Foxes>,
|
||||
mut commands: Commands,
|
||||
mut player: Query<(Entity, &mut AnimationPlayer)>,
|
||||
mut done: Local<bool>,
|
||||
) {
|
||||
if !*done && player.iter().len() == foxes.count {
|
||||
for (entity, mut player) in &mut player {
|
||||
player.play(animations.0[0].clone_weak()).repeat();
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(animations.graph.clone())
|
||||
.insert(AnimationTransitions::new());
|
||||
|
||||
let playing_animation = player.play(animations.node_indices[0]).repeat();
|
||||
if !foxes.sync {
|
||||
player.seek_to(entity.index() as f32 / 10.0);
|
||||
playing_animation.seek_to(entity.index() as f32 / 10.0);
|
||||
}
|
||||
}
|
||||
*done = true;
|
||||
|
@ -254,7 +272,7 @@ fn update_fox_rings(
|
|||
|
||||
fn keyboard_animation_control(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut animation_player: Query<&mut AnimationPlayer>,
|
||||
mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
animations: Res<Animations>,
|
||||
mut current_animation: Local<usize>,
|
||||
mut foxes: ResMut<Foxes>,
|
||||
|
@ -272,42 +290,39 @@ fn keyboard_animation_control(
|
|||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
*current_animation = (*current_animation + 1) % animations.0.len();
|
||||
*current_animation = (*current_animation + 1) % animations.node_indices.len();
|
||||
}
|
||||
|
||||
for mut player in &mut animation_player {
|
||||
for (mut player, mut transitions) in &mut animation_player {
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
if player.is_paused() {
|
||||
player.resume();
|
||||
if player.all_paused() {
|
||||
player.resume_all();
|
||||
} else {
|
||||
player.pause();
|
||||
player.pause_all();
|
||||
}
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 1.25);
|
||||
player.adjust_speeds(1.25);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||
let speed = player.speed();
|
||||
player.set_speed(speed * 0.8);
|
||||
player.adjust_speeds(0.8);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||
let elapsed = player.seek_time();
|
||||
player.seek_to(elapsed - 0.1);
|
||||
player.seek_all_by(-0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||
let elapsed = player.seek_time();
|
||||
player.seek_to(elapsed + 0.1);
|
||||
player.seek_all_by(0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
player
|
||||
.play_with_transition(
|
||||
animations.0[*current_animation].clone_weak(),
|
||||
transitions
|
||||
.play(
|
||||
&mut player,
|
||||
animations.node_indices[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
)
|
||||
.repeat();
|
||||
|
|
|
@ -8,21 +8,24 @@ use crate::scene_viewer_plugin::SceneHandle;
|
|||
/// Controls animation clips for a unique entity.
|
||||
#[derive(Component)]
|
||||
struct Clips {
|
||||
clips: Vec<Handle<AnimationClip>>,
|
||||
nodes: Vec<AnimationNodeIndex>,
|
||||
current: usize,
|
||||
}
|
||||
impl Clips {
|
||||
fn new(clips: Vec<Handle<AnimationClip>>) -> Self {
|
||||
Clips { clips, current: 0 }
|
||||
fn new(clips: Vec<AnimationNodeIndex>) -> Self {
|
||||
Clips {
|
||||
nodes: clips,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
/// # Panics
|
||||
///
|
||||
/// When no clips are present.
|
||||
fn current(&self) -> Handle<AnimationClip> {
|
||||
self.clips[self.current].clone_weak()
|
||||
fn current(&self) -> AnimationNodeIndex {
|
||||
self.nodes[self.current]
|
||||
}
|
||||
fn advance_to_next(&mut self) {
|
||||
self.current = (self.current + 1) % self.clips.len();
|
||||
self.current = (self.current + 1) % self.nodes.len();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,6 +41,7 @@ fn assign_clips(
|
|||
clips: Res<Assets<AnimationClip>>,
|
||||
gltf_assets: Res<Assets<Gltf>>,
|
||||
assets: Res<AssetServer>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
mut commands: Commands,
|
||||
mut setup: Local<bool>,
|
||||
) {
|
||||
|
@ -69,7 +73,8 @@ fn assign_clips(
|
|||
// is considered to belong to an animation player if all targets of the clip
|
||||
// refer to entities whose nearest ancestor player is that animation player.
|
||||
|
||||
let mut player_to_clips: EntityHashMap<Vec<_>> = EntityHashMap::default();
|
||||
let mut player_to_graph: EntityHashMap<(AnimationGraph, Vec<AnimationNodeIndex>)> =
|
||||
EntityHashMap::default();
|
||||
|
||||
for (clip_id, clip) in clips.iter() {
|
||||
let mut ancestor_player = None;
|
||||
|
@ -120,23 +125,27 @@ fn assign_clips(
|
|||
continue;
|
||||
};
|
||||
|
||||
player_to_clips
|
||||
.entry(ancestor_player)
|
||||
.or_default()
|
||||
.push(clip_handle);
|
||||
let &mut (ref mut graph, ref mut clip_indices) =
|
||||
player_to_graph.entry(ancestor_player).or_default();
|
||||
let node_index = graph.add_clip(clip_handle, 1.0, graph.root);
|
||||
clip_indices.push(node_index);
|
||||
}
|
||||
|
||||
// Now that we've built up a list of all clips that belong to each player,
|
||||
// package them up into a `Clips` component, play the first such animation,
|
||||
// and add that component to the player.
|
||||
for (player_entity, clips) in player_to_clips {
|
||||
for (player_entity, (graph, clips)) in player_to_graph {
|
||||
let Ok(mut player) = players.get_mut(player_entity) else {
|
||||
warn!("Animation targets referenced a nonexistent player. This shouldn't happen.");
|
||||
continue;
|
||||
};
|
||||
let graph = graphs.add(graph);
|
||||
let animations = Clips::new(clips);
|
||||
player.play(animations.current()).repeat();
|
||||
commands.entity(player_entity).insert(animations);
|
||||
commands
|
||||
.entity(player_entity)
|
||||
.insert(animations)
|
||||
.insert(graph);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,30 +159,30 @@ fn handle_inputs(
|
|||
None => format!("entity {entity:?}"),
|
||||
};
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
if player.is_paused() {
|
||||
info!("resuming animation for {display_entity_name}");
|
||||
player.resume();
|
||||
if player.all_paused() {
|
||||
info!("resuming animations for {display_entity_name}");
|
||||
player.resume_all();
|
||||
} else {
|
||||
info!("pausing animation for {display_entity_name}");
|
||||
player.pause();
|
||||
player.pause_all();
|
||||
}
|
||||
}
|
||||
if clips.clips.len() <= 1 {
|
||||
if clips.nodes.len() <= 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
info!("switching to new animation for {display_entity_name}");
|
||||
|
||||
let resume = !player.is_paused();
|
||||
let resume = !player.all_paused();
|
||||
// set the current animation to its start and pause it to reset to its starting state
|
||||
player.seek_to(0.0).pause();
|
||||
player.rewind_all().pause_all();
|
||||
|
||||
clips.advance_to_next();
|
||||
let current_clip = clips.current();
|
||||
player.play(current_clip).repeat();
|
||||
if resume {
|
||||
player.resume();
|
||||
player.resume_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue