From 62ae01cfe5f8ed9f8b967d7ee01d3a7d776e9df7 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 14 Sep 2024 11:10:48 +0100 Subject: [PATCH 1/4] Add additive node API --- crates/bevy_animation/src/graph.rs | 167 ++++++++++++++++++++++++++--- 1 file changed, 153 insertions(+), 14 deletions(-) diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 6f04ca794f958..cf6871f1e8467 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -33,23 +33,23 @@ use crate::{AnimationClip, AnimationTargetId}; /// For example, consider the following graph: /// /// ```text -/// ┌────────────┐ -/// │ │ -/// │ Idle ├─────────────────────┐ -/// │ │ │ -/// └────────────┘ │ -/// │ +/// ┌────────────┐ +/// │ │ +/// │ Idle ├─────────────────────┐ +/// │ │ │ +/// └────────────┘ │ +/// │ /// ┌────────────┐ │ ┌────────────┐ /// │ │ │ │ │ /// │ Run ├──┐ ├──┤ Root │ /// │ │ │ ┌────────────┐ │ │ │ /// └────────────┘ │ │ Blend │ │ └────────────┘ -/// ├──┤ ├──┘ -/// ┌────────────┐ │ │ 0.5 │ -/// │ │ │ └────────────┘ -/// │ Walk ├──┘ -/// │ │ -/// └────────────┘ +/// ├──┤ ├──┘ +/// ┌────────────┐ │ │ 0.5 │ +/// │ │ │ └────────────┘ +/// │ Walk ├──┘ +/// │ │ +/// └────────────┘ /// ``` /// /// In this case, assuming that Idle, Run, and Walk are all playing with weight @@ -147,6 +147,16 @@ pub struct AnimationGraphNode { /// 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, + + /// Whether animations under this node will be applied additively or + /// blended. + /// + /// If a node is additive, it will not share weight with other nodes in + /// the graph. This is useful for animations which should be played on top + /// of other animations, rather than being blended together. + // TODO: what's the name for a non-additive node? "shared weight" node? + // TODO: what happens if you have a non-additive node under an additive node? + pub additive: bool, } /// An [`AssetLoader`] that can load [`AnimationGraph`]s as assets. @@ -202,6 +212,8 @@ pub struct SerializedAnimationGraphNode { pub mask: AnimationMask, /// Corresponds to the `weight` field on [`AnimationGraphNode`]. pub weight: f32, + /// Corresponds to the `additive` field on [`AnimationGraphNode`]. + pub additive: bool, } /// A version of `Handle` suitable for serializing as an asset. @@ -272,6 +284,11 @@ impl AnimationGraph { /// /// The animation clip will be the child of the given parent. The resulting /// node will have no mask. + /// + /// This animation node will share its weight with other nodes, which may + /// reduce how intense this animation is played. If you want this animation + /// to be unaffected by sharing weight, see + /// [`AnimationGraph::add_additive_clip`]. pub fn add_clip( &mut self, clip: Handle, @@ -282,6 +299,7 @@ impl AnimationGraph { clip: Some(clip), mask: 0, weight, + additive: false, }); self.graph.add_edge(parent, node_index, ()); node_index @@ -291,6 +309,11 @@ impl AnimationGraph { /// and mask, and returns its index. /// /// The animation clip will be the child of the given parent. + /// + /// This animation node will share its weight with other nodes, which may + /// reduce how intense this animation is played. If you want this animation + /// to be unaffected by sharing weight, see + /// [`AnimationGraph::add_additive_clip_with_mask`]. pub fn add_clip_with_mask( &mut self, clip: Handle, @@ -302,6 +325,55 @@ impl AnimationGraph { clip: Some(clip), mask, weight, + additive: false, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an [`AnimationClip`] to the animation graph as an additive node + /// with the given weight, and returns its index. + /// + /// The animation clip will be the child of the given parent. The resulting + /// node will have no mask. + /// + /// See [`AnimationGraphNode::additive`] for an explanation of how additive + /// nodes behave. + pub fn add_additive_clip( + &mut self, + clip: Handle, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: Some(clip), + mask: 0, + weight, + additive: true, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an [`AnimationClip`] to the animation graph as an additive node + /// with the given weight and mask, and returns its index. + /// + /// The animation clip will be the child of the given parent. + /// + /// See [`AnimationGraphNode::additive`] for an explanation of how additive + /// nodes behave. + pub fn add_additive_clip_with_mask( + &mut self, + clip: Handle, + mask: AnimationMask, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: Some(clip), + mask, + weight, + additive: true, }); self.graph.add_edge(parent, node_index, ()); node_index @@ -336,24 +408,35 @@ impl AnimationGraph { /// 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. + /// + /// This animation node will share its weight with other nodes, which may + /// reduce how intense this animation is played. If you want this animation + /// to be unaffected by sharing weight, see + /// [`AnimationGraph::add_additive_blend`]. pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { clip: None, mask: 0, weight, + additive: false, }); 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. + /// Adds a blend node to the animation graph with the given weight and mask, + /// 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`. + /// + /// This animation node will share its weight with other nodes, which may + /// reduce how intense this animation is played. If you want this animation + /// to be unaffected by sharing weight, see + /// [`AnimationGraph::add_additive_blend_with_mask`]. pub fn add_blend_with_mask( &mut self, mask: AnimationMask, @@ -364,6 +447,59 @@ impl AnimationGraph { clip: None, mask, weight, + additive: false, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an additive 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. + /// + /// See [`AnimationGraphNode::additive`] for an explanation of how additive + /// nodes behave. + pub fn add_additive_blend( + &mut self, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: None, + mask: 0, + weight, + additive: true, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an additive blend node to the animation graph with the given weight + /// and mask, 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`. + /// + /// See [`AnimationGraphNode::additive`] for an explanation of how additive + /// nodes behave. + pub fn add_additive_blend_with_mask( + &mut self, + mask: AnimationMask, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: None, + mask, + weight, + additive: true, }); self.graph.add_edge(parent, node_index, ()); node_index @@ -491,6 +627,7 @@ impl Default for AnimationGraphNode { clip: None, mask: 0, weight: 1.0, + additive: false, } } } @@ -536,6 +673,7 @@ impl AssetLoader for AnimationGraphAssetLoader { }), mask: serialized_node.mask, weight: serialized_node.weight, + additive: serialized_node.additive, }, |_, _| (), ), @@ -559,6 +697,7 @@ impl From for SerializedAnimationGraph { |_, node| SerializedAnimationGraphNode { weight: node.weight, mask: node.mask, + additive: node.additive, clip: node.clip.as_ref().map(|clip| match clip.path() { Some(path) => SerializedAnimationClip::AssetPath(path.clone()), None => SerializedAnimationClip::AssetId(clip.id()), From a19c7e8bfb717096bea58a7649f4343ef15da907 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Sat, 14 Sep 2024 11:13:59 +0100 Subject: [PATCH 2/4] Additive blending impl --- crates/bevy_animation/src/lib.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index c8f521fdbc5b4..f678757ce6d2d 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -906,9 +906,14 @@ pub fn animate_targets( continue; } - let Some(clip) = animation_graph - .get(animation_graph_node_index) - .and_then(|animation_graph_node| animation_graph_node.clip.as_ref()) + let Some(animation_graph_node) = animation_graph.get(animation_graph_node_index) + else { + continue; + }; + + let Some(clip) = animation_graph_node + .clip + .as_ref() .and_then(|animation_clip_handle| clips.get(animation_clip_handle)) else { continue; @@ -918,10 +923,15 @@ pub fn animate_targets( continue; }; - let weight = active_animation.computed_weight; - total_weight += weight; + let applied_weight = if animation_graph_node.additive { + active_animation.computed_weight + } else { + let weight = active_animation.computed_weight; + total_weight += weight; + weight / total_weight + }; - target_context.apply(curves, weight / total_weight, active_animation.seek_time); + target_context.apply(curves, applied_weight, active_animation.seek_time); } }); } From b89fdd9989682c507275045ba0eaced73195bb57 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Sep 2024 09:52:08 +0100 Subject: [PATCH 3/4] Update fox animgraph --- assets/animation_graphs/Fox.animgraph.ron | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index cf87b1400e3b2..21bbec3a0b012 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -5,26 +5,31 @@ clip: None, mask: 0, weight: 1.0, + additive: false, ), ( clip: None, mask: 0, weight: 0.5, + additive: false, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation0")), mask: 0, weight: 1.0, + additive: false, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation1")), mask: 0, weight: 1.0, + additive: false, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation2")), mask: 0, weight: 1.0, + additive: false, ), ], node_holes: [], @@ -38,4 +43,4 @@ ), root: 0, mask_groups: {}, -) \ No newline at end of file +) From 772f66ac306ad85569743d229c820b19954cf1c5 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Wed, 18 Sep 2024 12:00:59 +0100 Subject: [PATCH 4/4] Add additive checkboxes to animation graph example --- examples/animation/animation_graph.rs | 156 +++++++++++++++++++++----- 1 file changed, 130 insertions(+), 26 deletions(-) diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 50423e21fb4fa..2d25265439cfd 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -31,7 +31,8 @@ static ANIMATION_GRAPH_PATH: &str = "animation_graphs/Fox.animgraph.ron"; 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"; +static HELP_TEXT: &str = "Click and drag an animation clip node to change its weight +Click the checkbox to toggle between additive and shared mode"; /// The node widgets in the UI. static NODE_TYPES: [NodeType; 5] = [ @@ -88,7 +89,14 @@ fn main() { .add_systems(Update, init_animations.before(animate_targets)) .add_systems( Update, - (handle_weight_drag, update_ui, sync_weights).chain(), + ( + handle_weight_drag, + handle_additive_toggle, + update_ui, + sync_weights, + sync_additive_mode, + ) + .chain(), ) .insert_resource(args) .insert_resource(AmbientLight { @@ -114,11 +122,17 @@ struct Args { #[derive(Clone, Resource)] struct ExampleAnimationGraph(Handle); -/// The current weights of the three playing animations. +/// The current states of the three playing animations. #[derive(Component)] -struct ExampleAnimationWeights { - /// The weights of the three playing animations. - weights: [f32; 3], +struct ExampleAnimationStates([ExampleAnimationState; 3]); + +/// The current state of a single animation clip node. +#[derive(Clone, Copy)] +struct ExampleAnimationState { + /// Weight of the clip node. + weight: f32, + /// Whether this node is additive or not. + additive: bool, } /// Initializes the scene. @@ -320,8 +334,8 @@ fn setup_node_rects(commands: &mut Commands) { container.id() }; - // Create the background color. - if let NodeType::Clip(_) = node_type { + if let NodeType::Clip(ref clip) = node_type { + // Create the background color. let background = commands .spawn(NodeBundle { style: Style { @@ -338,6 +352,29 @@ fn setup_node_rects(commands: &mut Commands) { .id(); commands.entity(container).add_child(background); + + // Create the additive toggle checkbox. + let additive_toggle = commands + .spawn(( + NodeBundle { + style: Style { + position_type: PositionType::Absolute, + bottom: Val::Px(5.), + left: Val::Px(5.), + height: Val::Px(20.), + width: Val::Px(20.), + ..default() + }, + ..default() + }, + Outline::new(Val::Px(1.), Val::ZERO, Color::WHITE), + Interaction::None, + clip.clone(), + AdditiveModeCheckbox, + )) + .id(); + + commands.entity(container).add_child(additive_toggle); } commands.entity(container).add_child(text); @@ -394,10 +431,9 @@ fn init_animations( } for (entity, mut player) in query.iter_mut() { - commands.entity(entity).insert(( - animation_graph.0.clone(), - ExampleAnimationWeights::default(), - )); + commands + .entity(entity) + .insert((animation_graph.0.clone(), ExampleAnimationStates::default())); for &node_index in &CLIP_NODE_INDICES { player.play(node_index.into()).repeat(); } @@ -409,10 +445,13 @@ fn init_animations( /// 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>, + interaction_query: Query< + (&Interaction, &RelativeCursorPosition, &ClipNode), + Without, + >, + mut animation_states_query: Query<&mut ExampleAnimationStates>, ) { - for (interaction, relative_cursor, clip_node) in &mut interaction_query { + for (interaction, relative_cursor, clip_node) in &interaction_query { if !matches!(*interaction, Interaction::Pressed) { continue; } @@ -421,8 +460,29 @@ fn handle_weight_drag( continue; }; - for mut animation_weights in animation_weights_query.iter_mut() { - animation_weights.weights[clip_node.index] = pos.x.clamp(0., 1.); + for mut animation_states in animation_states_query.iter_mut() { + animation_states.0[clip_node.index].weight = pos.x.clamp(0., 1.); + } + } +} + +/// Reads interactions with the additive mode check boxes, allowing the user to +/// change whether a specific node is additive or not. +fn handle_additive_toggle( + interaction_query: Query< + (&Interaction, &ClipNode), + (Changed, With), + >, + mut animation_states_query: Query<&mut ExampleAnimationStates>, +) { + for (interaction, clip_node) in &interaction_query { + if !matches!(*interaction, Interaction::Pressed) { + continue; + } + + for mut animation_states in animation_states_query.iter_mut() { + let state = &mut animation_states.0[clip_node.index]; + state.additive = !state.additive; } } } @@ -431,8 +491,9 @@ fn handle_weight_drag( fn update_ui( mut text_query: Query<&mut Text>, mut background_query: Query<&mut Style, Without>, + mut additive_toggle_query: Query<&mut BackgroundColor, (Without, With)>, container_query: Query<(&Children, &ClipNode)>, - animation_weights_query: Query<&ExampleAnimationWeights, Changed>, + animation_weights_query: Query<&ExampleAnimationStates, Changed>, ) { for animation_weights in animation_weights_query.iter() { for (children, clip_node) in &container_query { @@ -441,7 +502,17 @@ fn update_ui( 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]); + Val::Px(NODE_RECTS[0].width * animation_weights.0[clip_node.index].weight); + } + + // Update the background of the additive checkbox. + let mut additive_toggle_iter = additive_toggle_query.iter_many_mut(children); + if let Some(mut bg_color) = additive_toggle_iter.fetch_next() { + *bg_color = if animation_weights.0[clip_node.index].additive { + WHITE.into() + } else { + Color::NONE.into() + }; } // Update the node labels with the current weights. @@ -449,7 +520,7 @@ fn update_ui( 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] + clip_node.text, animation_weights.0[clip_node.index].weight ); } } @@ -458,11 +529,11 @@ fn update_ui( /// 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 +fn sync_weights(mut query: Query<(&mut AnimationPlayer, &ExampleAnimationStates)>) { + for (mut animation_player, animation_states) in query.iter_mut() { + for (&animation_node_index, animation_weight) in CLIP_NODE_INDICES .iter() - .zip(animation_weights.weights.iter()) + .zip(animation_states.0.iter().map(|state| state.weight)) { // If the animation happens to be no longer active, restart it. if !animation_player.animation_is_playing(animation_node_index.into()) { @@ -479,6 +550,30 @@ fn sync_weights(mut query: Query<(&mut AnimationPlayer, &ExampleAnimationWeights } } +/// Takes the state of the additive checkboxes in the UI and assigns them to the +/// actual nodes in the animation graph. +fn sync_additive_mode( + query: Query<(&Handle, &ExampleAnimationStates)>, + mut animation_graphs: ResMut>, +) { + for (animation_graph_handle, animation_states) in query.iter() { + let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else { + continue; + }; + + for (&animation_node_index, additive) in CLIP_NODE_INDICES + .iter() + .zip(animation_states.0.iter().map(|state| state.additive)) + { + let animation_node_index = AnimationNodeIndex::from(animation_node_index); + let animation_node = animation_graph.get_mut(animation_node_index).unwrap(); + + // Set the additive mode of this node. + animation_node.additive = additive; + } + } +} + /// An on-screen representation of a node. #[derive(Debug)] struct NodeRect { @@ -526,9 +621,18 @@ struct ClipNode { index: usize, } -impl Default for ExampleAnimationWeights { +/// Marker component for UI nodes which represent the additive toggle check box. +#[derive(Component)] +struct AdditiveModeCheckbox; + +impl Default for ExampleAnimationStates { fn default() -> Self { - Self { weights: [1.0; 3] } + Self( + [ExampleAnimationState { + weight: 1.0, + additive: false, + }; 3], + ) } }