Skip to content

Commit e2a709e

Browse files
authored
Update morph_targets and many_foxes examples to use observers (#20531)
## Objective Change some examples to follow best practices for playing animations, and as a bonus work around an issue with scenes spawning multiple times. ## Background Examples that play skeletal animations usually have two steps: 1) Start scene spawning. 2) Play animations after the scene has spawned. Different examples use different approaches for triggering part 2, including a scene spawning observer and `Added<AnimationPlayer>` queries. The observer approach is arguably best as it's more tightly scoped and easier for users to extend. The other approaches work in simple examples but fall down when users want multiple scenes or animations. See #17421 for more detail. As a bonus, the scene spawning observer works around a current issue with scenes spawning multiple times - see #20393, #20430. Although there's an argument that this PR shouldn't land until the issue is properly fixed, as these examples are a useful test case. ## Solution Update the `morph_targets` and `many_foxes` examples to use observers. I also made a few tweaks and fixes to `morph_targets`. - Fix documentation referring to an `update_weights` feature that isn't in the example. - Use the same `AnimationToPlay` component as the `animated_mesh` example. - Change the `name_morphs` system to be event driven and print the asset name. - This is maybe too complex, but could also be nice for users to c&p into their app for debugging. I haven't updated the `animated_mesh_control`, `animated_mesh_events`, and `animation_masks` examples, which still use `Added<AnimationPlayer>`. ## Testing ```sh cargo run --example morph_targets cargo run --example many_foxes ```
1 parent f72617d commit e2a709e

File tree

2 files changed

+78
-92
lines changed

2 files changed

+78
-92
lines changed

examples/animation/morph_targets.rs

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,98 @@
1-
//! Controls morph targets in a loaded scene.
1+
//! Play an animation with morph targets.
22
//!
3-
//! Illustrates:
4-
//!
5-
//! - How to access and modify individual morph target weights.
6-
//! See the `update_weights` system for details.
7-
//! - How to read morph target names in `name_morphs`.
8-
//! - How to play morph target animations in `setup_animations`.
3+
//! Also illustrates how to read morph target names in `name_morphs`.
94
10-
use bevy::prelude::*;
5+
use bevy::{prelude::*, scene::SceneInstanceReady};
116
use std::f32::consts::PI;
127

8+
const GLTF_PATH: &str = "models/animated/MorphStressTest.gltf";
9+
1310
fn main() {
1411
App::new()
15-
.add_plugins(DefaultPlugins.set(WindowPlugin {
16-
primary_window: Some(Window {
17-
title: "morph targets".to_string(),
18-
..default()
19-
}),
20-
..default()
21-
}))
12+
.add_plugins(DefaultPlugins)
2213
.insert_resource(AmbientLight {
2314
brightness: 150.0,
2415
..default()
2516
})
2617
.add_systems(Startup, setup)
27-
.add_systems(Update, (name_morphs, setup_animations))
18+
.add_systems(Update, name_morphs)
2819
.run();
2920
}
3021

31-
#[derive(Resource)]
32-
struct MorphData {
33-
the_wave: Handle<AnimationClip>,
34-
mesh: Handle<Mesh>,
22+
#[derive(Component)]
23+
struct AnimationToPlay {
24+
graph_handle: Handle<AnimationGraph>,
25+
index: AnimationNodeIndex,
3526
}
3627

37-
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
38-
commands.insert_resource(MorphData {
39-
the_wave: asset_server
40-
.load(GltfAssetLabel::Animation(2).from_asset("models/animated/MorphStressTest.gltf")),
41-
mesh: asset_server.load(
42-
GltfAssetLabel::Primitive {
43-
mesh: 0,
44-
primitive: 0,
45-
}
46-
.from_asset("models/animated/MorphStressTest.gltf"),
47-
),
48-
});
49-
commands.spawn(SceneRoot(asset_server.load(
50-
GltfAssetLabel::Scene(0).from_asset("models/animated/MorphStressTest.gltf"),
51-
)));
28+
fn setup(
29+
mut commands: Commands,
30+
asset_server: Res<AssetServer>,
31+
mut graphs: ResMut<Assets<AnimationGraph>>,
32+
) {
33+
let (graph, index) = AnimationGraph::from_clip(
34+
asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
35+
);
36+
37+
commands
38+
.spawn((
39+
AnimationToPlay {
40+
graph_handle: graphs.add(graph),
41+
index,
42+
},
43+
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH))),
44+
))
45+
.observe(play_animation_when_ready);
46+
5247
commands.spawn((
5348
DirectionalLight::default(),
5449
Transform::from_rotation(Quat::from_rotation_z(PI / 2.0)),
5550
));
51+
5652
commands.spawn((
5753
Camera3d::default(),
5854
Transform::from_xyz(3.0, 2.1, 10.2).looking_at(Vec3::ZERO, Vec3::Y),
5955
));
6056
}
6157

