Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_animation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ blake3 = { version = "1.0" }
thiserror = "1"
thread_local = "1"
uuid = { version = "1.7", features = ["v4"] }
smallvec = "1"

[lints]
workspace = true
Expand Down
8 changes: 2 additions & 6 deletions crates/bevy_animation/src/animatable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,12 @@ pub(crate) trait GetKeyframe {
/// This is factored out so that it can be shared between implementations of
/// [`crate::keyframes::Keyframes`].
pub(crate) fn interpolate_keyframes<T>(
dest: &mut T,
keyframes: &(impl GetKeyframe<Output = T> + ?Sized),
interpolation: Interpolation,
step_start: usize,
time: f32,
weight: f32,
duration: f32,
) -> Result<(), AnimationEvaluationError>
) -> Result<T, AnimationEvaluationError>
where
T: Animatable + Clone,
{
Expand Down Expand Up @@ -265,9 +263,7 @@ where
}
};

*dest = T::interpolate(dest, &value, weight);

Ok(())
Ok(value)
}

/// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the
Expand Down
216 changes: 213 additions & 3 deletions crates/bevy_animation/src/graph.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
//! The animation graph, which allows animations to be blended together.

use core::ops::{Index, IndexMut};
use core::iter;
use core::ops::{Index, IndexMut, Range};
use std::io::{self, Write};

use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext};
use bevy_asset::{
io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext,
};
use bevy_ecs::{
event::EventReader,
system::{Res, ResMut, Resource},
};
use bevy_reflect::{Reflect, ReflectSerialize};
use bevy_utils::HashMap;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::{
graph::{DiGraph, NodeIndex},
Direction,
};
use ron::de::SpannedError;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use thiserror::Error;

use crate::{AnimationClip, AnimationTargetId};
Expand Down Expand Up @@ -172,6 +183,96 @@ pub enum AnimationGraphLoadError {
SpannedRon(#[from] SpannedError),
}

/// Acceleration structures for animation graphs that allows Bevy to evaluate
/// them quickly.
///
/// These are kept up to date as [`AnimationGraph`] instances are added,
/// modified, and removed.
#[derive(Default, Reflect, Resource)]
pub struct ThreadedAnimationGraphs(
pub(crate) HashMap<AssetId<AnimationGraph>, ThreadedAnimationGraph>,
);

/// An acceleration structure for an animation graph that allows Bevy to
/// evaluate it quickly.
///
/// This is kept up to date as the associated [`AnimationGraph`] instance is
/// added, modified, or removed.
#[derive(Default, Reflect)]
pub(crate) struct ThreadedAnimationGraph {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to expose this publicly to users, so that if they store animation graphs in somewhere other than Assets<AnimationGraph>, they can still use this (with some more involved manual setup)?

/// A cached postorder traversal of the graph.
///
/// The node indices here are stored in postorder. Siblings are stored in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you document this below, but I think it would be useful to say upfront, right at the top of this doc comment, that it's stored in post-order with children being iterated in reverse (4, 3, 6, 5, 2, 1 instead of 5, 6, 2, 3, 4, 1)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does say "siblings are stored in descending order".

/// descending order. This is because the [`KeyframeEvaluator`] uses a stack
/// for evaluation. Consider this graph:
///
/// ┌─────┐
/// │ │
/// │ 1 │
/// │ │
/// └──┬──┘
/// │
/// ┌───────┼───────┐
/// │ │ │
/// ▼ ▼ ▼
/// ┌─────┐ ┌─────┐ ┌─────┐
/// │ │ │ │ │ │
/// │ 2 │ │ 3 │ │ 4 │
/// │ │ │ │ │ │
/// └──┬──┘ └─────┘ └─────┘
/// │
/// ┌───┴───┐
/// │ │
/// ▼ ▼
/// ┌─────┐ ┌─────┐
/// │ │ │ │
/// │ 5 │ │ 6 │
/// │ │ │ │
/// └─────┘ └─────┘
///
/// The postorder traversal in this case will be (4, 3, 6, 5, 2, 1).
///
/// The fact that the children of each node are sorted in reverse ensures
/// that, at each level, the order of blending proceeds in ascending order
/// by node index, as we guarantee. To illustrate this, consider the way
/// the graph above is evaluated. (Interpolation is represented with the ⊕
/// symbol.)
///
/// | Step | Node | Operation | Stack (after operation) | Blend Register |
/// | ---- | -----| ---------- | ----------------------- | -------------- |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
/// | ---- | -----| ---------- | ----------------------- | -------------- |
/// | ---- | ---- | ---------- | ----------------------- | -------------- |

/// | 1 | 4 | Push | 4 | |
/// | 2 | 3 | Push | 4 3 | |
/// | 3 | 6 | Push | 4 3 6 | |
/// | 4 | 5 | Push | 4 3 6 5 | |
/// | 5 | 2 | Blend 5 | 4 3 6 | 5 |
/// | 6 | 2 | Blend 6 | 4 3 | 5 ⊕ 6 |
/// | 7 | 2 | Push Blend | 4 3 2 | |
/// | 8 | 1 | Blend 2 | 4 3 | 2 |
/// | 9 | 1 | Blend 3 | 4 | 2 ⊕ 3 |
/// | 10 | 1 | Blend 4 | | 2 ⊕ 3 ⊕ 4 |
/// | 11 | 1 | Push Blend | 1 | |
/// | 12 | | Commit | | |
pub(crate) threaded_graph: Vec<AnimationNodeIndex>,

/// A mapping from each parent node index to the range within
/// [`Self::sorted_edges`].
///
/// This allows for quick lookup of the children of each node, sorted in
/// ascending order of node index, without having to sort the result of the
/// `petgraph` traversal functions every frame.
pub(crate) sorted_edge_ranges: Vec<Range<u32>>,

/// A list of the children of each node, sorted in ascending order.
pub(crate) sorted_edges: Vec<AnimationNodeIndex>,

/// A mapping from node index to a bitfield specifying the mask groups that
/// this node masks *out* (i.e. doesn't animate).
///
/// A 1 in bit position N indicates that this node doesn't animate any
/// targets of mask group N.
pub(crate) computed_masks: Vec<u64>,
}

/// A version of [`AnimationGraph`] suitable for serializing as an asset.
///
/// Animation nodes can refer to external animation clips, and the [`AssetId`]
Expand Down Expand Up @@ -571,3 +672,112 @@ impl From<AnimationGraph> for SerializedAnimationGraph {
}
}
}

