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"
|
category = "Animation"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "morph_targets"
|
name = "morph_targets"
|
||||||
path = "examples/animation/morph_targets.rs"
|
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_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||||
bevy_asset = { path = "../bevy_asset", 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_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_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
|
||||||
"bevy",
|
"bevy",
|
||||||
|
"petgraph",
|
||||||
] }
|
] }
|
||||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||||
bevy_time = { path = "../bevy_time", 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" }
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
|
||||||
|
|
||||||
# other
|
# other
|
||||||
|
fixedbitset = "0.4"
|
||||||
|
petgraph = { version = "0.6", features = ["serde-1"] }
|
||||||
|
ron = "0.8"
|
||||||
|
serde = "1"
|
||||||
sha1_smol = { version = "1.0" }
|
sha1_smol = { version = "1.0" }
|
||||||
|
thiserror = "1"
|
||||||
|
thread_local = "1"
|
||||||
uuid = { version = "1.7", features = ["v4"] }
|
uuid = { version = "1.7", features = ["v4"] }
|
||||||
|
|
||||||
[lints]
|
[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 crate::{Asset, AssetIndex};
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use std::{
|
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 identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`).
|
||||||
///
|
///
|
||||||
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
|
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
|
||||||
#[derive(Reflect)]
|
#[derive(Reflect, Serialize, Deserialize)]
|
||||||
pub enum AssetId<A: Asset> {
|
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
|
/// 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
|
/// 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"]
|
bevy = ["smallvec", "bevy_math", "smol_str"]
|
||||||
glam = ["dep:glam"]
|
glam = ["dep:glam"]
|
||||||
bevy_math = ["glam", "dep:bevy_math"]
|
bevy_math = ["glam", "dep:bevy_math"]
|
||||||
|
petgraph = ["dep:petgraph"]
|
||||||
smallvec = ["dep:smallvec"]
|
smallvec = ["dep:smallvec"]
|
||||||
uuid = ["dep:uuid"]
|
uuid = ["dep:uuid"]
|
||||||
# When enabled, allows documentation comments to be accessed via reflection
|
# When enabled, allows documentation comments to be accessed via reflection
|
||||||
|
@ -37,6 +38,7 @@ serde = "1"
|
||||||
smallvec = { version = "1.11", optional = true }
|
smallvec = { version = "1.11", optional = true }
|
||||||
|
|
||||||
glam = { version = "0.25", features = ["serde"], 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 }
|
smol_str = { version = "0.2.0", optional = true }
|
||||||
uuid = { version = "1.0", optional = true, features = ["v4", "serde"] }
|
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 primitives3d;
|
||||||
mod rect;
|
mod rect;
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "petgraph")]
|
||||||
|
mod petgraph;
|
||||||
#[cfg(feature = "smallvec")]
|
#[cfg(feature = "smallvec")]
|
||||||
mod smallvec;
|
mod smallvec;
|
||||||
#[cfg(feature = "smol_str")]
|
#[cfg(feature = "smol_str")]
|
||||||
|
|
|
@ -97,8 +97,11 @@ struct ExampleAssets {
|
||||||
// The glTF scene containing the animated fox.
|
// The glTF scene containing the animated fox.
|
||||||
fox: Handle<Scene>,
|
fox: Handle<Scene>,
|
||||||
|
|
||||||
// The animation that the fox will play.
|
// The graph containing the animation that the fox will play.
|
||||||
fox_animation: Handle<AnimationClip>,
|
fox_animation_graph: Handle<AnimationGraph>,
|
||||||
|
|
||||||
|
// The node within the animation graph containing the animation.
|
||||||
|
fox_animation_node: AnimationNodeIndex,
|
||||||
|
|
||||||
// The voxel cube mesh.
|
// The voxel cube mesh.
|
||||||
voxel_cube: Handle<Mesh>,
|
voxel_cube: Handle<Mesh>,
|
||||||
|
@ -513,6 +516,10 @@ fn handle_mouse_clicks(
|
||||||
|
|
||||||
impl FromWorld for ExampleAssets {
|
impl FromWorld for ExampleAssets {
|
||||||
fn from_world(world: &mut World) -> Self {
|
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 {
|
ExampleAssets {
|
||||||
main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)),
|
main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)),
|
||||||
fox: world.load_asset("models/animated/Fox.glb#Scene0"),
|
fox: world.load_asset("models/animated/Fox.glb#Scene0"),
|
||||||
|
@ -520,7 +527,8 @@ impl FromWorld for ExampleAssets {
|
||||||
main_scene: world
|
main_scene: world
|
||||||
.load_asset("models/IrradianceVolumeExample/IrradianceVolumeExample.glb#Scene0"),
|
.load_asset("models/IrradianceVolumeExample/IrradianceVolumeExample.glb#Scene0"),
|
||||||
irradiance_volume: world.load_asset("irradiance_volumes/Example.vxgi.ktx2"),
|
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()),
|
voxel_cube: world.add_asset(Cuboid::default()),
|
||||||
// Just use a specular map for the skybox since it's not too blurry.
|
// 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
|
// 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.
|
// Plays the animation on the fox.
|
||||||
fn play_animations(assets: Res<ExampleAssets>, mut players: Query<&mut AnimationPlayer>) {
|
fn play_animations(
|
||||||
for mut player in players.iter_mut() {
|
mut commands: Commands,
|
||||||
// This will safely do nothing if the animation is already playing.
|
assets: Res<ExampleAssets>,
|
||||||
player.play(assets.fox_animation.clone()).repeat();
|
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 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
|
[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
|
[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
|
[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
|
[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::f32::consts::PI;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bevy::{animation::RepeatAnimation, pbr::CascadeShadowConfigBuilder, prelude::*};
|
use bevy::{
|
||||||
|
animation::{animate_targets, RepeatAnimation},
|
||||||
|
pbr::CascadeShadowConfigBuilder,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -13,28 +17,47 @@ fn main() {
|
||||||
})
|
})
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(
|
.add_systems(Update, setup_scene_once_loaded.before(animate_targets))
|
||||||
Update,
|
.add_systems(Update, keyboard_animation_control)
|
||||||
(setup_scene_once_loaded, keyboard_animation_control),
|
|
||||||
)
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct Animations(Vec<Handle<AnimationClip>>);
|
struct Animations {
|
||||||
|
animations: Vec<AnimationNodeIndex>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
graph: Handle<AnimationGraph>,
|
||||||
|
}
|
||||||
|
|
||||||
fn setup(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
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
|
// Insert a resource with the current scene information
|
||||||
commands.insert_resource(Animations(vec![
|
let graph = graphs.add(graph);
|
||||||
asset_server.load("models/animated/Fox.glb#Animation2"),
|
commands.insert_resource(Animations {
|
||||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
animations,
|
||||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
graph: graph.clone(),
|
||||||
]));
|
});
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
commands.spawn(Camera3dBundle {
|
commands.spawn(Camera3dBundle {
|
||||||
|
@ -83,76 +106,108 @@ fn setup(
|
||||||
|
|
||||||
// Once the scene is loaded, start the animation
|
// Once the scene is loaded, start the animation
|
||||||
fn setup_scene_once_loaded(
|
fn setup_scene_once_loaded(
|
||||||
|
mut commands: Commands,
|
||||||
animations: Res<Animations>,
|
animations: Res<Animations>,
|
||||||
mut players: Query<&mut AnimationPlayer, Added<AnimationPlayer>>,
|
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||||
) {
|
) {
|
||||||
for mut player in &mut players {
|
for (entity, mut player) in &mut players {
|
||||||
player.play(animations.0[0].clone_weak()).repeat();
|
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(
|
fn keyboard_animation_control(
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
mut animation_players: Query<&mut AnimationPlayer>,
|
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||||
animations: Res<Animations>,
|
animations: Res<Animations>,
|
||||||
mut current_animation: Local<usize>,
|
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 keyboard_input.just_pressed(KeyCode::Space) {
|
||||||
if player.is_paused() {
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.resume();
|
if playing_animation.is_paused() {
|
||||||
|
playing_animation.resume();
|
||||||
} else {
|
} else {
|
||||||
player.pause();
|
playing_animation.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||||
let speed = player.speed();
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.set_speed(speed * 1.2);
|
let speed = playing_animation.speed();
|
||||||
|
playing_animation.set_speed(speed * 1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||||
let speed = player.speed();
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.set_speed(speed * 0.8);
|
let speed = playing_animation.speed();
|
||||||
|
playing_animation.set_speed(speed * 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||||
let elapsed = player.seek_time();
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.seek_to(elapsed - 0.1);
|
let elapsed = playing_animation.seek_time();
|
||||||
|
playing_animation.seek_to(elapsed - 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||||
let elapsed = player.seek_time();
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.seek_to(elapsed + 0.1);
|
let elapsed = playing_animation.seek_time();
|
||||||
|
playing_animation.seek_to(elapsed + 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||||
*current_animation = (*current_animation + 1) % animations.0.len();
|
*current_animation = (*current_animation + 1) % animations.animations.len();
|
||||||
player
|
|
||||||
.play_with_transition(
|
transitions
|
||||||
animations.0[*current_animation].clone_weak(),
|
.play(
|
||||||
|
&mut player,
|
||||||
|
animations.animations[*current_animation],
|
||||||
Duration::from_millis(250),
|
Duration::from_millis(250),
|
||||||
)
|
)
|
||||||
.repeat();
|
.repeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Digit1) {
|
if keyboard_input.just_pressed(KeyCode::Digit1) {
|
||||||
player.set_repeat(RepeatAnimation::Count(1));
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.replay();
|
playing_animation
|
||||||
|
.set_repeat(RepeatAnimation::Count(1))
|
||||||
|
.replay();
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Digit3) {
|
if keyboard_input.just_pressed(KeyCode::Digit3) {
|
||||||
player.set_repeat(RepeatAnimation::Count(3));
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.replay();
|
playing_animation
|
||||||
|
.set_repeat(RepeatAnimation::Count(3))
|
||||||
|
.replay();
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Digit5) {
|
if keyboard_input.just_pressed(KeyCode::Digit5) {
|
||||||
player.set_repeat(RepeatAnimation::Count(5));
|
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||||
player.replay();
|
playing_animation
|
||||||
|
.set_repeat(RepeatAnimation::Count(5))
|
||||||
|
.replay();
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::KeyL) {
|
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 meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
mut animations: ResMut<Assets<AnimationClip>>,
|
mut animations: ResMut<Assets<AnimationClip>>,
|
||||||
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
) {
|
) {
|
||||||
// Camera
|
// Camera
|
||||||
commands.spawn(Camera3dBundle {
|
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
|
// Create the animation player, and set it to repeat
|
||||||
let mut player = AnimationPlayer::default();
|
let mut player = AnimationPlayer::default();
|
||||||
player.play(animations.add(animation)).repeat();
|
player.play(animation_index).repeat();
|
||||||
|
|
||||||
// Create the scene that will be animated
|
// Create the scene that will be animated
|
||||||
// First entity is the planet
|
// First entity is the planet
|
||||||
|
@ -138,8 +142,9 @@ fn setup(
|
||||||
material: materials.add(Color::srgb(0.8, 0.7, 0.6)),
|
material: materials.add(Color::srgb(0.8, 0.7, 0.6)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
// Add the animation player
|
// Add the animation graph and player
|
||||||
planet,
|
planet,
|
||||||
|
graphs.add(graph),
|
||||||
player,
|
player,
|
||||||
))
|
))
|
||||||
.id();
|
.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.
|
/// Plays an [`AnimationClip`] from the loaded [`Gltf`] on the [`AnimationPlayer`] created by the spawned scene.
|
||||||
fn setup_animations(
|
fn setup_animations(
|
||||||
mut has_setup: Local<bool>,
|
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>,
|
morph_data: Res<MorphData>,
|
||||||
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
) {
|
) {
|
||||||
if *has_setup {
|
if *has_setup {
|
||||||
return;
|
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"
|
// The name of the entity in the GLTF scene containing the AnimationPlayer for our morph targets is "Main"
|
||||||
if name.as_str() != "Main" {
|
if name.as_str() != "Main" {
|
||||||
continue;
|
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;
|
*has_setup = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use bevy::{
|
||||||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||||
pbr::CascadeShadowConfigBuilder,
|
pbr::CascadeShadowConfigBuilder,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
window::{PresentMode, WindowPlugin, WindowResolution},
|
window::{PresentMode, WindowResolution},
|
||||||
winit::{UpdateMode, WinitSettings},
|
winit::{UpdateMode, WinitSettings},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +78,10 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct Animations(Vec<Handle<AnimationClip>>);
|
struct Animations {
|
||||||
|
node_indices: Vec<AnimationNodeIndex>,
|
||||||
|
graph: Handle<AnimationGraph>,
|
||||||
|
}
|
||||||
|
|
||||||
const RING_SPACING: f32 = 2.0;
|
const RING_SPACING: f32 = 2.0;
|
||||||
const FOX_SPACING: f32 = 2.0;
|
const FOX_SPACING: f32 = 2.0;
|
||||||
|
@ -108,16 +111,25 @@ fn setup(
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
foxes: Res<Foxes>,
|
foxes: Res<Foxes>,
|
||||||
) {
|
) {
|
||||||
warn!(include_str!("warning_string.txt"));
|
warn!(include_str!("warning_string.txt"));
|
||||||
|
|
||||||
// Insert a resource with the current scene information
|
// 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#Animation2"),
|
||||||
asset_server.load("models/animated/Fox.glb#Animation1"),
|
asset_server.load("models/animated/Fox.glb#Animation1"),
|
||||||
asset_server.load("models/animated/Fox.glb#Animation0"),
|
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
|
// Foxes
|
||||||
// Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
|
// 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(
|
fn setup_scene_once_loaded(
|
||||||
animations: Res<Animations>,
|
animations: Res<Animations>,
|
||||||
foxes: Res<Foxes>,
|
foxes: Res<Foxes>,
|
||||||
|
mut commands: Commands,
|
||||||
mut player: Query<(Entity, &mut AnimationPlayer)>,
|
mut player: Query<(Entity, &mut AnimationPlayer)>,
|
||||||
mut done: Local<bool>,
|
mut done: Local<bool>,
|
||||||
) {
|
) {
|
||||||
if !*done && player.iter().len() == foxes.count {
|
if !*done && player.iter().len() == foxes.count {
|
||||||
for (entity, mut player) in &mut player {
|
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 {
|
if !foxes.sync {
|
||||||
player.seek_to(entity.index() as f32 / 10.0);
|
playing_animation.seek_to(entity.index() as f32 / 10.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*done = true;
|
*done = true;
|
||||||
|
@ -254,7 +272,7 @@ fn update_fox_rings(
|
||||||
|
|
||||||
fn keyboard_animation_control(
|
fn keyboard_animation_control(
|
||||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
mut animation_player: Query<&mut AnimationPlayer>,
|
mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||||
animations: Res<Animations>,
|
animations: Res<Animations>,
|
||||||
mut current_animation: Local<usize>,
|
mut current_animation: Local<usize>,
|
||||||
mut foxes: ResMut<Foxes>,
|
mut foxes: ResMut<Foxes>,
|
||||||
|
@ -272,42 +290,39 @@ fn keyboard_animation_control(
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
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 keyboard_input.just_pressed(KeyCode::Space) {
|
||||||
if player.is_paused() {
|
if player.all_paused() {
|
||||||
player.resume();
|
player.resume_all();
|
||||||
} else {
|
} else {
|
||||||
player.pause();
|
player.pause_all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||||
let speed = player.speed();
|
player.adjust_speeds(1.25);
|
||||||
player.set_speed(speed * 1.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||||
let speed = player.speed();
|
player.adjust_speeds(0.8);
|
||||||
player.set_speed(speed * 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||||
let elapsed = player.seek_time();
|
player.seek_all_by(-0.1);
|
||||||
player.seek_to(elapsed - 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||||
let elapsed = player.seek_time();
|
player.seek_all_by(0.1);
|
||||||
player.seek_to(elapsed + 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||||
player
|
transitions
|
||||||
.play_with_transition(
|
.play(
|
||||||
animations.0[*current_animation].clone_weak(),
|
&mut player,
|
||||||
|
animations.node_indices[*current_animation],
|
||||||
Duration::from_millis(250),
|
Duration::from_millis(250),
|
||||||
)
|
)
|
||||||
.repeat();
|
.repeat();
|
||||||
|
|
|
@ -8,21 +8,24 @@ use crate::scene_viewer_plugin::SceneHandle;
|
||||||
/// Controls animation clips for a unique entity.
|
/// Controls animation clips for a unique entity.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct Clips {
|
struct Clips {
|
||||||
clips: Vec<Handle<AnimationClip>>,
|
nodes: Vec<AnimationNodeIndex>,
|
||||||
current: usize,
|
current: usize,
|
||||||
}
|
}
|
||||||
impl Clips {
|
impl Clips {
|
||||||
fn new(clips: Vec<Handle<AnimationClip>>) -> Self {
|
fn new(clips: Vec<AnimationNodeIndex>) -> Self {
|
||||||
Clips { clips, current: 0 }
|
Clips {
|
||||||
|
nodes: clips,
|
||||||
|
current: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// When no clips are present.
|
/// When no clips are present.
|
||||||
fn current(&self) -> Handle<AnimationClip> {
|
fn current(&self) -> AnimationNodeIndex {
|
||||||
self.clips[self.current].clone_weak()
|
self.nodes[self.current]
|
||||||
}
|
}
|
||||||
fn advance_to_next(&mut self) {
|
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>>,
|
clips: Res<Assets<AnimationClip>>,
|
||||||
gltf_assets: Res<Assets<Gltf>>,
|
gltf_assets: Res<Assets<Gltf>>,
|
||||||
assets: Res<AssetServer>,
|
assets: Res<AssetServer>,
|
||||||
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut setup: Local<bool>,
|
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
|
// 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.
|
// 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() {
|
for (clip_id, clip) in clips.iter() {
|
||||||
let mut ancestor_player = None;
|
let mut ancestor_player = None;
|
||||||
|
@ -120,23 +125,27 @@ fn assign_clips(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
player_to_clips
|
let &mut (ref mut graph, ref mut clip_indices) =
|
||||||
.entry(ancestor_player)
|
player_to_graph.entry(ancestor_player).or_default();
|
||||||
.or_default()
|
let node_index = graph.add_clip(clip_handle, 1.0, graph.root);
|
||||||
.push(clip_handle);
|
clip_indices.push(node_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we've built up a list of all clips that belong to each player,
|
// 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,
|
// package them up into a `Clips` component, play the first such animation,
|
||||||
// and add that component to the player.
|
// 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 {
|
let Ok(mut player) = players.get_mut(player_entity) else {
|
||||||
warn!("Animation targets referenced a nonexistent player. This shouldn't happen.");
|
warn!("Animation targets referenced a nonexistent player. This shouldn't happen.");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let graph = graphs.add(graph);
|
||||||
let animations = Clips::new(clips);
|
let animations = Clips::new(clips);
|
||||||
player.play(animations.current()).repeat();
|
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:?}"),
|
None => format!("entity {entity:?}"),
|
||||||
};
|
};
|
||||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||||
if player.is_paused() {
|
if player.all_paused() {
|
||||||
info!("resuming animation for {display_entity_name}");
|
info!("resuming animations for {display_entity_name}");
|
||||||
player.resume();
|
player.resume_all();
|
||||||
} else {
|
} else {
|
||||||
info!("pausing animation for {display_entity_name}");
|
info!("pausing animation for {display_entity_name}");
|
||||||
player.pause();
|
player.pause_all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if clips.clips.len() <= 1 {
|
if clips.nodes.len() <= 1 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||||
info!("switching to new animation for {display_entity_name}");
|
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
|
// 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();
|
clips.advance_to_next();
|
||||||
let current_clip = clips.current();
|
let current_clip = clips.current();
|
||||||
player.play(current_clip).repeat();
|
player.play(current_clip).repeat();
|
||||||
if resume {
|
if resume {
|
||||||
player.resume();
|
player.resume_all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue