Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/animation/animation_masks.rs
6592 views
1
//! Demonstrates how to use masks to limit the scope of animations.
2
3
use bevy::{
4
animation::{AnimationTarget, AnimationTargetId},
5
color::palettes::css::{LIGHT_GRAY, WHITE},
6
prelude::*,
7
};
8
use std::collections::HashSet;
9
10
// IDs of the mask groups we define for the running fox model.
11
//
12
// Each mask group defines a set of bones for which animations can be toggled on
13
// and off.
14
const MASK_GROUP_HEAD: u32 = 0;
15
const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1;
16
const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2;
17
const MASK_GROUP_LEFT_HIND_LEG: u32 = 3;
18
const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4;
19
const MASK_GROUP_TAIL: u32 = 5;
20
21
// The width in pixels of the small buttons that allow the user to toggle a mask
22
// group on or off.
23
const MASK_GROUP_BUTTON_WIDTH: f32 = 250.0;
24
25
// The names of the bones that each mask group consists of. Each mask group is
26
// defined as a (prefix, suffix) tuple. The mask group consists of a single
27
// bone chain rooted at the prefix. For example, if the chain's prefix is
28
// "A/B/C" and the suffix is "D/E", then the bones that will be included in the
29
// mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E".
30
//
31
// The fact that our mask groups are single chains of bones isn't an engine
32
// requirement; it just so happens to be the case for the model we're using. A
33
// mask group can consist of any set of animation targets, regardless of whether
34
// they form a single chain.
35
const MASK_GROUP_PATHS: [(&str, &str); 6] = [
36
// Head
37
(
38
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03",
39
"b_Neck_04/b_Head_05",
40
),
41
// Left front leg
42
(
43
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09",
44
"b_LeftForeArm_010/b_LeftHand_011",
45
),
46
// Right front leg
47
(
48
"root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_RightUpperArm_06",
49
"b_RightForeArm_07/b_RightHand_08",
50
),
51
// Left hind leg
52
(
53
"root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015",
54
"b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018",
55
),
56
// Right hind leg
57
(
58
"root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019",
59
"b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022",
60
),
61
// Tail
62
(
63
"root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012",
64
"b_Tail02_013/b_Tail03_014",
65
),
66
];
67
68
#[derive(Clone, Copy, Component)]
69
struct AnimationControl {
70
// The ID of the mask group that this button controls.
71
group_id: u32,
72
label: AnimationLabel,
73
}
74
75
#[derive(Clone, Copy, Component, PartialEq, Debug)]
76
enum AnimationLabel {
77
Idle = 0,
78
Walk = 1,
79
Run = 2,
80
Off = 3,
81
}
82
83
#[derive(Clone, Debug, Resource)]
84
struct AnimationNodes([AnimationNodeIndex; 3]);
85
86
#[derive(Clone, Copy, Debug, Resource)]
87
struct AppState([MaskGroupState; 6]);
88
89
#[derive(Clone, Copy, Debug)]
90
struct MaskGroupState {
91
clip: u8,
92
}
93
94
// The application entry point.
95
fn main() {
96
App::new()
97
.add_plugins(DefaultPlugins.set(WindowPlugin {
98
primary_window: Some(Window {
99
title: "Bevy Animation Masks Example".into(),
100
..default()
101
}),
102
..default()
103
}))
104
.add_systems(Startup, (setup_scene, setup_ui))
105
.add_systems(Update, setup_animation_graph_once_loaded)
106
.add_systems(Update, handle_button_toggles)
107
.add_systems(Update, update_ui)
108
.insert_resource(AmbientLight {
109
color: WHITE.into(),
110
brightness: 100.0,
111
..default()
112
})
113
.init_resource::<AppState>()
114
.run();
115
}
116
117
// Spawns the 3D objects in the scene, and loads the fox animation from the glTF
118
// file.
119
fn setup_scene(
120
mut commands: Commands,
121
asset_server: Res<AssetServer>,
122
mut meshes: ResMut<Assets<Mesh>>,
123
mut materials: ResMut<Assets<StandardMaterial>>,
124
) {
125
// Spawn the camera.
126
commands.spawn((
127
Camera3d::default(),
128
Transform::from_xyz(-15.0, 10.0, 20.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
129
));
130
131
// Spawn the light.
132
commands.spawn((
133
PointLight {
134
intensity: 10_000_000.0,
135
shadows_enabled: true,
136
..default()
137
},
138
Transform::from_xyz(-4.0, 8.0, 13.0),
139
));
140
141
// Spawn the fox.
142
commands.spawn((
143
SceneRoot(
144
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
145
),
146
Transform::from_scale(Vec3::splat(0.07)),
147
));
148
149
// Spawn the ground.
150
commands.spawn((
151
Mesh3d(meshes.add(Circle::new(7.0))),
152
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
153
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
154
));
155
}
156
157
// Creates the UI.
158
fn setup_ui(mut commands: Commands) {
159
// Add help text.
160
commands.spawn((
161
Text::new("Click on a button to toggle animations for its associated bones"),
162
Node {
163
position_type: PositionType::Absolute,
164
left: px(12),
165
top: px(12),
166
..default()
167
},
168
));
169
170
// Add the buttons that allow the user to toggle mask groups on and off.
171
commands.spawn((
172
Node {
173
flex_direction: FlexDirection::Column,
174
position_type: PositionType::Absolute,
175
row_gap: px(6),
176
left: px(12),
177
bottom: px(12),
178
..default()
179
},
180
children![
181
new_mask_group_control("Head", auto(), MASK_GROUP_HEAD),
182
(
183
Node {
184
flex_direction: FlexDirection::Row,
185
column_gap: px(6),
186
..default()
187
},
188
children![
189
new_mask_group_control(
190
"Left Front Leg",
191
px(MASK_GROUP_BUTTON_WIDTH),
192
MASK_GROUP_LEFT_FRONT_LEG,
193
),
194
new_mask_group_control(
195
"Right Front Leg",
196
px(MASK_GROUP_BUTTON_WIDTH),
197
MASK_GROUP_RIGHT_FRONT_LEG,
198
)
199
],
200
),
201
(
202
Node {
203
flex_direction: FlexDirection::Row,
204
column_gap: px(6),
205
..default()
206
},
207
children![
208
new_mask_group_control(
209
"Left Hind Leg",
210
px(MASK_GROUP_BUTTON_WIDTH),
211
MASK_GROUP_LEFT_HIND_LEG,
212
),
213
new_mask_group_control(
214
"Right Hind Leg",
215
px(MASK_GROUP_BUTTON_WIDTH),
216
MASK_GROUP_RIGHT_HIND_LEG,
217
)
218
]
219
),
220
new_mask_group_control("Tail", auto(), MASK_GROUP_TAIL),
221
],
222
));
223
}
224
225
// Adds a button that allows the user to toggle a mask group on and off.
226
//
227
// The button will automatically become a child of the parent that owns the
228
// given `ChildSpawnerCommands`.
229
fn new_mask_group_control(label: &str, width: Val, mask_group_id: u32) -> impl Bundle {
230
let button_text_style = (
231
TextFont {
232
font_size: 14.0,
233
..default()
234
},
235
TextColor::WHITE,
236
);
237
let selected_button_text_style = (button_text_style.0.clone(), TextColor::BLACK);
238
let label_text_style = (
239
button_text_style.0.clone(),
240
TextColor(Color::Srgba(LIGHT_GRAY)),
241
);
242
243
let make_animation_label = {
244
let button_text_style = button_text_style.clone();
245
let selected_button_text_style = selected_button_text_style.clone();
246
move |first: bool, label: AnimationLabel| {
247
(
248
Button,
249
BackgroundColor(if !first { Color::BLACK } else { Color::WHITE }),
250
Node {
251
flex_grow: 1.0,
252
border: if !first {
253
UiRect::left(px(1))
254
} else {
255
UiRect::ZERO
256
},
257
..default()
258
},
259
BorderColor::all(Color::WHITE),
260
AnimationControl {
261
group_id: mask_group_id,
262
label,
263
},
264
children![(
265
Text(format!("{label:?}")),
266
if !first {
267
button_text_style.clone()
268
} else {
269
selected_button_text_style.clone()
270
},
271
TextLayout::new_with_justify(Justify::Center),
272
Node {
273
flex_grow: 1.0,
274
margin: UiRect::vertical(px(3)),
275
..default()
276
},
277
)],
278
)
279
}
280
};
281
282
(
283
Node {
284
border: UiRect::all(px(1)),
285
width,
286
flex_direction: FlexDirection::Column,
287
justify_content: JustifyContent::Center,
288
align_items: AlignItems::Center,
289
padding: UiRect::ZERO,
290
margin: UiRect::ZERO,
291
..default()
292
},
293
BorderColor::all(Color::WHITE),
294
BorderRadius::all(px(3)),
295
BackgroundColor(Color::BLACK),
296
children![
297
(
298
Node {
299
border: UiRect::ZERO,
300
width: percent(100),
301
justify_content: JustifyContent::Center,
302
align_items: AlignItems::Center,
303
padding: UiRect::ZERO,
304
margin: UiRect::ZERO,
305
..default()
306
},
307
BackgroundColor(Color::BLACK),
308
children![(
309
Text::new(label),
310
label_text_style.clone(),
311
Node {
312
margin: UiRect::vertical(px(3)),
313
..default()
314
},
315
)]
316
),
317
(
318
Node {
319
width: percent(100),
320
flex_direction: FlexDirection::Row,
321
justify_content: JustifyContent::Center,
322
align_items: AlignItems::Center,
323
border: UiRect::top(px(1)),
324
..default()
325
},
326
BorderColor::all(Color::WHITE),
327
children![
328
make_animation_label(true, AnimationLabel::Run),
329
make_animation_label(false, AnimationLabel::Walk),
330
make_animation_label(false, AnimationLabel::Idle),
331
make_animation_label(false, AnimationLabel::Off),
332
]
333
)
334
],
335
)
336
}
337
338
// Builds up the animation graph, including the mask groups, and adds it to the
339
// entity with the `AnimationPlayer` that the glTF loader created.
340
fn setup_animation_graph_once_loaded(
341
mut commands: Commands,
342
asset_server: Res<AssetServer>,
343
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
344
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
345
targets: Query<(Entity, &AnimationTarget)>,
346
) {
347
for (entity, mut player) in &mut players {
348
// Load the animation clip from the glTF file.
349
let mut animation_graph = AnimationGraph::new();
350
let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root);
351
352
let animation_graph_nodes: [AnimationNodeIndex; 3] =
353
std::array::from_fn(|animation_index| {
354
let handle = asset_server.load(
355
GltfAssetLabel::Animation(animation_index)
356
.from_asset("models/animated/Fox.glb"),
357
);
358
let mask = if animation_index == 0 { 0 } else { 0x3f };
359
animation_graph.add_clip_with_mask(handle, mask, 1.0, blend_node)
360
});
361
362
// Create each mask group.
363
let mut all_animation_target_ids = HashSet::new();
364
for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in
365
MASK_GROUP_PATHS.iter().enumerate()
366
{
367
// Split up the prefix and suffix, and convert them into `Name`s.
368
let prefix: Vec<_> = mask_group_prefix.split('/').map(Name::new).collect();
369
let suffix: Vec<_> = mask_group_suffix.split('/').map(Name::new).collect();
370
371
// Add each bone in the chain to the appropriate mask group.
372
for chain_length in 0..=suffix.len() {
373
let animation_target_id = AnimationTargetId::from_names(
374
prefix.iter().chain(suffix[0..chain_length].iter()),
375
);
376
animation_graph
377
.add_target_to_mask_group(animation_target_id, mask_group_index as u32);
378
all_animation_target_ids.insert(animation_target_id);
379
}
380
}
381
382
// We're doing constructing the animation graph. Add it as an asset.
383
let animation_graph = animation_graphs.add(animation_graph);
384
commands
385
.entity(entity)
386
.insert(AnimationGraphHandle(animation_graph));
387
388
// Remove animation targets that aren't in any of the mask groups. If we
389
// don't do that, those bones will play all animations at once, which is
390
// ugly.
391
for (target_entity, target) in &targets {
392
if !all_animation_target_ids.contains(&target.id) {
393
commands.entity(target_entity).remove::<AnimationTarget>();
394
}
395
}
396
397
// Play the animation.
398
for animation_graph_node in animation_graph_nodes {
399
player.play(animation_graph_node).repeat();
400
}
401
402
// Record the graph nodes.
403
commands.insert_resource(AnimationNodes(animation_graph_nodes));
404
}
405
}
406
407
// A system that handles requests from the user to toggle mask groups on and
408
// off.
409
fn handle_button_toggles(
410
mut interactions: Query<(&Interaction, &mut AnimationControl), Changed<Interaction>>,
411
mut animation_players: Query<&AnimationGraphHandle, With<AnimationPlayer>>,
412
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
413
mut animation_nodes: Option<ResMut<AnimationNodes>>,
414
mut app_state: ResMut<AppState>,
415
) {
416
let Some(ref mut animation_nodes) = animation_nodes else {
417
return;
418
};
419
420
for (interaction, animation_control) in interactions.iter_mut() {
421
// We only care about press events.
422
if *interaction != Interaction::Pressed {
423
continue;
424
}
425
426
// Toggle the state of the clip.
427
app_state.0[animation_control.group_id as usize].clip = animation_control.label as u8;
428
429
// Now grab the animation player. (There's only one in our case, but we
430
// iterate just for clarity's sake.)
431
for animation_graph_handle in animation_players.iter_mut() {
432
// The animation graph needs to have loaded.
433
let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else {
434
continue;
435
};
436
437
for (clip_index, &animation_node_index) in animation_nodes.0.iter().enumerate() {
438
let Some(animation_node) = animation_graph.get_mut(animation_node_index) else {
439
continue;
440
};
441
442
if animation_control.label as usize == clip_index {
443
animation_node.mask &= !(1 << animation_control.group_id);
444
} else {
445
animation_node.mask |= 1 << animation_control.group_id;
446
}
447
}
448
}
449
}
450
}
451
452
// A system that updates the UI based on the current app state.
453
fn update_ui(
454
mut animation_controls: Query<(&AnimationControl, &mut BackgroundColor, &Children)>,
455
texts: Query<Entity, With<Text>>,
456
mut writer: TextUiWriter,
457
app_state: Res<AppState>,
458
) {
459
for (animation_control, mut background_color, kids) in animation_controls.iter_mut() {
460
let enabled =
461
app_state.0[animation_control.group_id as usize].clip == animation_control.label as u8;
462
463
*background_color = if enabled {
464
BackgroundColor(Color::WHITE)
465
} else {
466
BackgroundColor(Color::BLACK)
467
};
468
469
for &kid in kids {
470
let Ok(text) = texts.get(kid) else {
471
continue;
472
};
473
474
writer.for_each_color(text, |mut color| {
475
color.0 = if enabled { Color::BLACK } else { Color::WHITE };
476
});
477
}
478
}
479
}
480
481
impl Default for AppState {
482
fn default() -> Self {
483
AppState([MaskGroupState { clip: 0 }; 6])
484
}
485
}
486
487