mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
Implement animation masks, allowing fine control of the targets that animations affect. (#15013)
This commit adds support for *masks* to the animation graph. A mask is a set of animation targets (bones) that neither a node nor its descendants are allowed to animate. Animation targets can be assigned one or more *mask group*s, which are specific to a single graph. If a node masks out any mask group that an animation target belongs to, animation curves for that target will be ignored during evaluation. The canonical use case for masks is to support characters holding objects. Typically, character animations will contain hand animations in the case that the character's hand is empty. (For example, running animations may close a character's fingers into a fist.) However, when the character is holding an object, the animation must be altered so that the hand grips the object. Bevy currently has no convenient way to handle this. The only workaround that I can see is to have entirely separate animation clips for characters' hands and bodies and keep them in sync, which is burdensome and doesn't match artists' expectations from other engines, which all effectively have support for masks. However, with mask group support, this task is simple. We assign each hand to a mask group and parent all character animations to a node. When a character grasps an object in hand, we position the fingers as appropriate and then enable the mask group for that hand in that node. This allows the character's animations to run normally, while the object remains correctly attached to the hand. Note that even with this PR, we won't have support for running separate animations for a character's hand and the rest of the character. This is because we're missing additive blending: there's no way to combine the two masked animations together properly. I intend that to be a follow-up PR. The major engines all have support for masks, though the workflow varies from engine to engine: * Unity has support for masks [essentially as implemented here], though with layers instead of a tree. However, when using the Mecanim ("Humanoid") feature, precise control over bones is lost in favor of predefined muscle groups. * Unreal has a feature named [*layered blend per bone*]. This allows for separate blend weights for different bones, effectively achieving masks. I believe that the combination of blend nodes and masks make Bevy's animation graph as expressible as that of Unreal, once we have support for additive blending, though you may have to use more nodes than you would in Unreal. Moreover, separating out the concepts of "blend weight" and "which bones this node applies to" seems like a cleaner design than what Unreal has. * Godot's `AnimationTree` has the notion of [*blend filters*], which are essentially the same as masks as implemented in this PR. Additionally, this patch fixes a bug with weight evaluation whereby weights weren't properly propagated down to grandchildren, because the weight evaluation for a node only checked its parent's weight, not its evaluated weight. I considered submitting this as a separate PR, but given that this PR refactors that code entirely to support masks and weights under a unified "evaluated node" concept, I simply included the fix here. A new example, `animation_masks`, has been added. It demonstrates how to toggle masks on and off for specific portions of a skin. This is part of #14395, but I'm going to defer closing that issue until we have additive blending. [essentially as implemented here]: https://docs.unity3d.com/560/Documentation/Manual/class-AvatarMask.html [*layered blend per bone*]: https://dev.epicgames.com/documentation/en-us/unreal-engine/using-layered-animations-in-unreal-engine [*blend filters*]: https://docs.godotengine.org/en/stable/tutorials/animation/animation_tree.html ## Migration Guide * The serialized format of animation graphs has changed with the addition of animation masks. To upgrade animation graph RON files, add `mask` and `mask_groups` fields as appropriate. (They can be safely set to zero.)
This commit is contained in:
parent
49a06e9c76
commit
d2624765d0
6 changed files with 594 additions and 18 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -3408,6 +3408,17 @@ description = "Demonstrates picking sprites and sprite atlases"
|
|||
category = "Picking"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "animation_masks"
|
||||
path = "examples/animation/animation_masks.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.animation_masks]
|
||||
name = "Animation Masks"
|
||||
description = "Demonstrates animation masks"
|
||||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
|
|
|
@ -3,22 +3,27 @@
|
|||
nodes: [
|
||||
(
|
||||
clip: None,
|
||||
mask: 0,
|
||||
weight: 1.0,
|
||||
),
|
||||
(
|
||||
clip: None,
|
||||
mask: 0,
|
||||
weight: 0.5,
|
||||
),
|
||||
(
|
||||
clip: Some(AssetPath("models/animated/Fox.glb#Animation0")),
|
||||
mask: 0,
|
||||
weight: 1.0,
|
||||
),
|
||||
(
|
||||
clip: Some(AssetPath("models/animated/Fox.glb#Animation1")),
|
||||
mask: 0,
|
||||
weight: 1.0,
|
||||
),
|
||||
(
|
||||
clip: Some(AssetPath("models/animated/Fox.glb#Animation2")),
|
||||
mask: 0,
|
||||
weight: 1.0,
|
||||
),
|
||||
],
|
||||
|
@ -32,4 +37,5 @@
|
|||
],
|
||||
),
|
||||
root: 0,
|
||||
mask_groups: {},
|
||||
)
|
|
@ -5,12 +5,13 @@ use std::ops::{Index, IndexMut};
|
|||
|
||||
use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext};
|
||||
use bevy_reflect::{Reflect, ReflectSerialize};
|
||||
use bevy_utils::HashMap;
|
||||
use petgraph::graph::{DiGraph, NodeIndex};
|
||||
use ron::de::SpannedError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::AnimationClip;
|
||||
use crate::{AnimationClip, AnimationTargetId};
|
||||
|
||||
/// A graph structure that describes how animation clips are to be blended
|
||||
/// together.
|
||||
|
@ -56,6 +57,28 @@ use crate::AnimationClip;
|
|||
/// their weights will be halved and finally blended with the Idle animation.
|
||||
/// Thus the weight of Run and Walk are effectively half of the weight of Idle.
|
||||
///
|
||||
/// Nodes can optionally have a *mask*, a bitfield that restricts the set of
|
||||
/// animation targets that the node and its descendants affect. Each bit in the
|
||||
/// mask corresponds to a *mask group*, which is a set of animation targets
|
||||
/// (bones). An animation target can belong to any number of mask groups within
|
||||
/// the context of an animation graph.
|
||||
///
|
||||
/// When the appropriate bit is set in a node's mask, neither the node nor its
|
||||
/// descendants will animate any animation targets belonging to that mask group.
|
||||
/// That is, setting a mask bit to 1 *disables* the animation targets in that
|
||||
/// group. If an animation target belongs to multiple mask groups, masking any
|
||||
/// one of the mask groups that it belongs to will mask that animation target.
|
||||
/// (Thus an animation target will only be animated if *all* of its mask groups
|
||||
/// are unmasked.)
|
||||
///
|
||||
/// A common use of masks is to allow characters to hold objects. For this, the
|
||||
/// typical workflow is to assign each character's hand to a mask group. Then,
|
||||
/// when the character picks up an object, the application masks out the hand
|
||||
/// that the object is held in for the character's animation set, then positions
|
||||
/// the hand's digits as necessary to grasp the object. The character's
|
||||
/// animations will continue to play but will not affect the hand, which will
|
||||
/// continue to be depicted as holding the object.
|
||||
///
|
||||
/// Animation graphs are assets and can be serialized to and loaded from [RON]
|
||||
/// files. Canonically, such files have an `.animgraph.ron` extension.
|
||||
///
|
||||
|
@ -71,8 +94,20 @@ use crate::AnimationClip;
|
|||
pub struct AnimationGraph {
|
||||
/// The `petgraph` data structure that defines the animation graph.
|
||||
pub graph: AnimationDiGraph,
|
||||
|
||||
/// The index of the root node in the animation graph.
|
||||
pub root: NodeIndex,
|
||||
|
||||
/// The mask groups that each animation target (bone) belongs to.
|
||||
///
|
||||
/// Each value in this map is a bitfield, in which 0 in bit position N
|
||||
/// indicates that the animation target doesn't belong to mask group N, and
|
||||
/// a 1 in position N indicates that the animation target does belong to
|
||||
/// mask group N.
|
||||
///
|
||||
/// Animation targets not in this collection are treated as though they
|
||||
/// don't belong to any mask groups.
|
||||
pub mask_groups: HashMap<AnimationTargetId, AnimationMask>,
|
||||
}
|
||||
|
||||
/// A type alias for the `petgraph` data structure that defines the animation
|
||||
|
@ -98,6 +133,14 @@ pub struct AnimationGraphNode {
|
|||
/// Otherwise, this node is a *blend node*.
|
||||
pub clip: Option<Handle<AnimationClip>>,
|
||||
|
||||
/// A bitfield specifying the mask groups that this node and its descendants
|
||||
/// will not affect.
|
||||
///
|
||||
/// A 0 in bit N indicates that this node and its descendants *can* animate
|
||||
/// animation targets in mask group N, while a 1 in bit N indicates that
|
||||
/// this node and its descendants *cannot* animate mask group N.
|
||||
pub mask: AnimationMask,
|
||||
|
||||
/// The weight of this node.
|
||||
///
|
||||
/// Weights are propagated down to descendants. Thus if an animation clip
|
||||
|
@ -144,6 +187,8 @@ pub struct SerializedAnimationGraph {
|
|||
pub graph: DiGraph<SerializedAnimationGraphNode, (), u32>,
|
||||
/// Corresponds to the `root` field on [`AnimationGraph`].
|
||||
pub root: NodeIndex,
|
||||
/// Corresponds to the `mask_groups` field on [`AnimationGraph`].
|
||||
pub mask_groups: HashMap<AnimationTargetId, AnimationMask>,
|
||||
}
|
||||
|
||||
/// A version of [`AnimationGraphNode`] suitable for serializing as an asset.
|
||||
|
@ -153,6 +198,8 @@ pub struct SerializedAnimationGraph {
|
|||
pub struct SerializedAnimationGraphNode {
|
||||
/// Corresponds to the `clip` field on [`AnimationGraphNode`].
|
||||
pub clip: Option<SerializedAnimationClip>,
|
||||
/// Corresponds to the `mask` field on [`AnimationGraphNode`].
|
||||
pub mask: AnimationMask,
|
||||
/// Corresponds to the `weight` field on [`AnimationGraphNode`].
|
||||
pub weight: f32,
|
||||
}
|
||||
|
@ -172,12 +219,24 @@ pub enum SerializedAnimationClip {
|
|||
AssetId(AssetId<AnimationClip>),
|
||||
}
|
||||
|
||||
/// The type of an animation mask bitfield.
|
||||
///
|
||||
/// Bit N corresponds to mask group N.
|
||||
///
|
||||
/// Because this is a 64-bit value, there is currently a limitation of 64 mask
|
||||
/// groups per animation graph.
|
||||
pub type AnimationMask = u64;
|
||||
|
||||
impl AnimationGraph {
|
||||
/// Creates a new animation graph with a root node and no other nodes.
|
||||
pub fn new() -> Self {
|
||||
let mut graph = DiGraph::default();
|
||||
let root = graph.add_node(AnimationGraphNode::default());
|
||||
Self { graph, root }
|
||||
Self {
|
||||
graph,
|
||||
root,
|
||||
mask_groups: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenience function for creating an [`AnimationGraph`] from a single
|
||||
|
@ -211,7 +270,8 @@ impl AnimationGraph {
|
|||
/// Adds an [`AnimationClip`] to the animation graph with the given weight
|
||||
/// and returns its index.
|
||||
///
|
||||
/// The animation clip will be the child of the given parent.
|
||||
/// The animation clip will be the child of the given parent. The resulting
|
||||
/// node will have no mask.
|
||||
pub fn add_clip(
|
||||
&mut self,
|
||||
clip: Handle<AnimationClip>,
|
||||
|
@ -220,6 +280,27 @@ impl AnimationGraph {
|
|||
) -> AnimationNodeIndex {
|
||||
let node_index = self.graph.add_node(AnimationGraphNode {
|
||||
clip: Some(clip),
|
||||
mask: 0,
|
||||
weight,
|
||||
});
|
||||
self.graph.add_edge(parent, node_index, ());
|
||||
node_index
|
||||
}
|
||||
|
||||
/// Adds an [`AnimationClip`] to the animation graph with the given weight
|
||||
/// and mask, and returns its index.
|
||||
///
|
||||
/// The animation clip will be the child of the given parent.
|
||||
pub fn add_clip_with_mask(
|
||||
&mut self,
|
||||
clip: Handle<AnimationClip>,
|
||||
mask: AnimationMask,
|
||||
weight: f32,
|
||||
parent: AnimationNodeIndex,
|
||||
) -> AnimationNodeIndex {
|
||||
let node_index = self.graph.add_node(AnimationGraphNode {
|
||||
clip: Some(clip),
|
||||
mask,
|
||||
weight,
|
||||
});
|
||||
self.graph.add_edge(parent, node_index, ());
|
||||
|
@ -253,11 +334,37 @@ impl AnimationGraph {
|
|||
///
|
||||
/// 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.
|
||||
/// weights multiplied by the weight of the blend. The blend node will have
|
||||
/// no mask.
|
||||
pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex {
|
||||
let node_index = self
|
||||
.graph
|
||||
.add_node(AnimationGraphNode { clip: None, weight });
|
||||
let node_index = self.graph.add_node(AnimationGraphNode {
|
||||
clip: None,
|
||||
mask: 0,
|
||||
weight,
|
||||
});
|
||||
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.
|
||||
///
|
||||
/// 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`.
|
||||
pub fn add_blend_with_mask(
|
||||
&mut self,
|
||||
mask: AnimationMask,
|
||||
weight: f32,
|
||||
parent: AnimationNodeIndex,
|
||||
) -> AnimationNodeIndex {
|
||||
let node_index = self.graph.add_node(AnimationGraphNode {
|
||||
clip: None,
|
||||
mask,
|
||||
weight,
|
||||
});
|
||||
self.graph.add_edge(parent, node_index, ());
|
||||
node_index
|
||||
}
|
||||
|
@ -313,6 +420,55 @@ impl AnimationGraph {
|
|||
let mut ron_serializer = ron::ser::Serializer::new(writer, None)?;
|
||||
Ok(self.serialize(&mut ron_serializer)?)
|
||||
}
|
||||
|
||||
/// Adds an animation target (bone) to the mask group with the given ID.
|
||||
///
|
||||
/// Calling this method multiple times with the same animation target but
|
||||
/// different mask groups will result in that target being added to all of
|
||||
/// the specified groups.
|
||||
pub fn add_target_to_mask_group(&mut self, target: AnimationTargetId, mask_group: u32) {
|
||||
*self.mask_groups.entry(target).or_default() |= 1 << mask_group;
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationGraphNode {
|
||||
/// Masks out the mask groups specified by the given `mask` bitfield.
|
||||
///
|
||||
/// A 1 in bit position N causes this function to mask out mask group N, and
|
||||
/// thus neither this node nor its descendants will animate any animation
|
||||
/// targets that belong to group N.
|
||||
pub fn add_mask(&mut self, mask: AnimationMask) -> &mut Self {
|
||||
self.mask |= mask;
|
||||
self
|
||||
}
|
||||
|
||||
/// Unmasks the mask groups specified by the given `mask` bitfield.
|
||||
///
|
||||
/// A 1 in bit position N causes this function to unmask mask group N, and
|
||||
/// thus this node and its descendants will be allowed to animate animation
|
||||
/// targets that belong to group N, unless another mask masks those targets
|
||||
/// out.
|
||||
pub fn remove_mask(&mut self, mask: AnimationMask) -> &mut Self {
|
||||
self.mask &= !mask;
|
||||
self
|
||||
}
|
||||
|
||||
/// Masks out the single mask group specified by `group`.
|
||||
///
|
||||
/// After calling this function, neither this node nor its descendants will
|
||||
/// animate any animation targets that belong to the given `group`.
|
||||
pub fn add_mask_group(&mut self, group: u32) -> &mut Self {
|
||||
self.add_mask(1 << group)
|
||||
}
|
||||
|
||||
/// Unmasks the single mask group specified by `group`.
|
||||
///
|
||||
/// After calling this function, this node and its descendants will be
|
||||
/// allowed to animate animation targets that belong to the given `group`,
|
||||
/// unless another mask masks those targets out.
|
||||
pub fn remove_mask_group(&mut self, group: u32) -> &mut Self {
|
||||
self.remove_mask(1 << group)
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<AnimationNodeIndex> for AnimationGraph {
|
||||
|
@ -333,6 +489,7 @@ impl Default for AnimationGraphNode {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
clip: None,
|
||||
mask: 0,
|
||||
weight: 1.0,
|
||||
}
|
||||
}
|
||||
|
@ -377,11 +534,13 @@ impl AssetLoader for AnimationGraphAssetLoader {
|
|||
load_context.load(asset_path)
|
||||
}
|
||||
}),
|
||||
mask: serialized_node.mask,
|
||||
weight: serialized_node.weight,
|
||||
},
|
||||
|_, _| (),
|
||||
),
|
||||
root: serialized_animation_graph.root,
|
||||
mask_groups: serialized_animation_graph.mask_groups,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -399,6 +558,7 @@ impl From<AnimationGraph> for SerializedAnimationGraph {
|
|||
graph: animation_graph.graph.map(
|
||||
|_, node| SerializedAnimationGraphNode {
|
||||
weight: node.weight,
|
||||
mask: node.mask,
|
||||
clip: node.clip.as_ref().map(|clip| match clip.path() {
|
||||
Some(path) => SerializedAnimationClip::AssetPath(path.clone()),
|
||||
None => SerializedAnimationClip::AssetId(clip.id()),
|
||||
|
@ -407,6 +567,7 @@ impl From<AnimationGraph> for SerializedAnimationGraph {
|
|||
|_, _| (),
|
||||
),
|
||||
root: animation_graph.root,
|
||||
mask_groups: animation_graph.mask_groups,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,9 @@ use bevy_utils::{
|
|||
NoOpHash,
|
||||
};
|
||||
use fixedbitset::FixedBitSet;
|
||||
use graph::AnimationMask;
|
||||
use petgraph::{graph::NodeIndex, Direction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thread_local::ThreadLocal;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -226,7 +228,7 @@ pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, NoOpHa
|
|||
/// connected to a bone named `Stomach`.
|
||||
///
|
||||
/// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug, Serialize, Deserialize)]
|
||||
pub struct AnimationTargetId(pub Uuid);
|
||||
|
||||
impl Hash for AnimationTargetId {
|
||||
|
@ -354,6 +356,9 @@ pub struct ActiveAnimation {
|
|||
/// The actual weight of this animation this frame, taking the
|
||||
/// [`AnimationGraph`] into account.
|
||||
computed_weight: f32,
|
||||
/// The mask groups that are masked out (i.e. won't be animated) this frame,
|
||||
/// taking the `AnimationGraph` into account.
|
||||
computed_mask: AnimationMask,
|
||||
repeat: RepeatAnimation,
|
||||
speed: f32,
|
||||
/// Total time the animation has been played.
|
||||
|
@ -375,6 +380,7 @@ impl Default for ActiveAnimation {
|
|||
Self {
|
||||
weight: 1.0,
|
||||
computed_weight: 1.0,
|
||||
computed_mask: 0,
|
||||
repeat: RepeatAnimation::default(),
|
||||
speed: 1.0,
|
||||
elapsed: 0.0,
|
||||
|
@ -573,8 +579,19 @@ pub struct AnimationGraphEvaluator {
|
|||
dfs_stack: Vec<NodeIndex>,
|
||||
/// The list of visited nodes during the depth-first traversal.
|
||||
dfs_visited: FixedBitSet,
|
||||
/// Accumulated weights for each node.
|
||||
weights: Vec<f32>,
|
||||
/// Accumulated weights and masks for each node.
|
||||
nodes: Vec<EvaluatedAnimationGraphNode>,
|
||||
}
|
||||
|
||||
/// The accumulated weight and computed mask for a single node.
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
struct EvaluatedAnimationGraphNode {
|
||||
/// The weight that has been accumulated for this node, taking its
|
||||
/// ancestors' weights into account.
|
||||
weight: f32,
|
||||
/// The mask that has been computed for this node, taking its ancestors'
|
||||
/// masks into account.
|
||||
mask: AnimationMask,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
@ -762,15 +779,17 @@ pub fn advance_animations(
|
|||
|
||||
let node = &animation_graph[node_index];
|
||||
|
||||
// Calculate weight from the graph.
|
||||
let mut weight = node.weight;
|
||||
// Calculate weight and mask from the graph.
|
||||
let (mut weight, mut mask) = (node.weight, node.mask);
|
||||
for parent_index in animation_graph
|
||||
.graph
|
||||
.neighbors_directed(node_index, Direction::Incoming)
|
||||
{
|
||||
weight *= animation_graph[parent_index].weight;
|
||||
let evaluated_parent = &evaluator.nodes[parent_index.index()];
|
||||
weight *= evaluated_parent.weight;
|
||||
mask |= evaluated_parent.mask;
|
||||
}
|
||||
evaluator.weights[node_index.index()] = weight;
|
||||
evaluator.nodes[node_index.index()] = EvaluatedAnimationGraphNode { weight, mask };
|
||||
|
||||
if let Some(active_animation) = active_animations.get_mut(&node_index) {
|
||||
// Tick the animation if necessary.
|
||||
|
@ -787,9 +806,10 @@ pub fn advance_animations(
|
|||
weight *= blend_weight;
|
||||
}
|
||||
|
||||
// Write in the computed weight.
|
||||
// Write in the computed weight and mask for this node.
|
||||
if let Some(active_animation) = active_animations.get_mut(&node_index) {
|
||||
active_animation.computed_weight = weight;
|
||||
active_animation.computed_mask = mask;
|
||||
}
|
||||
|
||||
// Push children.
|
||||
|
@ -848,6 +868,13 @@ pub fn animate_targets(
|
|||
morph_weights,
|
||||
};
|
||||
|
||||
// Determine which mask groups this animation target belongs to.
|
||||
let target_mask = animation_graph
|
||||
.mask_groups
|
||||
.get(&target.id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Apply the animations one after another. The way we accumulate
|
||||
// weights ensures that the order we apply them in doesn't matter.
|
||||
//
|
||||
|
@ -868,7 +895,11 @@ pub fn animate_targets(
|
|||
for (&animation_graph_node_index, active_animation) in
|
||||
animation_player.active_animations.iter()
|
||||
{
|
||||
if active_animation.weight == 0.0 {
|
||||
// If the weight is zero or the current animation target is
|
||||
// masked out, stop here.
|
||||
if active_animation.weight == 0.0
|
||||
|| (target_mask & active_animation.computed_mask) != 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1254,8 +1285,9 @@ impl AnimationGraphEvaluator {
|
|||
self.dfs_visited.grow(node_count);
|
||||
self.dfs_visited.clear();
|
||||
|
||||
self.weights.clear();
|
||||
self.weights.extend(iter::repeat(0.0).take(node_count));
|
||||
self.nodes.clear();
|
||||
self.nodes
|
||||
.extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -187,6 +187,7 @@ Example | Description
|
|||
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
|
||||
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
|
||||
[Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph
|
||||
[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks
|
||||
[Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces
|
||||
[Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve
|
||||
[Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code
|
||||
|
|
365
examples/animation/animation_masks.rs
Normal file
365
examples/animation/animation_masks.rs
Normal file
|
@ -0,0 +1,365 @@
|
|||
//! Demonstrates how to use masks to limit the scope of animations.
|
||||
|
||||
use bevy::{animation::AnimationTargetId, color::palettes::css::WHITE, prelude::*};
|
||||
|
||||
// IDs of the mask groups we define for the running fox model.
|
||||
//
|
||||
// Each mask group defines a set of bones for which animations can be toggled on
|
||||
// and off.
|
||||
const MASK_GROUP_LEFT_FRONT_LEG: u32 = 0;
|
||||
const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 1;
|
||||
const MASK_GROUP_LEFT_HIND_LEG: u32 = 2;
|
||||
const MASK_GROUP_RIGHT_HIND_LEG: u32 = 3;
|
||||
const MASK_GROUP_TAIL: u32 = 4;
|
||||
|
||||
// The width in pixels of the small buttons that allow the user to toggle a mask
|
||||
// group on or off.
|
||||
const MASK_GROUP_SMALL_BUTTON_WIDTH: f32 = 150.0;
|
||||
|
||||
// The ID of the animation in the glTF file that we're going to play.
|
||||
const FOX_RUN_ANIMATION: usize = 2;
|
||||
|
||||
// The names of the bones that each mask group consists of. Each mask group is
|
||||
// defined as a (prefix, suffix) tuple. The mask group consists of a single
|
||||
// bone chain rooted at the prefix. For example, if the chain's prefix is
|
||||
// "A/B/C" and the suffix is "D/E", then the bones that will be included in the
|
||||
// mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E".
|
||||
//
|
||||
// The fact that our mask groups are single chains of bones isn't anything
|
||||
// specific to Bevy; it just so happens to be the case for the model we're
|
||||
// using. A mask group can consist of any set of animation targets, regardless
|
||||
// of whether they form a single chain.
|
||||
const MASK_GROUP_PATHS: [(&str, &str); 5] = [
|
||||
// Left front leg
|
||||
(
|
||||
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09",
|
||||
"b_LeftForeArm_010/b_LeftHand_011",
|
||||
),
|
||||
// Right front leg
|
||||
(
|
||||
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_RightUpperArm_06",
|
||||
"b_RightForeArm_07/b_RightHand_08",
|
||||
),
|
||||
// Left hind leg
|
||||
(
|
||||
"root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015",
|
||||
"b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018",
|
||||
),
|
||||
// Right hind leg
|
||||
(
|
||||
"root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019",
|
||||
"b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022",
|
||||
),
|
||||
// Tail
|
||||
(
|
||||
"root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012",
|
||||
"b_Tail02_013/b_Tail03_014",
|
||||
),
|
||||
];
|
||||
|
||||
// A component that identifies a clickable button that allows the user to toggle
|
||||
// a mask group on or off.
|
||||
#[derive(Component)]
|
||||
struct MaskGroupControl {
|
||||
// The ID of the mask group that this button controls.
|
||||
group_id: u32,
|
||||
|
||||
// Whether animations are playing for this mask group.
|
||||
//
|
||||
// Note that this is the opposite of the `mask` field in `AnimationGraph`:
|
||||
// i.e. it's true if the group is *not* presently masked, and false if the
|
||||
// group *is* masked.
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
// The application entry point.
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
title: "Bevy Animation Masks Example".into(),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}))
|
||||
.add_systems(Startup, (setup_scene, setup_ui))
|
||||
.add_systems(Update, setup_animation_graph_once_loaded)
|
||||
.add_systems(Update, handle_button_toggles)
|
||||
.insert_resource(AmbientLight {
|
||||
color: WHITE.into(),
|
||||
brightness: 100.0,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
// Spawns the 3D objects in the scene, and loads the fox animation from the glTF
|
||||
// file.
|
||||
fn setup_scene(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
// Spawn the camera.
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-15.0, 10.0, 20.0)
|
||||
.looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
|
||||
// Spawn the light.
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
intensity: 10_000_000.0,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(-4.0, 8.0, 13.0),
|
||||
..default()
|
||||
});
|
||||
|
||||
// Spawn the fox.
|
||||
commands.spawn(SceneBundle {
|
||||
scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
|
||||
transform: Transform::from_scale(Vec3::splat(0.07)),
|
||||
..default()
|
||||
});
|
||||
|
||||
// Spawn the ground.
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Circle::new(7.0)),
|
||||
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
|
||||
transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
// Creates the UI.
|
||||
fn setup_ui(mut commands: Commands) {
|
||||
// Add help text.
|
||||
commands.spawn(
|
||||
TextBundle::from_section(
|
||||
"Click on a button to toggle animations for its associated bones",
|
||||
TextStyle::default(),
|
||||
)
|
||||
.with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(10.0),
|
||||
top: Val::Px(10.0),
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
|
||||
// Add the buttons that allow the user to toggle mask groups on and off.
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
position_type: PositionType::Absolute,
|
||||
row_gap: Val::Px(6.0),
|
||||
left: Val::Px(10.0),
|
||||
bottom: Val::Px(10.0),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
let row_style = Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
column_gap: Val::Px(6.0),
|
||||
..default()
|
||||
};
|
||||
|
||||
parent
|
||||
.spawn(NodeBundle {
|
||||
style: row_style.clone(),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
add_mask_group_control(
|
||||
parent,
|
||||
"Left Front Leg",
|
||||
Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
|
||||
MASK_GROUP_LEFT_FRONT_LEG,
|
||||
);
|
||||
add_mask_group_control(
|
||||
parent,
|
||||
"Right Front Leg",
|
||||
Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
|
||||
MASK_GROUP_RIGHT_FRONT_LEG,
|
||||
);
|
||||
});
|
||||
|
||||
parent
|
||||
.spawn(NodeBundle {
|
||||
style: row_style,
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
add_mask_group_control(
|
||||
parent,
|
||||
"Left Hind Leg",
|
||||
Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
|
||||
MASK_GROUP_LEFT_HIND_LEG,
|
||||
);
|
||||
add_mask_group_control(
|
||||
parent,
|
||||
"Right Hind Leg",
|
||||
Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH),
|
||||
MASK_GROUP_RIGHT_HIND_LEG,
|
||||
);
|
||||
});
|
||||
|
||||
add_mask_group_control(parent, "Tail", Val::Auto, MASK_GROUP_TAIL);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds a button that allows the user to toggle a mask group on and off.
|
||||
//
|
||||
// The button will automatically become a child of the parent that owns the
|
||||
// given `ChildBuilder`.
|
||||
fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, mask_group_id: u32) {
|
||||
parent
|
||||
.spawn(ButtonBundle {
|
||||
style: Style {
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
width,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::all(Val::Px(6.0)),
|
||||
margin: UiRect::ZERO,
|
||||
..default()
|
||||
},
|
||||
border_color: BorderColor(Color::WHITE),
|
||||
border_radius: BorderRadius::all(Val::Px(3.0)),
|
||||
background_color: Color::WHITE.into(),
|
||||
..default()
|
||||
})
|
||||
.insert(MaskGroupControl {
|
||||
group_id: mask_group_id,
|
||||
enabled: true,
|
||||
})
|
||||
.with_child(TextBundle::from_section(
|
||||
label,
|
||||
TextStyle {
|
||||
font_size: 14.0,
|
||||
color: Color::BLACK,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Builds up the animation graph, including the mask groups, and adds it to the
|
||||
// entity with the `AnimationPlayer` that the glTF loader created.
|
||||
fn setup_animation_graph_once_loaded(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
) {
|
||||
for (entity, mut player) in &mut players {
|
||||
// Load the animation clip from the glTF file.
|
||||
let (mut animation_graph, node_index) = AnimationGraph::from_clip(asset_server.load(
|
||||
GltfAssetLabel::Animation(FOX_RUN_ANIMATION).from_asset("models/animated/Fox.glb"),
|
||||
));
|
||||
|
||||
// Create each mask group.
|
||||
for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in
|
||||
MASK_GROUP_PATHS.iter().enumerate()
|
||||
{
|
||||
// Split up the prefix and suffix, and convert them into `Name`s.
|
||||
let prefix: Vec<_> = mask_group_prefix.split('/').map(Name::new).collect();
|
||||
let suffix: Vec<_> = mask_group_suffix.split('/').map(Name::new).collect();
|
||||
|
||||
// Add each bone in the chain to the appropriate mask group.
|
||||
for chain_length in 0..=suffix.len() {
|
||||
let animation_target_id = AnimationTargetId::from_names(
|
||||
prefix.iter().chain(suffix[0..chain_length].iter()),
|
||||
);
|
||||
animation_graph
|
||||
.add_target_to_mask_group(animation_target_id, mask_group_index as u32);
|
||||
}
|
||||
}
|
||||
|
||||
// We're doing constructing the animation graph. Add it as an asset.
|
||||
let animation_graph = animation_graphs.add(animation_graph);
|
||||
commands.entity(entity).insert(animation_graph);
|
||||
|
||||
// Finally, play the animation.
|
||||
player.play(node_index).repeat();
|
||||
}
|
||||
}
|
||||
|
||||
// A system that handles requests from the user to toggle mask groups on and
|
||||
// off.
|
||||
fn handle_button_toggles(
|
||||
mut interactions: Query<
|
||||
(
|
||||
&Interaction,
|
||||
&mut MaskGroupControl,
|
||||
&mut BackgroundColor,
|
||||
&Children,
|
||||
),
|
||||
Changed<Interaction>,
|
||||
>,
|
||||
mut texts: Query<&mut Text>,
|
||||
mut animation_players: Query<(&Handle<AnimationGraph>, &AnimationPlayer)>,
|
||||
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
for (interaction, mut mask_group_control, mut button_background_color, children) in
|
||||
interactions.iter_mut()
|
||||
{
|
||||
// We only care about press events.
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Toggle the state of the mask.
|
||||
mask_group_control.enabled = !mask_group_control.enabled;
|
||||
|
||||
// Update the background color of the button.
|
||||
button_background_color.0 = if mask_group_control.enabled {
|
||||
Color::WHITE
|
||||
} else {
|
||||
Color::BLACK
|
||||
};
|
||||
|
||||
// Update the text color of the button.
|
||||
for &kid in children.iter() {
|
||||
if let Ok(mut text) = texts.get_mut(kid) {
|
||||
for section in &mut text.sections {
|
||||
section.style.color = if mask_group_control.enabled {
|
||||
Color::BLACK
|
||||
} else {
|
||||
Color::WHITE
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now grab the animation player. (There's only one in our case, but we
|
||||
// iterate just for clarity's sake.)
|
||||
for (animation_graph_handle, animation_player) in animation_players.iter_mut() {
|
||||
// The animation graph needs to have loaded.
|
||||
let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Grab the animation graph node that's currently playing.
|
||||
let Some((&animation_node_index, _)) = animation_player.playing_animations().next()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(animation_node) = animation_graph.get_mut(animation_node_index) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Enable or disable the mask group as appropriate.
|
||||
if mask_group_control.enabled {
|
||||
animation_node.mask &= !(1 << mask_group_control.group_id);
|
||||
} else {
|
||||
animation_node.mask |= 1 << mask_group_control.group_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue