Skip to main content

animation_masks/
animation_masks.rs

1//! Demonstrates how to use masks to limit the scope of animations.
2
3use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
4use bevy::{
5    animation::{AnimatedBy, AnimationTargetId},
6    color::palettes::css::WHITE,
7    feathers::{dark_theme::create_dark_theme, display::label, theme::UiTheme, FeathersPlugins},
8    prelude::*,
9    ui_widgets::{radio_self_update, ValueChange},
10};
11use std::collections::HashSet;
12
13#[path = "../helpers/radio.rs"]
14mod radio;
15
16// IDs of the mask groups we define for the running fox model.
17//
18// Each mask group defines a set of bones for which animations can be toggled on
19// and off.
20const MASK_GROUP_HEAD: u32 = 0;
21const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1;
22const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2;
23const MASK_GROUP_LEFT_HIND_LEG: u32 = 3;
24const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4;
25const MASK_GROUP_TAIL: u32 = 5;
26
27// The names of the bones that each mask group consists of. Each mask group is
28// defined as a (prefix, suffix) tuple. The mask group consists of a single
29// bone chain rooted at the prefix. For example, if the chain's prefix is
30// "A/B/C" and the suffix is "D/E", then the bones that will be included in the
31// mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E".
32//
33// The fact that our mask groups are single chains of bones isn't an engine
34// requirement; it just so happens to be the case for the model we're using. A
35// mask group can consist of any set of animation targets, regardless of whether
36// they form a single chain.
37const MASK_GROUP_PATHS: [(&str, &str); 6] = [
38    // Head
39    (
40        "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03",
41        "b_Neck_04/b_Head_05",
42    ),
43    // Left front leg
44    (
45        "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09",
46        "b_LeftForeArm_010/b_LeftHand_011",
47    ),
48    // Right front leg
49    (
50        "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_RightUpperArm_06",
51        "b_RightForeArm_07/b_RightHand_08",
52    ),
53    // Left hind leg
54    (
55        "root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015",
56        "b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018",
57    ),
58    // Right hind leg
59    (
60        "root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019",
61        "b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022",
62    ),
63    // Tail
64    (
65        "root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012",
66        "b_Tail02_013/b_Tail03_014",
67    ),
68];
69
70/// Identifies an animation for a specific mask group that the user
71/// can select.
72#[derive(Clone, Copy, Component, Default)]
73struct AnimationControl {
74    // The ID of the mask group that this button controls.
75    group_id: u32,
76    label: AnimationLabel,
77}
78
79impl AnimationControl {
80    fn new(group_id: u32, label: AnimationLabel) -> Self {
81        Self { group_id, label }
82    }
83}
84
85/// The four types of animations per mask group
86#[derive(Clone, Copy, Component, PartialEq, Debug, Default)]
87enum AnimationLabel {
88    #[default]
89    Idle = 0,
90    Walk = 1,
91    Run = 2,
92    Off = 3,
93}
94
95impl AnimationLabel {
96    fn label(&self) -> &'static str {
97        match self {
98            Self::Idle => "Idle",
99            Self::Walk => "Walk",
100            Self::Run => "Run",
101            Self::Off => "Off",
102        }
103    }
104}
105
106#[derive(Clone, Debug, Resource)]
107struct AnimationNodes([AnimationNodeIndex; 3]);
108
109// The application entry point.
110fn main() {
111    App::new()
112        .add_plugins((
113            DefaultPlugins.set(WindowPlugin {
114                primary_window: Some(Window {
115                    title: "Bevy Animation Masks Example".into(),
116                    ..default()
117                }),
118                ..default()
119            }),
120            FeathersPlugins,
121        ))
122        .insert_resource(UiTheme(create_dark_theme()))
123        .add_systems(Startup, (setup_scene, setup_ui))
124        .add_systems(Update, setup_animation_graph_once_loaded)
125        .add_observer(handle_animation_control_change)
126        .add_observer(radio_self_update)
127        .insert_resource(GlobalAmbientLight {
128            color: WHITE.into(),
129            brightness: 100.0,
130            ..default()
131        })
132        .run();
133}
134
135// Spawns the 3D objects in the scene, and loads the fox animation from the glTF
136// file.
137fn setup_scene(
138    mut commands: Commands,
139    asset_server: Res<AssetServer>,
140    mut meshes: ResMut<Assets<Mesh>>,
141    mut materials: ResMut<Assets<StandardMaterial>>,
142) {
143    // Spawn the camera.
144    commands.spawn((
145        Camera3d::default(),
146        Transform::from_xyz(-15.0, 10.0, 20.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
147    ));
148
149    // Spawn the light.
150    commands.spawn((
151        PointLight {
152            intensity: 10_000_000.0,
153            shadow_maps_enabled: true,
154            ..default()
155        },
156        Transform::from_xyz(-4.0, 8.0, 13.0),
157    ));
158
159    // Spawn the fox.
160    commands.spawn((
161        WorldAssetRoot(
162            asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
163        ),
164        Transform::from_scale(Vec3::splat(0.07)),
165    ));
166
167    // Spawn the ground.
168    commands.spawn((
169        Mesh3d(meshes.add(Circle::new(7.0))),
170        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
171        Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
172    ));
173}
174
175// Creates the UI.
176fn setup_ui(mut commands: Commands) {
177    // Add help text.
178    commands.spawn_scene(bsn! {
179        Node {
180            position_type: PositionType::Absolute,
181            left: px(12),
182            top: px(12),
183        }
184        Children [
185            Text("Click on a button to toggle animations for its associated bones")
186        ]
187    });
188
189    // Add the buttons that allow the user to toggle mask groups on and off.
190    commands.spawn_scene(bsn! {
191        @main_ui_node_scene()
192        Node {
193            align_items: AlignItems::Start,
194        }
195        Children [
196            @feathers_option_buttons("Head", &make_animation_controls(MASK_GROUP_HEAD), 2)
197            --
198            @label("--")
199            --
200            @feathers_option_buttons("Front Left Leg", &make_animation_controls(MASK_GROUP_LEFT_FRONT_LEG), 2)
201            --
202            @feathers_option_buttons("Front Right Leg", &make_animation_controls(MASK_GROUP_RIGHT_FRONT_LEG), 2)
203            --
204            @label("--")
205            --
206            @feathers_option_buttons("Hind Left Leg", &make_animation_controls(MASK_GROUP_LEFT_HIND_LEG), 2)
207            --
208            @feathers_option_buttons("Hind Right Leg", &make_animation_controls(MASK_GROUP_RIGHT_HIND_LEG), 2)
209            --
210            @label("--")
211            --
212            @feathers_option_buttons("Tail", &make_animation_controls(MASK_GROUP_TAIL), 2)
213        ]
214    });
215}
216
217// Makes the Radio Button Options for a given animation group.
218fn make_animation_controls(group_id: u32) -> [(AnimationControl, &'static str); 4] {
219    [
220        (
221            AnimationControl::new(group_id, AnimationLabel::Run),
222            AnimationLabel::Run.label(),
223        ),
224        (
225            AnimationControl::new(group_id, AnimationLabel::Walk),
226            AnimationLabel::Walk.label(),
227        ),
228        (
229            AnimationControl::new(group_id, AnimationLabel::Idle),
230            AnimationLabel::Idle.label(),
231        ),
232        (
233            AnimationControl::new(group_id, AnimationLabel::Off),
234            AnimationLabel::Off.label(),
235        ),
236    ]
237}
238
239// Builds up the animation graph, including the mask groups, and adds it to the
240// entity with the `AnimationPlayer` that the glTF loader created.
241fn setup_animation_graph_once_loaded(
242    mut commands: Commands,
243    asset_server: Res<AssetServer>,
244    mut animation_graphs: ResMut<Assets<AnimationGraph>>,
245    mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
246    targets: Query<(Entity, &AnimationTargetId)>,
247) {
248    for (entity, mut player) in &mut players {
249        // Load the animation clip from the glTF file.
250        let mut animation_graph = AnimationGraph::new();
251        let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root);
252
253        let animation_graph_nodes: [AnimationNodeIndex; 3] =
254            std::array::from_fn(|animation_index| {
255                let handle = asset_server.load(
256                    GltfAssetLabel::Animation(animation_index)
257                        .from_asset("models/animated/Fox.glb"),
258                );
259                let mask = if animation_index == 0 { 0 } else { 0x3f };
260                animation_graph.add_clip_with_mask(handle, mask, 1.0, blend_node)
261            });
262
263        // Create each mask group.
264        let mut all_animation_target_ids = HashSet::new();
265        for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in
266            MASK_GROUP_PATHS.iter().enumerate()
267        {
268            // Split up the prefix and suffix, and convert them into `Name`s.
269            let prefix: Vec<_> = mask_group_prefix.split('/').map(Name::new).collect();
270            let suffix: Vec<_> = mask_group_suffix.split('/').map(Name::new).collect();
271
272            // Add each bone in the chain to the appropriate mask group.
273            for chain_length in 0..=suffix.len() {
274                let animation_target_id = AnimationTargetId::from_names(
275                    prefix.iter().chain(suffix[0..chain_length].iter()),
276                );
277                animation_graph
278                    .add_target_to_mask_group(animation_target_id, mask_group_index as u32);
279                all_animation_target_ids.insert(animation_target_id);
280            }
281        }
282
283        // We're doing constructing the animation graph. Add it as an asset.
284        let animation_graph = animation_graphs.add(animation_graph);
285        commands
286            .entity(entity)
287            .insert(AnimationGraphHandle(animation_graph));
288
289        // Remove animation targets that aren't in any of the mask groups. If we
290        // don't do that, those bones will play all animations at once, which is
291        // ugly.
292        for (target_entity, target) in &targets {
293            if !all_animation_target_ids.contains(target) {
294                commands
295                    .entity(target_entity)
296                    .remove::<AnimationTargetId>()
297                    .remove::<AnimatedBy>();
298            }
299        }
300
301        // Play the animation.
302        for animation_graph_node in animation_graph_nodes {
303            player.play(animation_graph_node).repeat();
304        }
305
306        // Record the graph nodes.
307        commands.insert_resource(AnimationNodes(animation_graph_nodes));
308    }
309}
310
311// An observer that handles requests from the user to toggle mask groups on and
312// off.
313fn handle_animation_control_change(
314    event: On<ValueChange<Entity>>,
315    new_value_query: Query<&RadioButtonOptionValue<AnimationControl>>,
316    mut animation_players: Query<&AnimationGraphHandle, With<AnimationPlayer>>,
317    mut animation_graphs: ResMut<Assets<AnimationGraph>>,
318    mut animation_nodes: Option<ResMut<AnimationNodes>>,
319) {
320    let Some(ref mut animation_nodes) = animation_nodes else {
321        return;
322    };
323
324    let Ok(RadioButtonOptionValue(animation_control)) = new_value_query.get(event.value) else {
325        return;
326    };
327
328    // Grab the animation player. (There's only one in our case, but we
329    // iterate just for clarity's sake.)
330    for animation_graph_handle in animation_players.iter_mut() {
331        // The animation graph needs to have loaded.
332        let Some(mut animation_graph) = animation_graphs.get_mut(animation_graph_handle) else {
333            continue;
334        };
335
336        for (clip_index, &animation_node_index) in animation_nodes.0.iter().enumerate() {
337            let Some(animation_node) = animation_graph.get_mut(animation_node_index) else {
338                continue;
339            };
340
341            if animation_control.label as usize == clip_index {
342                animation_node.mask &= !(1 << animation_control.group_id);
343            } else {
344                animation_node.mask |= 1 << animation_control.group_id;
345            }
346        }
347    }
348}