diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron
index cf87b1400e..e9d6f4f9cf 100644
--- a/assets/animation_graphs/Fox.animgraph.ron
+++ b/assets/animation_graphs/Fox.animgraph.ron
@@ -2,27 +2,27 @@
graph: (
nodes: [
(
- clip: None,
+ node_type: Blend,
mask: 0,
weight: 1.0,
),
(
- clip: None,
- mask: 0,
- weight: 0.5,
- ),
- (
- clip: Some(AssetPath("models/animated/Fox.glb#Animation0")),
+ node_type: Blend,
mask: 0,
weight: 1.0,
),
(
- clip: Some(AssetPath("models/animated/Fox.glb#Animation1")),
+ node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")),
mask: 0,
weight: 1.0,
),
(
- clip: Some(AssetPath("models/animated/Fox.glb#Animation2")),
+ node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")),
+ mask: 0,
+ weight: 1.0,
+ ),
+ (
+ node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")),
mask: 0,
weight: 1.0,
),
diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs
index c0101ad5b8..e4a6e46734 100644
--- a/crates/bevy_animation/src/animation_curves.rs
+++ b/crates/bevy_animation/src/animation_curves.rs
@@ -96,7 +96,9 @@ use bevy_render::mesh::morph::MorphWeights;
use bevy_transform::prelude::Transform;
use crate::{
- graph::AnimationNodeIndex, prelude::Animatable, AnimationEntityMut, AnimationEvaluationError,
+ graph::AnimationNodeIndex,
+ prelude::{Animatable, BlendInput},
+ AnimationEntityMut, AnimationEvaluationError,
};
/// A value on a component that Bevy can animate.
@@ -297,7 +299,11 @@ where
P: AnimatableProperty,
{
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
- self.evaluator.blend(graph_node)
+ self.evaluator.combine(graph_node, /*additive=*/ false)
+ }
+
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.evaluator.combine(graph_node, /*additive=*/ true)
}
fn push_blend_register(
@@ -393,7 +399,11 @@ where
impl AnimationCurveEvaluator for TranslationCurveEvaluator {
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
- self.evaluator.blend(graph_node)
+ self.evaluator.combine(graph_node, /*additive=*/ false)
+ }
+
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.evaluator.combine(graph_node, /*additive=*/ true)
}
fn push_blend_register(
@@ -487,7 +497,11 @@ where
impl AnimationCurveEvaluator for RotationCurveEvaluator {
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
- self.evaluator.blend(graph_node)
+ self.evaluator.combine(graph_node, /*additive=*/ false)
+ }
+
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.evaluator.combine(graph_node, /*additive=*/ true)
}
fn push_blend_register(
@@ -581,7 +595,11 @@ where
impl AnimationCurveEvaluator for ScaleCurveEvaluator {
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
- self.evaluator.blend(graph_node)
+ self.evaluator.combine(graph_node, /*additive=*/ false)
+ }
+
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.evaluator.combine(graph_node, /*additive=*/ true)
}
fn push_blend_register(
@@ -708,8 +726,12 @@ where
}
}
-impl AnimationCurveEvaluator for WeightsCurveEvaluator {
- fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+impl WeightsCurveEvaluator {
+ fn combine(
+ &mut self,
+ graph_node: AnimationNodeIndex,
+ additive: bool,
+ ) -> Result<(), AnimationEvaluationError> {
let Some(&(_, top_graph_node)) = self.stack_blend_weights_and_graph_nodes.last() else {
return Ok(());
};
@@ -736,13 +758,27 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator {
.iter_mut()
.zip(stack_iter)
{
- *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight);
+ if additive {
+ *dest += src * weight_to_blend;
+ } else {
+ *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight);
+ }
}
}
}
Ok(())
}
+}
+
+impl AnimationCurveEvaluator for WeightsCurveEvaluator {
+ fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.combine(graph_node, /*additive=*/ false)
+ }
+
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ self.combine(graph_node, /*additive=*/ true)
+ }
fn push_blend_register(
&mut self,
@@ -826,7 +862,11 @@ impl BasicAnimationCurveEvaluator
where
A: Animatable,
{
- fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
+ fn combine(
+ &mut self,
+ graph_node: AnimationNodeIndex,
+ additive: bool,
+ ) -> Result<(), AnimationEvaluationError> {
let Some(top) = self.stack.last() else {
return Ok(());
};
@@ -840,15 +880,36 @@ where
graph_node: _,
} = self.stack.pop().unwrap();
- match self.blend_register {
+ match self.blend_register.take() {
None => self.blend_register = Some((value_to_blend, weight_to_blend)),
- Some((ref mut current_value, ref mut current_weight)) => {
- *current_weight += weight_to_blend;
- *current_value = A::interpolate(
- current_value,
- &value_to_blend,
- weight_to_blend / *current_weight,
- );
+ Some((mut current_value, mut current_weight)) => {
+ current_weight += weight_to_blend;
+
+ if additive {
+ current_value = A::blend(
+ [
+ BlendInput {
+ weight: 1.0,
+ value: current_value,
+ additive: true,
+ },
+ BlendInput {
+ weight: weight_to_blend,
+ value: value_to_blend,
+ additive: true,
+ },
+ ]
+ .into_iter(),
+ );
+ } else {
+ current_value = A::interpolate(
+ ¤t_value,
+ &value_to_blend,
+ weight_to_blend / current_weight,
+ );
+ }
+
+ self.blend_register = Some((current_value, current_weight));
}
}
@@ -967,6 +1028,22 @@ pub trait AnimationCurveEvaluator: Reflect {
/// 4. Return success.
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>;
+ /// Additively blends the top element of the stack with the blend register.
+ ///
+ /// The semantics of this method are as follows:
+ ///
+ /// 1. Pop the top element of the stack. Call its value vₘ and its weight
+ /// wₘ. If the stack was empty, return success.
+ ///
+ /// 2. If the blend register is empty, set the blend register value to vₘ
+ /// and the blend register weight to wₘ; then, return success.
+ ///
+ /// 3. If the blend register is nonempty, call its current value vₙ.
+ /// Then, set the value of the blend register to vₙ + vₘwₘ.
+ ///
+ /// 4. Return success.
+ fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>;
+
/// Pushes the current value of the blend register onto the stack.
///
/// If the blend register is empty, this method does nothing successfully.
diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs
index 22c0e1a608..6121ecb2ab 100644
--- a/crates/bevy_animation/src/graph.rs
+++ b/crates/bevy_animation/src/graph.rs
@@ -35,11 +35,12 @@ use crate::{AnimationClip, AnimationTargetId};
/// 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.
+/// There are three types of nodes: *blend nodes*, *add nodes*, and *clip
+/// nodes*, all of which can have an associated weight. Blend nodes and add
+/// nodes have no associated animation clip and combine the animations of their
+/// children according to those children's weights. 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:
///
@@ -133,16 +134,19 @@ pub type AnimationNodeIndex = NodeIndex;
/// An individual node within an animation graph.
///
-/// If `clip` is present, this is a *clip node*. Otherwise, it's a *blend node*.
-/// Both clip and blend nodes can have weights, and those weights are propagated
-/// down to descendants.
+/// The [`AnimationGraphNode::node_type`] field specifies the type of node: one
+/// of a *clip node*, a *blend node*, or an *add node*. Clip nodes, the leaves
+/// of the graph, contain animation clips to play. Blend and add nodes describe
+/// how to combine their children to produce a final animation. The difference
+/// between blend nodes and add nodes is that blend nodes normalize the weights
+/// of their children to 1.0, while add nodes don't.
#[derive(Clone, Reflect, Debug)]
pub struct AnimationGraphNode {
- /// The animation clip associated with this node, if any.
+ /// Animation node data specific to the type of node (clip, blend, or add).
///
- /// If the clip is present, this node is an *animation clip node*.
- /// Otherwise, this node is a *blend node*.
- pub clip: Option>,
+ /// In the case of clip nodes, this contains the actual animation clip
+ /// associated with the node.
+ pub node_type: AnimationNodeType,
/// A bitfield specifying the mask groups that this node and its descendants
/// will not affect.
@@ -155,11 +159,42 @@ pub struct AnimationGraphNode {
/// 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.
+ /// has weight 0.3 and its parent blend node has effective weight 0.6, the
+ /// computed weight of the animation clip is 0.18.
pub weight: f32,
}
+/// Animation node data specific to the type of node (clip, blend, or add).
+///
+/// In the case of clip nodes, this contains the actual animation clip
+/// associated with the node.
+#[derive(Clone, Default, Reflect, Debug)]
+pub enum AnimationNodeType {
+ /// A *clip node*, which plays an animation clip.
+ ///
+ /// These are always the leaves of the graph.
+ Clip(Handle),
+
+ /// A *blend node*, which blends its children according to their weights.
+ ///
+ /// The weights of all the children of this node are normalized to 1.0.
+ #[default]
+ Blend,
+
+ /// An *additive blend node*, which combines the animations of its children,
+ /// scaled by their weights.
+ ///
+ /// The weights of all the children of this node are *not* normalized to
+ /// 1.0.
+ ///
+ /// Add nodes are primarily useful for superimposing an animation for a
+ /// portion of a rig on top of the main animation. For example, an add node
+ /// could superimpose a weapon attack animation for a character's limb on
+ /// top of a running animation to produce an animation of a character
+ /// attacking while running.
+ Add,
+}
+
/// An [`AssetLoader`] that can load [`AnimationGraph`]s as assets.
///
/// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain
@@ -300,14 +335,26 @@ pub struct SerializedAnimationGraph {
/// See the comments in [`SerializedAnimationGraph`] for more information.
#[derive(Serialize, Deserialize)]
pub struct SerializedAnimationGraphNode {
- /// Corresponds to the `clip` field on [`AnimationGraphNode`].
- pub clip: Option,
+ /// Corresponds to the `node_type` field on [`AnimationGraphNode`].
+ pub node_type: SerializedAnimationNodeType,
/// Corresponds to the `mask` field on [`AnimationGraphNode`].
pub mask: AnimationMask,
/// Corresponds to the `weight` field on [`AnimationGraphNode`].
pub weight: f32,
}
+/// A version of [`AnimationNodeType`] suitable for serializing as part of a
+/// [`SerializedAnimationGraphNode`] asset.
+#[derive(Serialize, Deserialize)]
+pub enum SerializedAnimationNodeType {
+ /// Corresponds to [`AnimationNodeType::Clip`].
+ Clip(SerializedAnimationClip),
+ /// Corresponds to [`AnimationNodeType::Blend`].
+ Blend,
+ /// Corresponds to [`AnimationNodeType::Add`].
+ Add,
+}
+
/// A version of `Handle` suitable for serializing as an asset.
///
/// This replaces any handle that has a path with an [`AssetPath`]. Failing
@@ -383,7 +430,7 @@ impl AnimationGraph {
parent: AnimationNodeIndex,
) -> AnimationNodeIndex {
let node_index = self.graph.add_node(AnimationGraphNode {
- clip: Some(clip),
+ node_type: AnimationNodeType::Clip(clip),
mask: 0,
weight,
});
@@ -403,7 +450,7 @@ impl AnimationGraph {
parent: AnimationNodeIndex,
) -> AnimationNodeIndex {
let node_index = self.graph.add_node(AnimationGraphNode {
- clip: Some(clip),
+ node_type: AnimationNodeType::Clip(clip),
mask,
weight,
});
@@ -442,7 +489,7 @@ impl AnimationGraph {
/// no mask.
pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex {
let node_index = self.graph.add_node(AnimationGraphNode {
- clip: None,
+ node_type: AnimationNodeType::Blend,
mask: 0,
weight,
});
@@ -465,7 +512,51 @@ impl AnimationGraph {
parent: AnimationNodeIndex,
) -> AnimationNodeIndex {
let node_index = self.graph.add_node(AnimationGraphNode {
- clip: None,
+ node_type: AnimationNodeType::Blend,
+ mask,
+ weight,
+ });
+ self.graph.add_edge(parent, node_index, ());
+ node_index
+ }
+
+ /// 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. The blend node will have
+ /// no mask.
+ pub fn add_additive_blend(
+ &mut self,
+ weight: f32,
+ parent: AnimationNodeIndex,
+ ) -> AnimationNodeIndex {
+ let node_index = self.graph.add_node(AnimationGraphNode {
+ node_type: AnimationNodeType::Add,
+ mask: 0,
+ weight,
+ });
+ self.graph.add_edge(parent, node_index, ());
+ node_index
+ }
+
+ /// 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. Neither this node nor its
+ /// descendants will affect animation targets that belong to mask groups not
+ /// in the given `mask`.
+ pub fn add_additive_blend_with_mask(
+ &mut self,
+ mask: AnimationMask,
+ weight: f32,
+ parent: AnimationNodeIndex,
+ ) -> AnimationNodeIndex {
+ let node_index = self.graph.add_node(AnimationGraphNode {
+ node_type: AnimationNodeType::Add,
mask,
weight,
});
@@ -592,7 +683,7 @@ impl IndexMut for AnimationGraph {
impl Default for AnimationGraphNode {
fn default() -> Self {
Self {
- clip: None,
+ node_type: Default::default(),
mask: 0,
weight: 1.0,
}
@@ -632,12 +723,18 @@ impl AssetLoader for AnimationGraphAssetLoader {
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)
- }
- }),
+ node_type: match serialized_node.node_type {
+ SerializedAnimationNodeType::Clip(ref clip) => match clip {
+ SerializedAnimationClip::AssetId(asset_id) => {
+ AnimationNodeType::Clip(Handle::Weak(*asset_id))
+ }
+ SerializedAnimationClip::AssetPath(asset_path) => {
+ AnimationNodeType::Clip(load_context.load(asset_path))
+ }
+ },
+ SerializedAnimationNodeType::Blend => AnimationNodeType::Blend,
+ SerializedAnimationNodeType::Add => AnimationNodeType::Add,
+ },
mask: serialized_node.mask,
weight: serialized_node.weight,
},
@@ -663,10 +760,18 @@ impl From for SerializedAnimationGraph {
|_, node| SerializedAnimationGraphNode {
weight: node.weight,
mask: node.mask,
- clip: node.clip.as_ref().map(|clip| match clip.path() {
- Some(path) => SerializedAnimationClip::AssetPath(path.clone()),
- None => SerializedAnimationClip::AssetId(clip.id()),
- }),
+ node_type: match node.node_type {
+ AnimationNodeType::Clip(ref clip) => match clip.path() {
+ Some(path) => SerializedAnimationNodeType::Clip(
+ SerializedAnimationClip::AssetPath(path.clone()),
+ ),
+ None => SerializedAnimationNodeType::Clip(
+ SerializedAnimationClip::AssetId(clip.id()),
+ ),
+ },
+ AnimationNodeType::Blend => SerializedAnimationNodeType::Blend,
+ AnimationNodeType::Add => SerializedAnimationNodeType::Add,
+ },
},
|_, _| (),
),
@@ -762,7 +867,7 @@ impl ThreadedAnimationGraph {
) {
// Accumulate the mask.
mask |= graph.node_weight(node_index).unwrap().mask;
- self.computed_masks.insert(node_index.index(), mask);
+ self.computed_masks[node_index.index()] = mask;
// Gather up the indices of our children, and sort them.
let mut kids: SmallVec<[AnimationNodeIndex; 8]> = graph
diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs
index 529377cab7..f4b24807c1 100755
--- a/crates/bevy_animation/src/lib.rs
+++ b/crates/bevy_animation/src/lib.rs
@@ -23,6 +23,7 @@ use core::{
hash::{Hash, Hasher},
iter,
};
+use graph::AnimationNodeType;
use prelude::AnimationCurveEvaluator;
use crate::graph::ThreadedAnimationGraphs;
@@ -478,8 +479,6 @@ pub enum AnimationEvaluationError {
pub struct ActiveAnimation {
/// The factor by which the weight from the [`AnimationGraph`] is multiplied.
weight: f32,
- /// The mask groups that are masked out (i.e. won't be animated) this frame,
- /// taking the `AnimationGraph` into account.
repeat: RepeatAnimation,
speed: f32,
/// Total time the animation has been played.
@@ -868,7 +867,7 @@ pub fn advance_animations(
if let Some(active_animation) = active_animations.get_mut(&node_index) {
// Tick the animation if necessary.
if !active_animation.paused {
- if let Some(ref clip_handle) = node.clip {
+ if let AnimationNodeType::Clip(ref clip_handle) = node.node_type {
if let Some(clip) = animation_clips.get(clip_handle) {
active_animation.update(delta_seconds, clip.duration);
}
@@ -951,8 +950,8 @@ pub fn animate_targets(
continue;
};
- match animation_graph_node.clip {
- None => {
+ match animation_graph_node.node_type {
+ AnimationNodeType::Blend | AnimationNodeType::Add => {
// This is a blend node.
for edge_index in threaded_animation_graph.sorted_edge_ranges
[animation_graph_node_index.index()]
@@ -973,7 +972,7 @@ pub fn animate_targets(
}
}
- Some(ref animation_clip_handle) => {
+ AnimationNodeType::Clip(ref animation_clip_handle) => {
// This is a clip node.
let Some(active_animation) = animation_player
.active_animations
diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs
index 203fb8ce9c..4a9177074d 100644
--- a/examples/animation/animation_masks.rs
+++ b/examples/animation/animation_masks.rs
@@ -1,23 +1,26 @@
//! Demonstrates how to use masks to limit the scope of animations.
-use bevy::{animation::AnimationTargetId, color::palettes::css::WHITE, prelude::*};
+use bevy::{
+ animation::{AnimationTarget, AnimationTargetId},
+ color::palettes::css::{LIGHT_GRAY, WHITE},
+ prelude::*,
+ utils::hashbrown::HashSet,
+};
// IDs of the mask groups we define for the running fox model.
//
// Each mask group defines a set of bones for which animations can be toggled on
// and off.
-const MASK_GROUP_LEFT_FRONT_LEG: u32 = 0;
-const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 1;
-const MASK_GROUP_LEFT_HIND_LEG: u32 = 2;
-const MASK_GROUP_RIGHT_HIND_LEG: u32 = 3;
-const MASK_GROUP_TAIL: u32 = 4;
+const MASK_GROUP_HEAD: u32 = 0;
+const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1;
+const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2;
+const MASK_GROUP_LEFT_HIND_LEG: u32 = 3;
+const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4;
+const MASK_GROUP_TAIL: u32 = 5;
// The width in pixels of the small buttons that allow the user to toggle a mask
// group on or off.
-const MASK_GROUP_SMALL_BUTTON_WIDTH: f32 = 150.0;
-
-// The ID of the animation in the glTF file that we're going to play.
-const FOX_RUN_ANIMATION: usize = 2;
+const MASK_GROUP_BUTTON_WIDTH: f32 = 250.0;
// The names of the bones that each mask group consists of. Each mask group is
// defined as a (prefix, suffix) tuple. The mask group consists of a single
@@ -25,11 +28,16 @@ const FOX_RUN_ANIMATION: usize = 2;
// "A/B/C" and the suffix is "D/E", then the bones that will be included in the
// mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E".
//
-// The fact that our mask groups are single chains of bones isn't anything
-// specific to Bevy; it just so happens to be the case for the model we're
-// using. A mask group can consist of any set of animation targets, regardless
-// of whether they form a single chain.
-const MASK_GROUP_PATHS: [(&str, &str); 5] = [
+// The fact that our mask groups are single chains of bones isn't an engine
+// requirement; it just so happens to be the case for the model we're using. A
+// mask group can consist of any set of animation targets, regardless of whether
+// they form a single chain.
+const MASK_GROUP_PATHS: [(&str, &str); 6] = [
+ // Head
+ (
+ "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03",
+ "b_Neck_04/b_Head_05",
+ ),
// Left front leg
(
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09",
@@ -57,19 +65,30 @@ const MASK_GROUP_PATHS: [(&str, &str); 5] = [
),
];
-// A component that identifies a clickable button that allows the user to toggle
-// a mask group on or off.
-#[derive(Component)]
-struct MaskGroupControl {
+#[derive(Clone, Copy, Component)]
+struct AnimationControl {
// The ID of the mask group that this button controls.
group_id: u32,
+ label: AnimationLabel,
+}
- // Whether animations are playing for this mask group.
- //
- // Note that this is the opposite of the `mask` field in `AnimationGraph`:
- // i.e. it's true if the group is *not* presently masked, and false if the
- // group *is* masked.
- enabled: bool,
+#[derive(Clone, Copy, Component, PartialEq, Debug)]
+enum AnimationLabel {
+ Idle = 0,
+ Walk = 1,
+ Run = 2,
+ Off = 3,
+}
+
+#[derive(Clone, Debug, Resource)]
+struct AnimationNodes([AnimationNodeIndex; 3]);
+
+#[derive(Clone, Copy, Debug, Resource)]
+struct AppState([MaskGroupState; 6]);
+
+#[derive(Clone, Copy, Debug)]
+struct MaskGroupState {
+ clip: u8,
}
// The application entry point.
@@ -85,10 +104,12 @@ fn main() {
.add_systems(Startup, (setup_scene, setup_ui))
.add_systems(Update, setup_animation_graph_once_loaded)
.add_systems(Update, handle_button_toggles)
+ .add_systems(Update, update_ui)
.insert_resource(AmbientLight {
color: WHITE.into(),
brightness: 100.0,
})
+ .init_resource::()
.run();
}
@@ -169,6 +190,8 @@ fn setup_ui(mut commands: Commands) {
..default()
};
+ add_mask_group_control(parent, "Head", Val::Auto, MASK_GROUP_HEAD);
+
parent
.spawn(NodeBundle {
style: row_style.clone(),
@@ -178,13 +201,13 @@ fn setup_ui(mut commands: Commands) {
add_mask_group_control(
parent,
"Left Front Leg",
- Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
+ Val::Px(MASK_GROUP_BUTTON_WIDTH),
MASK_GROUP_LEFT_FRONT_LEG,
);
add_mask_group_control(
parent,
"Right Front Leg",
- Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
+ Val::Px(MASK_GROUP_BUTTON_WIDTH),
MASK_GROUP_RIGHT_FRONT_LEG,
);
});
@@ -198,13 +221,13 @@ fn setup_ui(mut commands: Commands) {
add_mask_group_control(
parent,
"Left Hind Leg",
- Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
+ Val::Px(MASK_GROUP_BUTTON_WIDTH),
MASK_GROUP_LEFT_HIND_LEG,
);
add_mask_group_control(
parent,
"Right Hind Leg",
- Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
+ Val::Px(MASK_GROUP_BUTTON_WIDTH),
MASK_GROUP_RIGHT_HIND_LEG,
);
});
@@ -218,34 +241,129 @@ fn setup_ui(mut commands: Commands) {
// The button will automatically become a child of the parent that owns the
// given `ChildBuilder`.
fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, mask_group_id: u32) {
+ let button_text_style = TextStyle {
+ font_size: 14.0,
+ color: Color::WHITE,
+ ..default()
+ };
+ let selected_button_text_style = TextStyle {
+ color: Color::BLACK,
+ ..button_text_style.clone()
+ };
+ let label_text_style = TextStyle {
+ color: Color::Srgba(LIGHT_GRAY),
+ ..button_text_style.clone()
+ };
+
parent
- .spawn(ButtonBundle {
+ .spawn(NodeBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
width,
+ flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
- padding: UiRect::all(Val::Px(6.0)),
+ padding: UiRect::ZERO,
margin: UiRect::ZERO,
..default()
},
border_color: BorderColor(Color::WHITE),
border_radius: BorderRadius::all(Val::Px(3.0)),
- background_color: Color::WHITE.into(),
+ background_color: Color::BLACK.into(),
..default()
})
- .insert(MaskGroupControl {
- group_id: mask_group_id,
- enabled: true,
- })
- .with_child(TextBundle::from_section(
- label,
- TextStyle {
- font_size: 14.0,
- color: Color::BLACK,
- ..default()
- },
- ));
+ .with_children(|builder| {
+ builder
+ .spawn(NodeBundle {
+ style: Style {
+ border: UiRect::ZERO,
+ width: Val::Percent(100.0),
+ justify_content: JustifyContent::Center,
+ align_items: AlignItems::Center,
+ padding: UiRect::ZERO,
+ margin: UiRect::ZERO,
+ ..default()
+ },
+ background_color: Color::BLACK.into(),
+ ..default()
+ })
+ .with_child(TextBundle {
+ text: Text::from_section(label, label_text_style.clone()),
+ style: Style {
+ margin: UiRect::vertical(Val::Px(3.0)),
+ ..default()
+ },
+ ..default()
+ });
+
+ builder
+ .spawn(NodeBundle {
+ style: Style {
+ width: Val::Percent(100.0),
+ flex_direction: FlexDirection::Row,
+ justify_content: JustifyContent::Center,
+ align_items: AlignItems::Center,
+ border: UiRect::top(Val::Px(1.0)),
+ ..default()
+ },
+ border_color: BorderColor(Color::WHITE),
+ ..default()
+ })
+ .with_children(|builder| {
+ for (index, label) in [
+ AnimationLabel::Run,
+ AnimationLabel::Walk,
+ AnimationLabel::Idle,
+ AnimationLabel::Off,
+ ]
+ .iter()
+ .enumerate()
+ {
+ builder
+ .spawn(ButtonBundle {
+ background_color: if index > 0 {
+ Color::BLACK.into()
+ } else {
+ Color::WHITE.into()
+ },
+ style: Style {
+ flex_grow: 1.0,
+ border: if index > 0 {
+ UiRect::left(Val::Px(1.0))
+ } else {
+ UiRect::ZERO
+ },
+ ..default()
+ },
+ border_color: BorderColor(Color::WHITE),
+ ..default()
+ })
+ .with_child(
+ TextBundle {
+ style: Style {
+ flex_grow: 1.0,
+ margin: UiRect::vertical(Val::Px(3.0)),
+ ..default()
+ },
+ text: Text::from_section(
+ format!("{:?}", label),
+ if index > 0 {
+ button_text_style.clone()
+ } else {
+ selected_button_text_style.clone()
+ },
+ ),
+ ..default()
+ }
+ .with_text_justify(JustifyText::Center),
+ )
+ .insert(AnimationControl {
+ group_id: mask_group_id,
+ label: *label,
+ });
+ }
+ });
+ });
}
// Builds up the animation graph, including the mask groups, and adds it to the
@@ -255,14 +373,25 @@ fn setup_animation_graph_once_loaded(
asset_server: Res,
mut animation_graphs: ResMut>,
mut players: Query<(Entity, &mut AnimationPlayer), Added>,
+ targets: Query<(Entity, &AnimationTarget)>,
) {
for (entity, mut player) in &mut players {
// Load the animation clip from the glTF file.
- let (mut animation_graph, node_index) = AnimationGraph::from_clip(asset_server.load(
- GltfAssetLabel::Animation(FOX_RUN_ANIMATION).from_asset("models/animated/Fox.glb"),
- ));
+ let mut animation_graph = AnimationGraph::new();
+ let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root);
+
+ let animation_graph_nodes: [AnimationNodeIndex; 3] =
+ std::array::from_fn(|animation_index| {
+ let handle = asset_server.load(
+ GltfAssetLabel::Animation(animation_index)
+ .from_asset("models/animated/Fox.glb"),
+ );
+ let mask = if animation_index == 0 { 0 } else { 0x3f };
+ animation_graph.add_clip_with_mask(handle, mask, 0.0, blend_node)
+ });
// Create each mask group.
+ let mut all_animation_target_ids = HashSet::new();
for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in
MASK_GROUP_PATHS.iter().enumerate()
{
@@ -277,6 +406,7 @@ fn setup_animation_graph_once_loaded(
);
animation_graph
.add_target_to_mask_group(animation_target_id, mask_group_index as u32);
+ all_animation_target_ids.insert(animation_target_id);
}
}
@@ -284,81 +414,100 @@ fn setup_animation_graph_once_loaded(
let animation_graph = animation_graphs.add(animation_graph);
commands.entity(entity).insert(animation_graph);
- // Finally, play the animation.
- player.play(node_index).repeat();
+ // Remove animation targets that aren't in any of the mask groups. If we
+ // don't do that, those bones will play all animations at once, which is
+ // ugly.
+ for (target_entity, target) in &targets {
+ if !all_animation_target_ids.contains(&target.id) {
+ commands.entity(target_entity).remove::();
+ }
+ }
+
+ // Play the animation.
+ for animation_graph_node in animation_graph_nodes {
+ player.play(animation_graph_node).repeat();
+ }
+
+ // Record the graph nodes.
+ commands.insert_resource(AnimationNodes(animation_graph_nodes));
}
}
// A system that handles requests from the user to toggle mask groups on and
// off.
fn handle_button_toggles(
- mut interactions: Query<
- (
- &Interaction,
- &mut MaskGroupControl,
- &mut BackgroundColor,
- &Children,
- ),
- Changed,
- >,
- mut texts: Query<&mut Text>,
- mut animation_players: Query<(&Handle, &AnimationPlayer)>,
+ mut interactions: Query<(&Interaction, &mut AnimationControl), Changed>,
+ mut animation_players: Query<&Handle, With>,
mut animation_graphs: ResMut>,
+ mut animation_nodes: Option>,
+ mut app_state: ResMut,
) {
- for (interaction, mut mask_group_control, mut button_background_color, children) in
- interactions.iter_mut()
- {
+ let Some(ref mut animation_nodes) = animation_nodes else {
+ return;
+ };
+
+ for (interaction, animation_control) in interactions.iter_mut() {
// We only care about press events.
if *interaction != Interaction::Pressed {
continue;
}
- // Toggle the state of the mask.
- mask_group_control.enabled = !mask_group_control.enabled;
-
- // Update the background color of the button.
- button_background_color.0 = if mask_group_control.enabled {
- Color::WHITE
- } else {
- Color::BLACK
- };
-
- // Update the text color of the button.
- for &kid in children.iter() {
- if let Ok(mut text) = texts.get_mut(kid) {
- for section in &mut text.sections {
- section.style.color = if mask_group_control.enabled {
- Color::BLACK
- } else {
- Color::WHITE
- };
- }
- }
- }
+ // Toggle the state of the clip.
+ app_state.0[animation_control.group_id as usize].clip = animation_control.label as u8;
// Now grab the animation player. (There's only one in our case, but we
// iterate just for clarity's sake.)
- for (animation_graph_handle, animation_player) in animation_players.iter_mut() {
+ for animation_graph_handle in animation_players.iter_mut() {
// The animation graph needs to have loaded.
let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else {
continue;
};
- // Grab the animation graph node that's currently playing.
- let Some((&animation_node_index, _)) = animation_player.playing_animations().next()
- else {
- continue;
- };
- let Some(animation_node) = animation_graph.get_mut(animation_node_index) else {
- continue;
- };
+ for (clip_index, &animation_node_index) in animation_nodes.0.iter().enumerate() {
+ let Some(animation_node) = animation_graph.get_mut(animation_node_index) else {
+ continue;
+ };
- // Enable or disable the mask group as appropriate.
- if mask_group_control.enabled {
- animation_node.mask &= !(1 << mask_group_control.group_id);
- } else {
- animation_node.mask |= 1 << mask_group_control.group_id;
+ if animation_control.label as usize == clip_index {
+ animation_node.mask &= !(1 << animation_control.group_id);
+ } else {
+ animation_node.mask |= 1 << animation_control.group_id;
+ }
}
}
}
}
+
+// A system that updates the UI based on the current app state.
+fn update_ui(
+ mut animation_controls: Query<(&AnimationControl, &mut BackgroundColor, &Children)>,
+ mut texts: Query<&mut Text>,
+ app_state: Res,
+) {
+ for (animation_control, mut background_color, kids) in animation_controls.iter_mut() {
+ let enabled =
+ app_state.0[animation_control.group_id as usize].clip == animation_control.label as u8;
+
+ *background_color = if enabled {
+ BackgroundColor(Color::WHITE)
+ } else {
+ BackgroundColor(Color::BLACK)
+ };
+
+ for &kid in kids {
+ let Ok(mut text) = texts.get_mut(kid) else {
+ continue;
+ };
+
+ for section in &mut text.sections {
+ section.style.color = if enabled { Color::BLACK } else { Color::WHITE };
+ }
+ }
+ }
+}
+
+impl Default for AppState {
+ fn default() -> Self {
+ AppState([MaskGroupState { clip: 0 }; 6])
+ }
+}