docs/bevy/examples/animation/animated_mesh_events.rs 9.2 K · 295 lines · raw · history

1 //! Plays animations from a skinned glTF.
2
3 use std::{f32::consts::PI, time::Duration};
4
5 use bevy::{
6 animation::{AnimationEvent, AnimationTargetId},
7 color::palettes::css::WHITE,
8 light::CascadeShadowConfigBuilder,
9 prelude::*,
10 };
11 use chacha20::ChaCha8Rng;
12 use rand::{RngExt, SeedableRng};
13
14 const FOX_PATH: &str = "models/animated/Fox.glb";
15
16 fn main() {
17 App::new()
18 .insert_resource(GlobalAmbientLight {
19 color: Color::WHITE,
20 brightness: 2000.,
21 ..default()
22 })
23 .add_plugins(DefaultPlugins)
24 .init_resource::<ParticleAssets>()
25 .init_resource::<FoxFeetTargets>()
26 .add_systems(Startup, setup)
27 .add_systems(Update, setup_scene_once_loaded)
28 .add_systems(Update, simulate_particles)
29 .add_observer(observe_on_step)
30 .run();
31 }
32
33 #[derive(Resource)]
34 struct SeededRng(ChaCha8Rng);
35
36 #[derive(Resource)]
37 struct Animations {
38 index: AnimationNodeIndex,
39 graph_handle: Handle<AnimationGraph>,
40 }
41
42 #[derive(AnimationEvent, Reflect, Clone)]
43 struct Step;
44
45 fn observe_on_step(
46 step: On<Step>,
47 particle: Res<ParticleAssets>,
48 mut commands: Commands,
49 transforms: Query<&GlobalTransform>,
50 mut seeded_rng: ResMut<SeededRng>,
51 ) -> Result {
52 let translation = transforms.get(step.trigger().target)?.translation();
53 // Spawn a bunch of particles.
54 for _ in 0..14 {
55 let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);
56 let vertical = seeded_rng.0.random_range(0.0..4.0);
57 let size = seeded_rng.0.random_range(0.2..1.0);
58
59 commands.spawn((
60 Particle {
61 lifetime_timer: Timer::from_seconds(
62 seeded_rng.0.random_range(0.2..0.6),
63 TimerMode::Once,
64 ),
65 size,
66 velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
67 },
68 Mesh3d(particle.mesh.clone()),
69 MeshMaterial3d(particle.material.clone()),
70 Transform {
71 translation,
72 scale: Vec3::splat(size),
73 ..Default::default()
74 },
75 ));
76 }
77 Ok(())
78 }
79
80 fn setup(
81 mut commands: Commands,
82 asset_server: Res<AssetServer>,
83 mut meshes: ResMut<Assets<Mesh>>,
84 mut materials: ResMut<Assets<StandardMaterial>>,
85 mut graphs: ResMut<Assets<AnimationGraph>>,
86 ) {
87 // Build the animation graph
88 let (graph, index) = AnimationGraph::from_clip(
89 // We specifically want the "run" animation, which is the third one.
90 asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
91 );
92
93 // Insert a resource with the current scene information
94 let graph_handle = graphs.add(graph);
95 commands.insert_resource(Animations {
96 index,
97 graph_handle,
98 });
99
100 // Camera
101 commands.spawn((
102 Camera3d::default(),
103 Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
104 ));
105
106 // Plane
107 commands.spawn((
108 Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
109 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
110 ));
111
112 // Light
113 commands.spawn((
114 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
115 DirectionalLight {
116 shadow_maps_enabled: true,
117 ..default()
118 },
119 CascadeShadowConfigBuilder {
120 first_cascade_far_bound: 200.0,
121 maximum_distance: 400.0,
122 ..default()
123 }
124 .build(),
125 ));
126
127 // Fox
128 commands.spawn(WorldAssetRoot(
129 asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),
130 ));
131
132 // We're seeding the PRNG here to make this example deterministic for testing purposes.
133 // This isn't strictly required in practical use unless you need your app to be deterministic.
134 let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
135 commands.insert_resource(SeededRng(seeded_rng));
136 }
137
138 // An `AnimationPlayer` is automatically added to the scene when it's ready.
139 // When the player is added, start the animation.
140 fn setup_scene_once_loaded(
141 mut commands: Commands,
142 animations: Res<Animations>,
143 feet: Res<FoxFeetTargets>,
144 graphs: Res<Assets<AnimationGraph>>,
145 mut clips: ResMut<Assets<AnimationClip>>,
146 mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
147 ) {
148 fn get_clip<'a>(
149 node: AnimationNodeIndex,
150 graph: &AnimationGraph,
151 clips: &'a mut Assets<AnimationClip>,
152 ) -> &'a mut AnimationClip {
153 let node = graph.get(node).unwrap();
154 let clip = match &node.node_type {
155 AnimationNodeType::Clip(handle) => clips.get_mut(handle),
156 _ => unreachable!(),
157 };
158 clip.unwrap().into_inner()
159 }
160
161 for (entity, mut player) in &mut players {
162 // Send `OnStep` events once the fox feet hits the ground in the running animation.
163
164 let graph = graphs.get(&animations.graph_handle).unwrap();
165 let running_animation = get_clip(animations.index, graph, &mut clips);
166
167 // You can determine the time an event should trigger if you know which frame it occurs and
168 // the frame rate of the animation. Let's say we want to trigger an event at frame 15,
169 // and the animation has a frame rate of 24 fps, then time = 15 / 24 = 0.625.
170 running_animation.add_event_to_target(feet.front_left, 0.625, Step);
171 running_animation.add_event_to_target(feet.front_right, 0.5, Step);
172 running_animation.add_event_to_target(feet.back_left, 0.0, Step);
173 running_animation.add_event_to_target(feet.back_right, 0.125, Step);
174
175 // Start the animation
176
177 let mut transitions = AnimationTransitions::new();
178
179 // Make sure to start the animation via the `AnimationTransitions`
180 // component. The `AnimationTransitions` component wants to manage all
181 // the animations and will get confused if the animations are started
182 // directly via the `AnimationPlayer`.
183 transitions
184 .play(&mut player, animations.index, Duration::ZERO)
185 .repeat();
186
187 commands
188 .entity(entity)
189 .insert(AnimationGraphHandle(animations.graph_handle.clone()))
190 .insert(transitions);
191 }
192 }
193
194 fn simulate_particles(
195 mut commands: Commands,
196 mut query: Query<(Entity, &mut Transform, &mut Particle)>,
197 time: Res<Time>,
198 ) {
199 for (entity, mut transform, mut particle) in &mut query {
200 if particle.lifetime_timer.tick(time.delta()).just_finished() {
201 commands.entity(entity).despawn();
202 return;
203 }
204
205 transform.translation += particle.velocity * time.delta_secs();
206 transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
207 particle
208 .velocity
209 .smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
210 }
211 }
212
213 #[derive(Component)]
214 struct Particle {
215 lifetime_timer: Timer,
216 size: f32,
217 velocity: Vec3,
218 }
219
220 #[derive(Resource)]
221 struct ParticleAssets {
222 mesh: Handle<Mesh>,
223 material: Handle<StandardMaterial>,
224 }
225
226 impl FromWorld for ParticleAssets {
227 fn from_world(world: &mut World) -> Self {
228 Self {
229 mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
230 material: world.add_asset::<StandardMaterial>(StandardMaterial {
231 base_color: WHITE.into(),
232 ..Default::default()
233 }),
234 }
235 }
236 }
237
238 /// Stores the `AnimationTargetId`s of the fox's feet
239 #[derive(Resource)]
240 struct FoxFeetTargets {
241 front_right: AnimationTargetId,
242 front_left: AnimationTargetId,
243 back_left: AnimationTargetId,
244 back_right: AnimationTargetId,
245 }
246
247 impl Default for FoxFeetTargets {
248 fn default() -> Self {
249 let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
250 let front_left_foot = hip_node.iter().chain(
251 [
252 "b_Spine01_02",
253 "b_Spine02_03",
254 "b_LeftUpperArm_09",
255 "b_LeftForeArm_010",
256 "b_LeftHand_011",
257 ]
258 .iter(),
259 );
260 let front_right_foot = hip_node.iter().chain(
261 [
262 "b_Spine01_02",
263 "b_Spine02_03",
264 "b_RightUpperArm_06",
265 "b_RightForeArm_07",
266 "b_RightHand_08",
267 ]
268 .iter(),
269 );
270 let back_left_foot = hip_node.iter().chain(
271 [
272 "b_LeftLeg01_015",
273 "b_LeftLeg02_016",
274 "b_LeftFoot01_017",
275 "b_LeftFoot02_018",
276 ]
277 .iter(),
278 );
279 let back_right_foot = hip_node.iter().chain(
280 [
281 "b_RightLeg01_019",
282 "b_RightLeg02_020",
283 "b_RightFoot01_021",
284 "b_RightFoot02_022",
285 ]
286 .iter(),
287 );
288 Self {
289 front_left: AnimationTargetId::from_iter(front_left_foot),
290 front_right: AnimationTargetId::from_iter(front_right_foot),
291 back_left: AnimationTargetId::from_iter(back_left_foot),
292 back_right: AnimationTargetId::from_iter(back_right_foot),
293 }
294 }
295 }