Skip to content

Commit

Permalink
Implement additive blending for animation graphs. (#15631)
Browse files Browse the repository at this point in the history
*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 #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 #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.
  • Loading branch information
pcwalton authored Oct 4, 2024
1 parent 7eadc1d commit 0094bcb
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 158 deletions.
12 changes: 6 additions & 6 deletions assets/animation_graphs/Fox.animgraph.ron
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
graph: (
nodes: [
(
clip: None,
node_type: Blend,
mask: 0,
weight: 1.0,
),
(
clip: None,
node_type: Blend,
mask: 0,
weight: 0.5,
weight: 1.0,
),
(
clip: Some(AssetPath("models/animated/Fox.glb#Animation0")),
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")),
mask: 0,
weight: 1.0,
),
(
clip: Some(AssetPath("models/animated/Fox.glb#Animation1")),
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")),
mask: 0,
weight: 1.0,
),
(
clip: Some(AssetPath("models/animated/Fox.glb#Animation2")),
node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")),
mask: 0,
weight: 1.0,
),
Expand Down
111 changes: 94 additions & 17 deletions crates/bevy_animation/src/animation_curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(());
};
Expand All @@ -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,
Expand Down Expand Up @@ -826,7 +862,11 @@ impl<A> BasicAnimationCurveEvaluator<A>
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(());
};
Expand All @@ -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(
&current_value,
&value_to_blend,
weight_to_blend / current_weight,
);
}

self.blend_register = Some((current_value, current_weight));
}
}

Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 0094bcb

Please sign in to comment.