bevy_animation_graph
Motivation
Animation graphs are an essential tool for managing the complexity present in the animation pipelines for modern 3D games. When your game has tens of animations with complex blends and transitions, or you want to generate your animations procedurally from very few keyframes, simple animation clip playback is not enough.
This library aims to fill this gap in the Bevy ecosystem.
Current Features
- Animation graphs are assets. They can be loaded from asset files, or created in code with an ergonomic API.
- Available nodes:
- Animation clip playback
- Animation chaining (i.e. play one node after another)
- Looping
- Linear Blending (in bone space)
- Mirror animation about the YZ plane
- Arithmetic nodes:
- F32: Add, Subtract, Multiply, Divide, Clamp.
- Speed up or slow down animation playback
- Animation graph node
- Nesting animation graphs as nodes within other graphs.
- Support for custom nodes written in Rust (with the caveat that custom nodes cannot be serialized/deserialized as assets)
- Export animation graphs in graphviz
.dot
format for visualization. - Output from graph nodes is cached to avoid unnecessary computations.
Planned Features
In order of priority:
- More documentation!
- Finite state machines.
- More procedural animation nodes:
- Apply transform to bone
- Two-bone IK
- Graph editor UI tool
- Ragdoll and physics integration (inititally
bevy_xpbd
, possibly rapier later):- Using a bone mask to specify which bones are kinematically driven, and which bones are simulated (i.e. ragdolled)
- Pose matching with joint motors (pending on joint motors being implemented in
bevy_xpbd
, currently WIP)
- FABRIK node (?).
Usage
Installation
This library is available in crates.io. To install the latest published version run
cargo add bevy_animation_graph
or manually add the latest version to your Cargo.toml
.
To install the latest git master, add the following to Cargo.toml
# ...
[dependencies]
# ...
bevy_animation_graph = { git = "https://github.com/mbrea-c/bevy_animation_graph.git" }
# ...
Animation assets
Animation clips are specified with asset files ending in .anim.ron
. For example:
// In file: assets/animations/walk.anim.ron
(
source: GltfNamed(
path: "models/character_rigged.gltf",
animation_name: "Walk",
),
)
Currently, the only supported source is GltfNamed
, where the path
field points to a gltf asset, and the animation_name
field contains the name label of the animation.
Animation graph assets
Animation graphs are stored as .animgraph.ron
files. See the explained example below and the code in examples/human.rs
for how to create, play and interact with animation graphs.
Examples
Locomotion graph
Consider the following scenario:
- Inputs:
- We have a partial running animation composed of two keyframes: one for the reaching pose and one for the passing pose. The animation only covers half of a running cycle.
- We have a partial walk animation similarly composed of two keyframes.
- We have a target movement speed for the character.
- We know the movement speeds corresponding to the unmodified walk and run animations, which we call
walk_base_speed
andrun_base_speed
. - We decide on a range of target speeds where the blend between walk and run should happen. We call this
blend_start
andblend_end
.
- Desired output:
- A complete movement animation that covers the full walk/run cycle and performs a synchronized blend between the walk and run animations based on the target speed.
A solution to this problem is as follows:
-
The blend factor between the two animations can be computed as
blend_fac = clamp((target_speed - blend_start) / (blend_end - blend_start), 0, 1)
The playback speed factor applied to both animations is then
speed_fac = target_speed / (walk_base_speed * (1 - blend_fac) + run_base_speed * blend_fac)
-
We mirror the run animation along the X axis, and chain the result to the original run animation to get a complete run cycle. Do the same with the walk animation.
-
Blend the two animations together using
blend_fac
. Loop the result and apply the speed factorspeed_fac
.
The resulting graph is defined like so:
// In file assets/animation_graphs/locomotion.animgraph.ron
(
nodes: [
(name: "Walk Clip", node: Clip("animations/walk.anim.ron", Some(1.))),
(name: "Run Clip", node: Clip("animations/run.anim.ron", Some(1.))),
(name: "Walk Flip LR", node: FlipLR),
(name: "Run Flip LR", node: FlipLR),
(name: "Walk Chain", node: Chain),
(name: "Run Chain", node: Chain),
(name: "Blend", node: Blend),
(name: "Loop", node: Loop),
(name: "Speed", node: Speed),
(name: "Param graph", node: Graph("animation_graphs/velocity_to_params.animgraph.ron")),
],
input_parameters: {
"Target Speed": F32(1.5),
},
output_time_dependent_spec: {
"Pose": PoseFrame,
},
input_edges: [
// Alpha parameters
("Target Speed", ("Param graph", "Target Speed")),
],
edges: [
(("Param graph", "blend_fac"),("Blend", "Factor")),
(("Param graph", "speed_fac"),("Speed", "Speed")),
(("Walk Clip", "Pose Out"), ("Walk Flip LR", "Pose In")),
(("Walk Clip", "Pose Out"), ("Walk Chain", "Pose In 1")),
(("Walk Flip LR", "Pose Out"), ("Walk Chain", "Pose In 2")),
(("Run Clip", "Pose Out"), ("Run Chain", "Pose In 1")),
(("Run Clip", "Pose Out"), ("Run Flip LR", "Pose In")),
(("Run Flip LR", "Pose Out"), ("Run Chain", "Pose In 2")),
(("Walk Chain", "Pose Out"), ("Blend", "Pose In 1")),
(("Run Chain", "Pose Out"), ("Blend", "Pose In 2")),
(("Blend", "Pose Out"), ("Loop", "Pose In")),
(("Loop", "Pose Out"), ("Speed", "Pose In")),
],
output_edges: [
(("Speed", "Pose Out"), "Pose"),
],
default_output: Some("Pose"),
)
We have extracted the computation of blend_fac
and speed_fac
into a separate graph that we reference as a node above:
// In file: assets/animation_graphs/locomotion.ron
(
nodes: [
(name: "Alpha Tmp 1", node: SubF32),
(name: "Alpha Tmp 2", node: SubF32),
(name: "Alpha Tmp 3", node: DivF32),
(name: "Alpha", node: ClampF32),
(name: "1-Alpha", node: SubF32),
(name: "Factored walk speed", node: MulF32),
(name: "Factored run speed", node: MulF32),
(name: "Blended base speed", node: AddF32),
(name: "Speed factor", node: DivF32),
],
input_parameters: {
"Walk Base Speed": F32(0.3),
"Run Base Speed": F32(0.8),
"Target Speed": F32(1.5),
"Blend Start": F32(1.0),
"Blend End": F32(3.0),
// Constant values
// TODO: Maybe there should be a better way to handle constant/defaults?
"ZERO": F32(0.),
"ONE": F32(1.),
},
output_parameter_spec: {
"speed_fac": F32,
"blend_fac": F32,
},
input_edges: [
// Alpha clamp range
("ZERO", ("Alpha", "Min")),
("ONE", ("Alpha", "Max")),
// Alpha parameters
("Target Speed", ("Alpha Tmp 1", "F32 In 1")),
("Blend Start", ("Alpha Tmp 1", "F32 In 2")),
("Blend End", ("Alpha Tmp 2", "F32 In 1")),
("Blend Start", ("Alpha Tmp 2", "F32 In 2")),
// Speed factor parameters
("ONE", ("1-Alpha", "F32 In 1")),
("Walk Base Speed", ("Factored walk speed", "F32 In 1")),
("Run Base Speed", ("Factored run speed", "F32 In 1")),
("Target Speed", ("Speed factor", "F32 In 1")),
],
edges: [
// Blend alpha computation
// ((target_speed - blend_start) / (blend_end - blend_start)).clamp(0., 1.);
(("Alpha Tmp 1", "F32 Out"), ("Alpha Tmp 3", "F32 In 1")),
(("Alpha Tmp 2", "F32 Out"), ("Alpha Tmp 3", "F32 In 2")),
(("Alpha Tmp 3", "F32 Out"), ("Alpha", "F32 In")),
// Speed factor computation
// target_speed / (walk_base_speed * (1. - alpha) + run_base_seed * alpha)
(("Alpha", "F32 Out"),("1-Alpha", "F32 In 2")),
(("1-Alpha", "F32 Out"),("Factored walk speed", "F32 In 2")),
(("Alpha", "F32 Out"),("Factored run speed", "F32 In 2")),
(("Factored walk speed", "F32 Out"), ("Blended base speed", "F32 In 1")),
(("Factored run speed", "F32 Out"), ("Blended base speed", "F32 In 2")),
(("Blended base speed", "F32 Out"),("Speed factor", "F32 In 2")),
],
output_edges: [
(("Alpha", "F32 Out"), "blend_fac"),
(("Speed factor", "F32 Out"), "speed_fac"),
],
)
The resulting locomotion graph looks like this:
And the parameter computation graph:
The resulting animation is this (please forgive the lack or artistic skill) :
movie.mp4
Contributing
If you run into a bug or want to suggest a missing feature, feel free to post an issue, open a PR or reach out to me in Discord (@mbreac in the Bevy discord).
FAQ
Is this ready for production?
No. The library is still in a very early stage, and there is likely to be bugs, missing features, performance issues and API breakage as I iron out the kinks in the coming months. However, it has reached the point where it is useful and stable enough that I've started to integrate it into my game, and it may be useful to you for small-ish projects or game jams.
Acknowledgements
Many thanks to Bobby Anguelov for his lectures on animation programming.