Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/animation/animation_graph.rs
6592 views
1
//! Demonstrates animation blending with animation graphs.
2
//!
3
//! The animation graph is shown on screen. You can change the weights of the
4
//! playing animations by clicking and dragging left or right within the nodes.
5
6
use bevy::{
7
color::palettes::{
8
basic::WHITE,
9
css::{ANTIQUE_WHITE, DARK_GREEN},
10
},
11
prelude::*,
12
ui::RelativeCursorPosition,
13
};
14
15
use argh::FromArgs;
16
17
#[cfg(not(target_arch = "wasm32"))]
18
use {
19
bevy::{asset::io::file::FileAssetReader, tasks::IoTaskPool},
20
ron::ser::PrettyConfig,
21
std::{fs::File, path::Path},
22
};
23
24
/// Where to find the serialized animation graph.
25
static ANIMATION_GRAPH_PATH: &str = "animation_graphs/Fox.animgraph.ron";
26
27
/// The indices of the nodes containing animation clips in the graph.
28
static CLIP_NODE_INDICES: [u32; 3] = [2, 3, 4];
29
30
/// The help text in the upper left corner.
31
static HELP_TEXT: &str = "Click and drag an animation clip node to change its weight";
32
33
/// The node widgets in the UI.
34
static NODE_TYPES: [NodeType; 5] = [
35
NodeType::Clip(ClipNode::new("Idle", 0)),
36
NodeType::Clip(ClipNode::new("Walk", 1)),
37
NodeType::Blend("Root"),
38
NodeType::Blend("Blend\n0.5"),
39
NodeType::Clip(ClipNode::new("Run", 2)),
40
];
41
42
/// The positions of the node widgets in the UI.
43
///
44
/// These are in the same order as [`NODE_TYPES`] above.
45
static NODE_RECTS: [NodeRect; 5] = [
46
NodeRect::new(10.00, 10.00, 97.64, 48.41),
47
NodeRect::new(10.00, 78.41, 97.64, 48.41),
48
NodeRect::new(286.08, 78.41, 97.64, 48.41),
49
NodeRect::new(148.04, 112.61, 97.64, 48.41), // was 44.20
50
NodeRect::new(10.00, 146.82, 97.64, 48.41),
51
];
52
53
/// The positions of the horizontal lines in the UI.
54
static HORIZONTAL_LINES: [Line; 6] = [
55
Line::new(107.64, 34.21, 158.24),
56
Line::new(107.64, 102.61, 20.20),
57
Line::new(107.64, 171.02, 20.20),
58
Line::new(127.84, 136.82, 20.20),
59
Line::new(245.68, 136.82, 20.20),
60
Line::new(265.88, 102.61, 20.20),
61
];
62
63
/// The positions of the vertical lines in the UI.
64
static VERTICAL_LINES: [Line; 2] = [
65
Line::new(127.83, 102.61, 68.40),
66
Line::new(265.88, 34.21, 102.61),
67
];
68
69
/// Initializes the app.
70
fn main() {
71
#[cfg(not(target_arch = "wasm32"))]
72
let args: Args = argh::from_env();
73
#[cfg(target_arch = "wasm32")]
74
let args = Args::from_args(&[], &[]).unwrap();
75
76
App::new()
77
.add_plugins(DefaultPlugins.set(WindowPlugin {
78
primary_window: Some(Window {
79
title: "Bevy Animation Graph Example".into(),
80
..default()
81
}),
82
..default()
83
}))
84
.add_systems(Startup, (setup_assets, setup_scene, setup_ui))
85
.add_systems(Update, init_animations)
86
.add_systems(
87
Update,
88
(handle_weight_drag, update_ui, sync_weights).chain(),
89
)
90
.insert_resource(args)
91
.insert_resource(AmbientLight {
92
color: WHITE.into(),
93
brightness: 100.0,
94
..default()
95
})
96
.run();
97
}
98
99
/// Demonstrates animation blending with animation graphs
100
#[derive(FromArgs, Resource)]
101
struct Args {
102
/// disables loading of the animation graph asset from disk
103
#[argh(switch)]
104
no_load: bool,
105
/// regenerates the asset file; implies `--no-load`
106
#[argh(switch)]
107
save: bool,
108
}
109
110
/// The [`AnimationGraph`] asset, which specifies how the animations are to
111
/// be blended together.
112
#[derive(Clone, Resource)]
113
struct ExampleAnimationGraph(Handle<AnimationGraph>);
114
115
/// The current weights of the three playing animations.
116
#[derive(Component)]
117
struct ExampleAnimationWeights {
118
/// The weights of the three playing animations.
119
weights: [f32; 3],
120
}
121
122
/// Initializes the scene.
123
fn setup_assets(
124
mut commands: Commands,
125
mut asset_server: ResMut<AssetServer>,
126
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
127
args: Res<Args>,
128
) {
129
// Create or load the assets.
130
if args.no_load || args.save {
131
setup_assets_programmatically(
132
&mut commands,
133
&mut asset_server,
134
&mut animation_graphs,
135
args.save,
136
);
137
} else {
138
setup_assets_via_serialized_animation_graph(&mut commands, &mut asset_server);
139
}
140
}
141
142
fn setup_ui(mut commands: Commands) {
143
setup_help_text(&mut commands);
144
setup_node_rects(&mut commands);
145
setup_node_lines(&mut commands);
146
}
147
148
/// Creates the assets programmatically, including the animation graph.
149
/// Optionally saves them to disk if `save` is present (corresponding to the
150
/// `--save` option).
151
fn setup_assets_programmatically(
152
commands: &mut Commands,
153
asset_server: &mut AssetServer,
154
animation_graphs: &mut Assets<AnimationGraph>,
155
_save: bool,
156
) {
157
// Create the nodes.
158
let mut animation_graph = AnimationGraph::new();
159
let blend_node = animation_graph.add_blend(0.5, animation_graph.root);
160
animation_graph.add_clip(
161
asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
162
1.0,
163
animation_graph.root,
164
);
165
animation_graph.add_clip(
166
asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
167
1.0,
168
blend_node,
169
);
170
animation_graph.add_clip(
171
asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
172
1.0,
173
blend_node,
174
);
175
176
// If asked to save, do so.
177
#[cfg(not(target_arch = "wasm32"))]
178
if _save {
179
let animation_graph = animation_graph.clone();
180
181
IoTaskPool::get()
182
.spawn(async move {
183
use std::io::Write;
184
185
let animation_graph: SerializedAnimationGraph = animation_graph
186
.try_into()
187
.expect("The animation graph failed to convert to its serialized form");
188
189
let serialized_graph =
190
ron::ser::to_string_pretty(&animation_graph, PrettyConfig::default())
191
.expect("Failed to serialize the animation graph");
192
let mut animation_graph_writer = File::create(Path::join(
193
&FileAssetReader::get_base_path(),
194
Path::join(Path::new("assets"), Path::new(ANIMATION_GRAPH_PATH)),
195
))
196
.expect("Failed to open the animation graph asset");
197
animation_graph_writer
198
.write_all(serialized_graph.as_bytes())
199
.expect("Failed to write the animation graph");
200
})
201
.detach();
202
}
203
204
// Add the graph.
205
let handle = animation_graphs.add(animation_graph);
206
207
// Save the assets in a resource.
208
commands.insert_resource(ExampleAnimationGraph(handle));
209
}
210
211
fn setup_assets_via_serialized_animation_graph(
212
commands: &mut Commands,
213
asset_server: &mut AssetServer,
214
) {
215
commands.insert_resource(ExampleAnimationGraph(
216
asset_server.load(ANIMATION_GRAPH_PATH),
217
));
218
}
219
220
/// Spawns the animated fox.
221
fn setup_scene(
222
mut commands: Commands,
223
asset_server: Res<AssetServer>,
224
mut meshes: ResMut<Assets<Mesh>>,
225
mut materials: ResMut<Assets<StandardMaterial>>,
226
) {
227
commands.spawn((
228
Camera3d::default(),
229
Transform::from_xyz(-10.0, 5.0, 13.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
230
));
231
232
commands.spawn((
233
PointLight {
234
intensity: 10_000_000.0,
235
shadows_enabled: true,
236
..default()
237
},
238
Transform::from_xyz(-4.0, 8.0, 13.0),
239
));
240
241
commands.spawn((
242
SceneRoot(
243
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
244
),
245
Transform::from_scale(Vec3::splat(0.07)),
246
));
247
248
// Ground
249
250
commands.spawn((
251
Mesh3d(meshes.add(Circle::new(7.0))),
252
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
253
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
254
));
255
}
256
257
/// Places the help text at the top left of the window.
258
fn setup_help_text(commands: &mut Commands) {
259
commands.spawn((
260
Text::new(HELP_TEXT),
261
Node {
262
position_type: PositionType::Absolute,
263
top: px(12),
264
left: px(12),
265
..default()
266
},
267
));
268
}
269
270
/// Initializes the node UI widgets.
271
fn setup_node_rects(commands: &mut Commands) {
272
for (node_rect, node_type) in NODE_RECTS.iter().zip(NODE_TYPES.iter()) {
273
let node_string = match *node_type {
274
NodeType::Clip(ref clip) => clip.text,
275
NodeType::Blend(text) => text,
276
};
277
278
let text = commands
279
.spawn((
280
Text::new(node_string),
281
TextFont {
282
font_size: 16.0,
283
..default()
284
},
285
TextColor(ANTIQUE_WHITE.into()),
286
TextLayout::new_with_justify(Justify::Center),
287
))
288
.id();
289
290
let container = {
291
let mut container = commands.spawn((
292
Node {
293
position_type: PositionType::Absolute,
294
bottom: px(node_rect.bottom),
295
left: px(node_rect.left),
296
height: px(node_rect.height),
297
width: px(node_rect.width),
298
align_items: AlignItems::Center,
299
justify_items: JustifyItems::Center,
300
align_content: AlignContent::Center,
301
justify_content: JustifyContent::Center,
302
..default()
303
},
304
BorderColor::all(WHITE),
305
Outline::new(px(1), Val::ZERO, Color::WHITE),
306
));
307
308
if let NodeType::Clip(clip) = node_type {
309
container.insert((
310
Interaction::None,
311
RelativeCursorPosition::default(),
312
(*clip).clone(),
313
));
314
}
315
316
container.id()
317
};
318
319
// Create the background color.
320
if let NodeType::Clip(_) = node_type {
321
let background = commands
322
.spawn((
323
Node {
324
position_type: PositionType::Absolute,
325
top: px(0),
326
left: px(0),
327
height: px(node_rect.height),
328
width: px(node_rect.width),
329
..default()
330
},
331
BackgroundColor(DARK_GREEN.into()),
332
))
333
.id();
334
335
commands.entity(container).add_child(background);
336
}
337
338
commands.entity(container).add_child(text);
339
}
340
}
341
342
/// Creates boxes for the horizontal and vertical lines.
343
///
344
/// This is a bit hacky: it uses 1-pixel-wide and 1-pixel-high boxes to draw
345
/// vertical and horizontal lines, respectively.
346
fn setup_node_lines(commands: &mut Commands) {
347
for line in &HORIZONTAL_LINES {
348
commands.spawn((
349
Node {
350
position_type: PositionType::Absolute,
351
bottom: px(line.bottom),
352
left: px(line.left),
353
height: px(0),
354
width: px(line.length),
355
border: UiRect::bottom(px(1)),
356
..default()
357
},
358
BorderColor::all(WHITE),
359
));
360
}
361
362
for line in &VERTICAL_LINES {
363
commands.spawn((
364
Node {
365
position_type: PositionType::Absolute,
366
bottom: px(line.bottom),
367
left: px(line.left),
368
height: px(line.length),
369
width: px(0),
370
border: UiRect::left(px(1)),
371
..default()
372
},
373
BorderColor::all(WHITE),
374
));
375
}
376
}
377
378
/// Attaches the animation graph to the scene, and plays all three animations.
379
fn init_animations(
380
mut commands: Commands,
381
mut query: Query<(Entity, &mut AnimationPlayer)>,
382
animation_graph: Res<ExampleAnimationGraph>,
383
mut done: Local<bool>,
384
) {
385
if *done {
386
return;
387
}
388
389
for (entity, mut player) in query.iter_mut() {
390
commands.entity(entity).insert((
391
AnimationGraphHandle(animation_graph.0.clone()),
392
ExampleAnimationWeights::default(),
393
));
394
for &node_index in &CLIP_NODE_INDICES {
395
player.play(node_index.into()).repeat();
396
}
397
398
*done = true;
399
}
400
}
401
402
/// Read cursor position relative to clip nodes, allowing the user to change weights
403
/// when dragging the node UI widgets.
404
fn handle_weight_drag(
405
mut interaction_query: Query<(&Interaction, &RelativeCursorPosition, &ClipNode)>,
406
mut animation_weights_query: Query<&mut ExampleAnimationWeights>,
407
) {
408
for (interaction, relative_cursor, clip_node) in &mut interaction_query {
409
if !matches!(*interaction, Interaction::Pressed) {
410
continue;
411
}
412
413
let Some(pos) = relative_cursor.normalized else {
414
continue;
415
};
416
417
for mut animation_weights in animation_weights_query.iter_mut() {
418
animation_weights.weights[clip_node.index] = pos.x.clamp(0., 1.);
419
}
420
}
421
}
422
423
// Updates the UI based on the weights that the user has chosen.
424
fn update_ui(
425
mut text_query: Query<&mut Text>,
426
mut background_query: Query<&mut Node, Without<Text>>,
427
container_query: Query<(&Children, &ClipNode)>,
428
animation_weights_query: Query<&ExampleAnimationWeights, Changed<ExampleAnimationWeights>>,
429
) {
430
for animation_weights in animation_weights_query.iter() {
431
for (children, clip_node) in &container_query {
432
// Draw the green background color to visually indicate the weight.
433
let mut bg_iter = background_query.iter_many_mut(children);
434
if let Some(mut node) = bg_iter.fetch_next() {
435
// All nodes are the same width, so `NODE_RECTS[0]` is as good as any other.
436
node.width = px(NODE_RECTS[0].width * animation_weights.weights[clip_node.index]);
437
}
438
439
// Update the node labels with the current weights.
440
let mut text_iter = text_query.iter_many_mut(children);
441
if let Some(mut text) = text_iter.fetch_next() {
442
**text = format!(
443
"{}\n{:.2}",
444
clip_node.text, animation_weights.weights[clip_node.index]
445
);
446
}
447
}
448
}
449
}
450
451
/// Takes the weights that were set in the UI and assigns them to the actual
452
/// playing animation.
453
fn sync_weights(mut query: Query<(&mut AnimationPlayer, &ExampleAnimationWeights)>) {
454
for (mut animation_player, animation_weights) in query.iter_mut() {
455
for (&animation_node_index, &animation_weight) in CLIP_NODE_INDICES
456
.iter()
457
.zip(animation_weights.weights.iter())
458
{
459
// If the animation happens to be no longer active, restart it.
460
if !animation_player.is_playing_animation(animation_node_index.into()) {
461
animation_player.play(animation_node_index.into());
462
}
463
464
// Set the weight.
465
if let Some(active_animation) =
466
animation_player.animation_mut(animation_node_index.into())
467
{
468
active_animation.set_weight(animation_weight);
469
}
470
}
471
}
472
}
473
474
/// An on-screen representation of a node.
475
#[derive(Debug)]
476
struct NodeRect {
477
/// The number of pixels that this rectangle is from the left edge of the
478
/// window.
479
left: f32,
480
/// The number of pixels that this rectangle is from the bottom edge of the
481
/// window.
482
bottom: f32,
483
/// The width of this rectangle in pixels.
484
width: f32,
485
/// The height of this rectangle in pixels.
486
height: f32,
487
}
488
489
/// Either a straight horizontal or a straight vertical line on screen.
490
///
491
/// The line starts at (`left`, `bottom`) and goes either right (if the line is
492
/// horizontal) or down (if the line is vertical).
493
struct Line {
494
/// The number of pixels that the start of this line is from the left edge
495
/// of the screen.
496
left: f32,
497
/// The number of pixels that the start of this line is from the bottom edge
498
/// of the screen.
499
bottom: f32,
500
/// The length of the line.
501
length: f32,
502
}
503
504
/// The type of each node in the UI: either a clip node or a blend node.
505
enum NodeType {
506
/// A clip node, which specifies an animation.
507
Clip(ClipNode),
508
/// A blend node with no animation and a string label.
509
Blend(&'static str),
510
}
511
512
/// The label for the UI representation of a clip node.
513
#[derive(Clone, Component)]
514
struct ClipNode {
515
/// The string label of the node.
516
text: &'static str,
517
/// Which of the three animations this UI widget represents.
518
index: usize,
519
}
520
521
impl Default for ExampleAnimationWeights {
522
fn default() -> Self {
523
Self { weights: [1.0; 3] }
524
}
525
}
526
527
impl ClipNode {
528
/// Creates a new [`ClipNodeText`] from a label and the animation index.
529
const fn new(text: &'static str, index: usize) -> Self {
530
Self { text, index }
531
}
532
}
533
534
impl NodeRect {
535
/// Creates a new [`NodeRect`] from the lower-left corner and size.
536
///
537
/// Note that node rectangles are anchored in the *lower*-left corner. The
538
/// `bottom` parameter specifies vertical distance from the *bottom* of the
539
/// window.
540
const fn new(left: f32, bottom: f32, width: f32, height: f32) -> NodeRect {
541
NodeRect {
542
left,
543
bottom,
544
width,
545
height,
546
}
547
}
548
}
549
550
impl Line {
551
/// Creates a new [`Line`], either horizontal or vertical.
552
///
553
/// Note that the line's start point is anchored in the lower-*left* corner,
554
/// and that the `length` extends either to the right or downward.
555
const fn new(left: f32, bottom: f32, length: f32) -> Self {
556
Self {
557
left,
558
bottom,
559
length,
560
}
561
}
562
}
563
564