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]) + } +}