Animation graphs in Bevy!

Overview

Crates.io Crates.io CI

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:

  1. More documentation!
  2. Finite state machines.
  3. More procedural animation nodes:
    1. Apply transform to bone
    2. Two-bone IK
  4. Graph editor UI tool
  5. Ragdoll and physics integration (inititally bevy_xpbd, possibly rapier later):
    1. Using a bone mask to specify which bones are kinematically driven, and which bones are simulated (i.e. ragdolled)
    2. Pose matching with joint motors (pending on joint motors being implemented in bevy_xpbd, currently WIP)
  6. 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 and run_base_speed.
    • We decide on a range of target speeds where the blend between walk and run should happen. We call this blend_start and blend_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:

  1. 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)
    
  2. 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.

  3. Blend the two animations together using blend_fac. Loop the result and apply the speed factor speed_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: Locomotion graph example

And the parameter computation graph: Parameter graph example

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.

Comments
  • Support separate time query for each time-dependent output

    Support separate time query for each time-dependent output

    Time queries currently only support one query per node. However, there may be cases where this is not enough: In particular, nested animation graph nodes may have an arbitrary number of time-dependent inputs and outputs, and currently the system will not be able to handle that correctly.

    This is not a big change, since we only support an arbitrary number of time-dependent inputs/outputs with specific duration values for each, so the foundation is there.

    bug enhancement 
    opened by mbrea-c 1
  • Simplify graph architecture

    Simplify graph architecture

    Objective

    The generality of the current graph architecture causes complications during processing, especially when it comes to cache output. Most other animation graph implementations (as far as I'm aware of) restrict pose outputs to one per node and only allow pose outputs to be connected to one other pin. This ensures that each pose output will be queried once per frame, simplifying caching and computation.

    This should also enable performance improvements, as it will be easier to replace string IDs with integers and hashmap lookups with integer indexing.

    Solution

    Refactored the code to enforce the following graph invariant:

    • Each node can only have a single pose output
    • Each pose output can only be connected to a single input

    Breaking changes

    As part of the refactoring, the animation graph format was changed slightly. Refer to the updated examples for instructions on how to use the updated format.

    enhancement 
    opened by mbrea-c 0
  • Enable `AnimationGraphPlayer` to pass parameters

    Enable `AnimationGraphPlayer` to pass parameters

    Objective

    Currently the only way to set input parameters of a top-level graph (i.e. a graph assigned directly to an AnimationGraphPlayer) is to access the asset mutably and modify its parameter node. This would require cloning the asset every time we would like to have a new actor play the graph, which is inefficient. Ideally, we would set the parameters on AnimationGraphPlayer directly, and the player would then pass the parameters when querying the graph as a node overlay.

    Solution

    Enable setting parameters in the AnimationGraphPlayer, similarly to how GraphNode does it.

    enhancement 
    opened by mbrea-c 0
  • Animated Scene Assets

    Animated Scene Assets

    Objective

    This plugin is currently not very convenient to use: the target scene needs to be added, a system must then find the bevy::animation::AnimationPlayer in the instantiated scene, replace it with an AnimationGraphPlayer and finally add the desired animation graph to it.

    This would be better handled by having an asset specifying the scene to be loaded together with the path (using entity Names) of the AnimationPlayer to be replaced and the asset path of the animation graph that should be played.

    Solution

    Provide AnimatedScene assets and an appropriate asset loader. Animated scenes are defined in .animscn.ron asset files as follows:

    (
        source: "models/character_rigged.gltf#Scene0",
        path_to_player: ["Main Controller"],
        animation_graph: "animation_graphs/locomotion.animgraph.ron",
    )
    

    where

    • source is the asset path of the original scene
    • path_to_player is the path (vector of entity Names) to the original AnimationPlayer.
    • animation_graph is the asset path of the AnimationGraph we want to playback.

    We can now simply instantiate an AnimatedSceneBundle with the given AnimatedScene handle, just like we would do with a regular scene:

        //...
        commands.spawn(AnimatedSceneBundle {
            animated_scene: asset_server.load("animated_scenes/character.animscn.ron"),
            ..default()
        });
        //...
    

    Once the animated scene is finished successfully spawning, an AnimatedSceneInstance component will be added to it. For convenience, this component contains the entity id of the child containing the AnimationGraphPlayer, in case the user decides to manually set some animation graph parameters. If the animated scene spawning fails, (e.g. because the given path_to_player is incorrect), an error will be printed and the AnimatedSceneFailed component will be added instead.

    opened by mbrea-c 0
  • Animated Scenes sometimes appear invisible

    Animated Scenes sometimes appear invisible

    Animated scenes spawning sometimes proceeds seemingly without issue (entities are spawned with correct components and animation works as expected) but the resulting meshes are invisible.

    This occurs relatively rarely (<1/10 times) and I have not found a way to reliably reproduce it.

    Any additional reports would be appreciated :)

    bug 
    opened by mbrea-c 0
  • Replace character example

    Replace character example

    The character example is not great: Plenty of candy-wrapper artefacts) are visible in the model during animation (which make the joints look weirdly thin), and I somehow got the walk/run cycle wrong (left arm moves forward while left leg is moving forward and vice versa).

    It might be better for now to replace with an example based on the Bevy fox.

    enhancement good first issue 
    opened by mbrea-c 0
  • State Machines

    State Machines

    Tracking issue for state machine nodes.

    Requirements

    1. Arbitrary number of states, with an arbitrary number of transitions between every two of them (note that this is different from the core animation graph: in the animation graph there can only be one edge into every node input).
    2. Every node is an animation graph with a single pose output.
    3. Every node transition is an animation graph which takes as input the source state pose and the target state pose (plus some parameters such as transition percentage, etc) , and returns single pose.
    4. Transitions may need to be cut short under certain events; this needs to be configurable.

    Questions

    For requirements 2 and 3, should we support arbitrary FSM outputs as long as every state and transition has the same output schema?

    I currently cannot think of a use-case for this, but if anything comes up it does seem doable.

    enhancement 
    opened by mbrea-c 0
  • Add docs.rs documentation

    Add docs.rs documentation

    Currently the docs.rs page is pretty barren. before leaving alpha this needs to be populated with enough information for users to get started.

    TODO:

    • [x] Minimal examples
      • [x] How to use graph animation player, and how to replace bevy's own animation players is specific entities.
      • [x] Graphs as assets
      • [ ] Programmatically creating graphs with API (Won't do in this PR)
      • [ ] Graph with custom node (Won't do in this PR)
    • [x] Explanation of how the animation graph works.
    • [x] Documentation of available nodes with inputs and outputs specified. This should be linked from front page. (Inputs and outputs will be documented in future patch).
    documentation 
    opened by mbrea-c 0
Releases(v0.1.0-alpha.4)
Owner
Manuel Brea Carreras
Manuel Brea Carreras
Tweening animation plugin for the Bevy game engine.

?? Bevy Tweening Tweening animation plugin for the Bevy game engine. Features Animate any field of any component or asset, including custom ones. Run

Jerome Humbert 135 Dec 23, 2022
📺 An implementation of the retro bouncing DVD logo screensaver animation built with bevy and rust

?? Bevy bouncing DVD logo This project is a simple demonstration of using the Bevy game engine and the Rust programming language to create a bouncing

Victor Aremu 5 Jan 12, 2023
Action-based animation system for Bevy.

bevy_action_animation Action-based animation system for Bevy. Introduction This plugin provides users of the Bevy game engine with an action/trigger-b

undersquire 5 Apr 15, 2023
Utilities for creating strictly ordered execution graphs of systems for the Bevy game engine

bevy_system_graph This crate provides the utilities for creating strictly ordered execution graphs of systems for the Bevy game engine. Bevy Version S

Hourai Teahouse 19 Dec 2, 2022
Interoperation with the Automaton animation engine

Automaton-rs an interopt layer for the Automaton animation engine for creative coding

Aaron Bies 9 Sep 18, 2021
Minecraft-esque voxel engine prototype made with the bevy game engine. Pending bevy 0.6 release to undergo a full rewrite.

vx_bevy A voxel engine prototype made using the Bevy game engine. Goals and features Very basic worldgen Animated chunk loading (ala cube world) Optim

Lucas Arriesse 125 Dec 31, 2022
bevy-hikari is an implementation of voxel cone tracing global illumination with anisotropic mip-mapping in Bevy

Bevy Voxel Cone Tracing bevy-hikari is an implementation of voxel cone tracing global illumination with anisotropic mip-mapping in Bevy. Bevy Version

研究社交 208 Dec 27, 2022
A simple extension for `bevy-editor-pls` to support tilemap editing right inside the bevy app.

What is this This is a simple tilemap editor plugin, that hooks right into bevy_editor_pls to work with bevy_ecs_tilemap. It works completely within i

null 3 May 8, 2023
Bevy Simple Portals is a Bevy game engine plugin aimed to create portals.

Portals for Bevy Bevy Simple Portals is a Bevy game engine plugin aimed to create portals. Those portals are (for now) purely visual and can be used t

Sélène Amanita 11 May 28, 2023
Minecraft using Bevy and Bevy-Meshem

minecraft_bevy Minecraft_bevy was built to showcase bevy_meshem. After a week of developing it has: Chunk loading / unloading each chunk's mesh is bei

Adam 7 Oct 4, 2023
A Bevy plugin for loading the LDtk 2D tile map format.

bevy_ldtk ( Tileset from "Cavernas" by Adam Saltsman ) A Bevy plugin for loading LDtk tile maps. Usage use bevy::prelude::*; use bevy_ldtk::*; fn mai

Katharos Technology 23 Jul 4, 2022
Basic first-person fly camera for the Bevy game engine

bevy_flycam A basic first-person fly camera for Bevy 0.4 Controls WASD to move horizontally SPACE to ascend LSHIFT to descend ESC to grab/release curs

Spencer Burris 85 Dec 23, 2022
An ergonomic physics API for bevy games.

Heron An ergonomic physics API for 2d and 3d bevy games. (powered by rapier) How it looks like fn main() { App::build() .add_plugins(DefaultPlug

Jonathan Cornaz 313 Dec 16, 2022
Inspector plugin for the bevy game engine

bevy-inspector-egui This crate provides the ability to annotate structs with a #[derive(Inspectable)], which opens a debug interface using egui where

Jakob Hellermann 517 Dec 31, 2022
A plugin for Egui integration into Bevy

bevy_egui This crate provides a Egui integration for the Bevy game engine. Features: Desktop and web (bevy_webgl2) platforms support Clipboard (web su

Vladyslav Batyrenko 453 Jan 3, 2023
Crossterm plugin for the bevy game engine

What is bevy_crossterm? bevy_crossterm is a Bevy plugin that uses crossterm as a renderer. It provides custom components and events which allow users

null 79 Nov 2, 2022
A 4X style camera for bevy.

A 4X style camera for bevy. Demo Default Key Bindings: W / A / S / D / Arrow Keys / Mouse Left - Move along the horizontal plane Q / E / Mouse Right -

null 30 Jan 4, 2023
Game physics in one weekend with bevy

Game Physics in a Weekend (in Rust) This project is an implementation of the Game Physics in a Weekend book using the Rust programming language and th

Cameron Hart 28 Dec 23, 2022
A Bevy plugin to use Kira for game audio

Bevy Kira audio This bevy plugin is intended to try integrating Kira into Bevy. The end goal would be to replace or update bevy_audio, if Kira turns o

Niklas Eicker 172 Jan 5, 2023