/// A system that creates, updates, and removes [`ThreadedAnimationGraph`]
/// structures for every changed [`AnimationGraph`].
///
/// The [`ThreadedAnimationGraph`] contains acceleration structures that allow
/// for quick evaluation of that graph's animations.
pub(crate) fn thread_animation_graphs(
mut threaded_animation_graphs: ResMut<ThreadedAnimationGraphs>,
animation_graphs: Res<Assets<AnimationGraph>>,
mut animation_graph_asset_events: EventReader<AssetEvent<AnimationGraph>>,
) {
for animation_graph_asset_event in animation_graph_asset_events.read() {
match *animation_graph_asset_event {
AssetEvent::Added { id }
| AssetEvent::Modified { id }
| AssetEvent::LoadedWithDependencies { id } => {
// Fetch the animation graph.
let Some(animation_graph) = animation_graphs.get(id) else {
continue;
};

// Reuse the allocation if possible.
let mut threaded_animation_graph =
threaded_animation_graphs.0.remove(&id).unwrap_or_default();
threaded_animation_graph.clear();

// Recursively thread the graph in postorder.
threaded_animation_graph.init(animation_graph);
threaded_animation_graph.build_from(
&animation_graph.graph,
animation_graph.root,
0,
);

// Write in the threaded graph.
threaded_animation_graphs
.0
.insert(id, threaded_animation_graph);
}

AssetEvent::Removed { id } => {
threaded_animation_graphs.0.remove(&id);
}
AssetEvent::Unused { .. } => {}
}
}
}

impl ThreadedAnimationGraph {
/// Removes all the data in this [`ThreadedAnimationGraph`], keeping the
/// memory around for later reuse.
fn clear(&mut self) {
self.threaded_graph.clear();
self.sorted_edge_ranges.clear();
self.sorted_edges.clear();
}

/// Prepares the [`ThreadedAnimationGraph`] for recursion.
fn init(&mut self, animation_graph: &AnimationGraph) {
let node_count = animation_graph.graph.node_count();
let edge_count = animation_graph.graph.edge_count();

self.threaded_graph.reserve(node_count);
self.sorted_edges.reserve(edge_count);

self.sorted_edge_ranges.clear();
self.sorted_edge_ranges
.extend(iter::repeat(0..0).take(node_count));

self.computed_masks.clear();
self.computed_masks.extend(iter::repeat(0).take(node_count));
}

/// Recursively constructs the [`ThreadedAnimationGraph`] for the subtree
/// rooted at the given node.
///
/// `mask` specifies the computed mask of the parent node. (It could be
/// fetched from the [`Self::computed_masks`] field, but we pass it
/// explicitly as a micro-optimization.)
fn build_from(
&mut self,
graph: &AnimationDiGraph,
node_index: AnimationNodeIndex,
mut mask: u64,
) {
// Accumulate the mask.
mask |= graph.node_weight(node_index).unwrap().mask;
self.computed_masks.insert(node_index.index(), mask);

// Gather up the indices of our children, and sort them.
let mut kids: SmallVec<[AnimationNodeIndex; 8]> = graph
.neighbors_directed(node_index, Direction::Outgoing)
.collect();
kids.sort_unstable();

// Write in the list of kids.
self.sorted_edge_ranges[node_index.index()] =
(self.sorted_edges.len() as u32)..((self.sorted_edges.len() + kids.len()) as u32);
self.sorted_edges.extend_from_slice(&kids);

// Recurse. (This is a postorder traversal.)
for kid in kids.into_iter().rev() {
self.build_from(graph, kid, mask);
}

// Finally, push our index.
self.threaded_graph.push(node_index);
}
}
Loading