diff --git a/Cargo.toml b/Cargo.toml index e8101a191c..3708e1d45b 100644 --- a/Cargo.toml +++ b/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" diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index a1b21f1254..cf87b1400e 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -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: {}, ) \ No newline at end of file diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 7154a77e97..6f04ca794f 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -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, } /// 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>, + /// 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, /// Corresponds to the `root` field on [`AnimationGraph`]. pub root: NodeIndex, + /// Corresponds to the `mask_groups` field on [`AnimationGraph`]. + pub mask_groups: HashMap, } /// 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, + /// 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), } +/// 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, @@ -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, + 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 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 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 for SerializedAnimationGraph { |_, _| (), ), root: animation_graph.root, + mask_groups: animation_graph.mask_groups, } } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index f1382906c7..e3ba7836f7 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -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, 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, /// The list of visited nodes during the depth-first traversal. dfs_visited: FixedBitSet, - /// Accumulated weights for each node. - weights: Vec, + /// Accumulated weights and masks for each node. + nodes: Vec, +} + +/// 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)); } } diff --git a/examples/README.md b/examples/README.md index f2ee223b37..cadf2f7a54 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs new file mode 100644 index 0000000000..c36ccc7eae --- /dev/null +++ b/examples/animation/animation_masks.rs @@ -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, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // 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, + mut animation_graphs: ResMut>, + mut players: Query<(Entity, &mut AnimationPlayer), Added>, +) { + 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, + >, + mut texts: Query<&mut Text>, + mut animation_players: Query<(&Handle, &AnimationPlayer)>, + mut animation_graphs: ResMut>, +) { + 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; + } + } + } +}