Skip to content

Commit 0094bcb

Browse files
authored
Implement additive blending for animation graphs. (bevyengine#15631)
*Additive blending* is an ubiquitous feature in game engines that allows animations to be concatenated instead of blended. The canonical use case is to allow a character to hold a weapon while performing arbitrary poses. For example, if you had a character that needed to be able to walk or run while attacking with a weapon, the typical workflow is to have an additive blend node that combines walking and running animation clips with an animation clip of one of the limbs performing a weapon attack animation. This commit adds support for additive blending to Bevy. It builds on top of the flexible infrastructure in bevyengine#15589 and introduces a new type of node, the *add node*. Like blend nodes, add nodes combine the animations of their children according to their weights. Unlike blend nodes, however, add nodes don't normalize the weights to 1.0. The `animation_masks` example has been overhauled to demonstrate the use of additive blending in combination with masks. There are now controls to choose an animation clip for every limb of the fox individually. This patch also fixes a bug whereby masks were incorrectly accumulated with `insert()` during the graph threading phase, which could cause corruption of computed masks in some cases. Note that the `clip` field has been replaced with an `AnimationNodeType` enum, which breaks `animgraph.ron` files. The `Fox.animgraph.ron` asset has been updated to the new format. Closes bevyengine#14395. ## Showcase https://github.com/user-attachments/assets/52dfe05f-fdb3-477a-9462-ec150f93df33 ## Migration Guide * The `animgraph.ron` format has changed to accommodate the new *additive blending* feature. You'll need to change `clip` fields to instances of the new `AnimationNodeType` enum.
1 parent 7eadc1d commit 0094bcb

File tree

5 files changed

+488
-158
lines changed

5 files changed

+488
-158
lines changed

assets/animation_graphs/Fox.animgraph.ron

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@
22
graph: (
33
nodes: [
44
(
5-
clip: None,
5+
node_type: Blend,
66
mask: 0,
77
weight: 1.0,
88
),
99
(
10-
clip: None,
10+
node_type: Blend,
1111
mask: 0,
12-
weight: 0.5,
12+
weight: 1.0,
1313
),
1414
(
15-
clip: Some(AssetPath("models/animated/Fox.glb#Animation0")),
15+
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")),
1616
mask: 0,
1717
weight: 1.0,
1818
),
1919
(
20-
clip: Some(AssetPath("models/animated/Fox.glb#Animation1")),
20+
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")),
2121
mask: 0,
2222
weight: 1.0,
2323
),
2424
(
25-
clip: Some(AssetPath("models/animated/Fox.glb#Animation2")),
25+
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")),
2626
mask: 0,
2727
weight: 1.0,
2828
),

crates/bevy_animation/src/animation_curves.rs

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ use bevy_render::mesh::morph::MorphWeights;
9696
use bevy_transform::prelude::Transform;
9797

9898
use crate::{
99-
graph::AnimationNodeIndex, prelude::Animatable, AnimationEntityMut, AnimationEvaluationError,
99+
graph::AnimationNodeIndex,
100+
prelude::{Animatable, BlendInput},
101+
AnimationEntityMut, AnimationEvaluationError,
100102
};
101103

102104
/// A value on a component that Bevy can animate.
@@ -297,7 +299,11 @@ where
297299
P: AnimatableProperty,
298300
{
299301
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
300-
self.evaluator.blend(graph_node)
302+
self.evaluator.combine(graph_node, /*additive=*/ false)
303+
}
304+
305+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
306+
self.evaluator.combine(graph_node, /*additive=*/ true)
301307
}
302308

303309
fn push_blend_register(
@@ -393,7 +399,11 @@ where
393399

394400
impl AnimationCurveEvaluator for TranslationCurveEvaluator {
395401
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
396-
self.evaluator.blend(graph_node)
402+
self.evaluator.combine(graph_node, /*additive=*/ false)
403+
}
404+
405+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
406+
self.evaluator.combine(graph_node, /*additive=*/ true)
397407
}
398408

399409
fn push_blend_register(
@@ -487,7 +497,11 @@ where
487497

488498
impl AnimationCurveEvaluator for RotationCurveEvaluator {
489499
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
490-
self.evaluator.blend(graph_node)
500+
self.evaluator.combine(graph_node, /*additive=*/ false)
501+
}
502+
503+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
504+
self.evaluator.combine(graph_node, /*additive=*/ true)
491505
}
492506

493507
fn push_blend_register(
@@ -581,7 +595,11 @@ where
581595

582596
impl AnimationCurveEvaluator for ScaleCurveEvaluator {
583597
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
584-
self.evaluator.blend(graph_node)
598+
self.evaluator.combine(graph_node, /*additive=*/ false)
599+
}
600+
601+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
602+
self.evaluator.combine(graph_node, /*additive=*/ true)
585603
}
586604

587605
fn push_blend_register(
@@ -708,8 +726,12 @@ where
708726
}
709727
}
710728

711-
impl AnimationCurveEvaluator for WeightsCurveEvaluator {
712-
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
729+
impl WeightsCurveEvaluator {
730+
fn combine(
731+
&mut self,
732+
graph_node: AnimationNodeIndex,
733+
additive: bool,
734+
) -> Result<(), AnimationEvaluationError> {
713735
let Some(&(_, top_graph_node)) = self.stack_blend_weights_and_graph_nodes.last() else {
714736
return Ok(());
715737
};
@@ -736,13 +758,27 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator {
736758
.iter_mut()
737759
.zip(stack_iter)
738760
{
739-
*dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight);
761+
if additive {
762+
*dest += src * weight_to_blend;
763+
} else {
764+
*dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight);
765+
}
740766
}
741767
}
742768
}
743769

744770
Ok(())
745771
}
772+
}
773+
774+
impl AnimationCurveEvaluator for WeightsCurveEvaluator {
775+
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
776+
self.combine(graph_node, /*additive=*/ false)
777+
}
778+
779+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
780+
self.combine(graph_node, /*additive=*/ true)
781+
}
746782

