1use bevy::{
4 animation::{AnimationTarget, AnimationTargetId},
5 color::palettes::css::{LIGHT_GRAY, WHITE},
6 prelude::*,
7};
8use std::collections::HashSet;
9
10const MASK_GROUP_HEAD: u32 = 0;
15const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1;
16const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2;
17const MASK_GROUP_LEFT_HIND_LEG: u32 = 3;
18const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4;
19const MASK_GROUP_TAIL: u32 = 5;
20
21const MASK_GROUP_BUTTON_WIDTH: f32 = 250.0;
24
25const MASK_GROUP_PATHS: [(&str, &str); 6] = [
36 (
38 "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03",
39 "b_Neck_04/b_Head_05",
40 ),
41 (
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 (
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 (
53 "root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015",
54 "b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018",
55 ),
56 (
58 "root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019",
59 "b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022",
60 ),
61 (
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)]
69struct AnimationControl {
70 group_id: u32,
72 label: AnimationLabel,
73}
74
75#[derive(Clone, Copy, Component, PartialEq, Debug)]
76enum AnimationLabel {
77 Idle = 0,
78 Walk = 1,
79 Run = 2,
80 Off = 3,
81}
82
83#[derive(Clone, Debug, Resource)]
84struct AnimationNodes([AnimationNodeIndex; 3]);
85
86#[derive(Clone, Copy, Debug, Resource)]
87struct AppState([MaskGroupState; 6]);
88
89#[derive(Clone, Copy, Debug)]
90struct MaskGroupState {
91 clip: u8,
92}
93
94fn 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
117fn 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 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 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 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 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
157fn setup_ui(mut commands: Commands) {
159 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 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
225fn 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
338fn 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 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 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 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 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 let animation_graph = animation_graphs.add(animation_graph);
384 commands
385 .entity(entity)
386 .insert(AnimationGraphHandle(animation_graph));
387
388 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 for animation_graph_node in animation_graph_nodes {
399 player.play(animation_graph_node).repeat();
400 }
401
402 commands.insert_resource(AnimationNodes(animation_graph_nodes));
404 }
405}
406
407fn 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 if *interaction != Interaction::Pressed {
423 continue;
424 }
425
426 app_state.0[animation_control.group_id as usize].clip = animation_control.label as u8;
428
429 for animation_graph_handle in animation_players.iter_mut() {
432 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
452fn 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
481impl Default for AppState {
482 fn default() -> Self {
483 AppState([MaskGroupState { clip: 0 }; 6])
484 }
485}