62-
/// Plays an [`AnimationClip`] from the loaded [`Gltf`] on the [`AnimationPlayer`] created by the spawned scene.
63-
fn setup_animations(
64-
mut has_setup: Local<bool>,
58+
fn play_animation_when_ready(
59+
trigger: On<SceneInstanceReady>,
6560
mut commands: Commands,
66-
mut players: Query<(Entity, &Name, &mut AnimationPlayer)>,
67-
morph_data: Res<MorphData>,
68-
mut graphs: ResMut<Assets<AnimationGraph>>,
61+
children: Query<&Children>,
62+
animations_to_play: Query<&AnimationToPlay>,
63+
mut players: Query<&mut AnimationPlayer>,
6964
) {
70-
if *has_setup {
71-
return;
72-
}
73-
for (entity, name, mut player) in &mut players {
74-
// The name of the entity in the GLTF scene containing the AnimationPlayer for our morph targets is "Main"
75-
if name.as_str() != "Main" {
76-
continue;
77-
}
65+
if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) {
66+
for child in children.iter_descendants(trigger.target()) {
67+
if let Ok(mut player) = players.get_mut(child) {
68+
player.play(animation_to_play.index).repeat();
7869

79-
let (graph, animation) = AnimationGraph::from_clip(morph_data.the_wave.clone());
80-
commands
81-
.entity(entity)
82-
.insert(AnimationGraphHandle(graphs.add(graph)));
83-
84-
player.play(animation).repeat();
85-
*has_setup = true;
70+
commands
71+
.entity(child)
72+
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
73+
}
74+
}
8675
}
8776
}
8877

89-
/// You can get the target names in their corresponding [`Mesh`].
90-
/// They are in the order of the weights.
78+
/// Whenever a mesh asset is loaded, print the name of the asset and the names
79+
/// of its morph targets.
9180
fn name_morphs(
92-
mut has_printed: Local<bool>,
93-
morph_data: Res<MorphData>,
81+
asset_server: Res<AssetServer>,
82+
mut events: EventReader<AssetEvent<Mesh>>,
9483
meshes: Res<Assets<Mesh>>,
9584
) {
96-
if *has_printed {
97-
return;
98-
}
99-
100-
let Some(mesh) = meshes.get(&morph_data.mesh) else {
101-
return;
102-
};
103-
let Some(names) = mesh.morph_target_names() else {
104-
return;
105-
};
85+
for event in events.read() {
86+
if let AssetEvent::<Mesh>::Added { id } = event
87+
&& let Some(path) = asset_server.get_path(*id)
88+
&& let Some(mesh) = meshes.get(*id)
89+
&& let Some(names) = mesh.morph_target_names()
90+
{
91+
info!("Morph target names for {path:?}:");
10692

107-
info!("Target names:");
108-
for name in names {
109-
info!(" {name}");
93+
for name in names {
94+
info!(" {name}");
95+
}
96+
}
11097
}
111-
*has_printed = true;
11298
}

examples/stress_tests/many_foxes.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use bevy::{
88
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
99
light::CascadeShadowConfigBuilder,
1010
prelude::*,
11+
scene::SceneInstanceReady,
1112
window::{PresentMode, WindowResolution},
1213
winit::{UpdateMode, WinitSettings},
1314
};
@@ -68,7 +69,6 @@ fn main() {
6869
.add_systems(
6970
Update,
7071
(
71-
setup_scene_once_loaded,
7272
keyboard_animation_control,
7373
update_fox_rings.after(keyboard_animation_control),
7474
),
@@ -173,12 +173,14 @@ fn setup(
173173
let (x, z) = (radius * c, radius * s);
174174

175175
commands.entity(ring_parent).with_children(|builder| {
176-
builder.spawn((
177-
SceneRoot(fox_handle.clone()),
178-
Transform::from_xyz(x, 0.0, z)
179-
.with_scale(Vec3::splat(0.01))
180-
.with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
181-
));
176+
builder
177+
.spawn((
178+
SceneRoot(fox_handle.clone()),
179+
Transform::from_xyz(x, 0.0, z)
180+
.with_scale(Vec3::splat(0.01))
181+
.with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
182+
))
183+
.observe(setup_scene_once_loaded);
182184
});
183185
}
184186

@@ -230,25 +232,23 @@ fn setup(
230232

231233
// Once the scene is loaded, start the animation
232234
fn setup_scene_once_loaded(
235+
trigger: On<SceneInstanceReady>,
233236
animations: Res<Animations>,
234237
foxes: Res<Foxes>,
235238
mut commands: Commands,
236-
mut player: Query<(Entity, &mut AnimationPlayer)>,
237-
mut done: Local<bool>,
239+
children: Query<&Children>,
240+
mut players: Query<&mut AnimationPlayer>,
238241
) {
239-
if !*done && player.iter().len() == foxes.count {
240-
for (entity, mut player) in &mut player {
241-
commands
242-
.entity(entity)
243-
.insert(AnimationGraphHandle(animations.graph.clone()))
244-
.insert(AnimationTransitions::new());
245-
242+
for child in children.iter_descendants(trigger.target()) {
243+
if let Ok(mut player) = players.get_mut(child) {
246244
let playing_animation = player.play(animations.node_indices[0]).repeat();
247245
if !foxes.sync {
248-
playing_animation.seek_to(entity.index() as f32 / 10.0);
246+
playing_animation.seek_to(trigger.target().index() as f32 / 10.0);
249247
}
248+
commands
249+
.entity(child)
250+
.insert(AnimationGraphHandle(animations.graph.clone()));
250251
}
251-
*done = true;
252252
}
253253
}
254254

0 commit comments

Comments
 (0)