747783
fn push_blend_register(
748784
&mut self,
@@ -826,7 +862,11 @@ impl<A> BasicAnimationCurveEvaluator<A>
826862
where
827863
A: Animatable,
828864
{
829-
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> {
865+
fn combine(
866+
&mut self,
867+
graph_node: AnimationNodeIndex,
868+
additive: bool,
869+
) -> Result<(), AnimationEvaluationError> {
830870
let Some(top) = self.stack.last() else {
831871
return Ok(());
832872
};
@@ -840,15 +880,36 @@ where
840880
graph_node: _,
841881
} = self.stack.pop().unwrap();
842882

843-
match self.blend_register {
883+
match self.blend_register.take() {
844884
None => self.blend_register = Some((value_to_blend, weight_to_blend)),
845-
Some((ref mut current_value, ref mut current_weight)) => {
846-
*current_weight += weight_to_blend;
847-
*current_value = A::interpolate(
848-
current_value,
849-
&value_to_blend,
850-
weight_to_blend / *current_weight,
851-
);
885+
Some((mut current_value, mut current_weight)) => {
886+
current_weight += weight_to_blend;
887+
888+
if additive {
889+
current_value = A::blend(
890+
[
891+
BlendInput {
892+
weight: 1.0,
893+
value: current_value,
894+
additive: true,
895+
},
896+
BlendInput {
897+
weight: weight_to_blend,
898+
value: value_to_blend,
899+
additive: true,
900+
},
901+
]
902+
.into_iter(),
903+
);
904+
} else {
905+
current_value = A::interpolate(
906+
&current_value,
907+
&value_to_blend,
908+
weight_to_blend / current_weight,
909+
);
910+
}
911+
912+
self.blend_register = Some((current_value, current_weight));
852913
}
853914
}
854915

@@ -967,6 +1028,22 @@ pub trait AnimationCurveEvaluator: Reflect {
9671028
/// 4. Return success.
9681029
fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>;
9691030

1031+
/// Additively blends the top element of the stack with the blend register.
1032+
///
1033+
/// The semantics of this method are as follows:
1034+
///
1035+
/// 1. Pop the top element of the stack. Call its value vₘ and its weight
1036+
/// wₘ. If the stack was empty, return success.
1037+
///
1038+
/// 2. If the blend register is empty, set the blend register value to vₘ
1039+
/// and the blend register weight to wₘ; then, return success.
1040+
///
1041+
/// 3. If the blend register is nonempty, call its current value vₙ.
1042+
/// Then, set the value of the blend register to vₙ + vₘwₘ.
1043+
///
1044+
/// 4. Return success.
1045+
fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>;
1046+
9701047
/// Pushes the current value of the blend register onto the stack.
9711048
///
9721049
/// If the blend register is empty, this method does nothing successfully.

0 commit comments

Comments
 (0)