docs/bevy/examples/animation/animation_masks.rs 11.8 K · 348 lines · raw · history

1 //! Demonstrates how to use masks to limit the scope of animations.
2
3 use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
4 use 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 };
11 use std::collections::HashSet;
12
13 #[path = "../helpers/radio.rs"]
14 mod 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.
20 const MASK_GROUP_HEAD: u32 = 0;
21 const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1;
22 const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2;
23 const MASK_GROUP_LEFT_HIND_LEG: u32 = 3;
24 const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4;
25 const 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.
37 const 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)]
73 struct AnimationControl {
74 // The ID of the mask group that this button controls.
75 group_id: u32,
76 label: AnimationLabel,
77 }
78
79 impl 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)]
87 enum AnimationLabel {
88 #[default]
89 Idle = 0,
90 Walk = 1,
91 Run = 2,
92 Off = 3,
93 }
94
95 impl 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)]
107 struct AnimationNodes([AnimationNodeIndex; 3]);
108
109 // The application entry point.
110 fn 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.
137 fn 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.
176 fn 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.
218 fn 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.
241 fn 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.
313 fn 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 }