gltf_extension_animation_graph/
gltf_extension_animation_graph.rs1use std::f32::consts::PI;
4
5use bevy::{
6 animation::{AnimationEvent, AnimationTargetId},
7 asset::LoadContext,
8 ecs::entity::EntityHashSet,
9 gltf::extensions::{ErasedGltfExtensionHandler, GltfExtensionHandler, GltfExtensionHandlers},
10 light::CascadeShadowConfigBuilder,
11 platform::collections::{HashMap, HashSet},
12 prelude::*,
13 world_serialization::WorldInstanceReady,
14};
15use chacha20::ChaCha8Rng;
16use rand::{RngExt, SeedableRng};
17
18const GLTF_PATH: &str = "models/animated/Fox.glb";
20
21fn main() {
22 App::new()
23 .insert_resource(GlobalAmbientLight {
24 color: Color::WHITE,
25 brightness: 2000.,
26 ..default()
27 })
28 .add_plugins((DefaultPlugins, GltfExtensionHandlerAnimationPlugin))
29 .init_resource::<ParticleAssets>()
30 .add_systems(
31 Startup,
32 (setup_mesh_and_animation, setup_camera_and_environment),
33 )
34 .add_systems(Update, simulate_particles)
35 .add_observer(observe_on_step)
36 .run();
37}
38
39#[derive(Resource)]
40struct SeededRng(ChaCha8Rng);
41
42#[derive(Component, Reflect)]
46#[reflect(Component)]
47struct AnimationToPlay {
48 graph_handle: Handle<AnimationGraph>,
49 index: AnimationNodeIndex,
50}
51
52fn setup_mesh_and_animation(mut commands: Commands, asset_server: Res<AssetServer>) {
53 commands
56 .spawn(WorldAssetRoot(
57 asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)),
58 ))
59 .observe(play_animation_when_ready);
60}
61
62fn play_animation_when_ready(
63 scene_ready: On<WorldInstanceReady>,
64 mut commands: Commands,
65 children: Query<&Children>,
66 mut players: Query<(&mut AnimationPlayer, &AnimationToPlay)>,
67) {
68 for child in children.iter_descendants(scene_ready.entity) {
69 let Ok((mut player, animation_to_play)) = players.get_mut(child) else {
70 continue;
71 };
72
73 player.play(animation_to_play.index).repeat();
79
80 commands
83 .entity(child)
84 .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
85 }
86}
87
88fn setup_camera_and_environment(
90 mut commands: Commands,
91 mut meshes: ResMut<Assets<Mesh>>,
92 mut materials: ResMut<Assets<StandardMaterial>>,
93) {
94 commands.spawn((
96 Camera3d::default(),
97 Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
98 ));
99
100 commands.spawn((
102 Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
103 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
104 ));
105
106 commands.spawn((
108 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
109 DirectionalLight {
110 shadow_maps_enabled: true,
111 ..default()
112 },
113 CascadeShadowConfigBuilder {
114 first_cascade_far_bound: 200.0,
115 maximum_distance: 400.0,
116 ..default()
117 }
118 .build(),
119 ));
120
121 let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
124 commands.insert_resource(SeededRng(seeded_rng));
125}
126
127struct GltfExtensionHandlerAnimationPlugin;
128
129impl Plugin for GltfExtensionHandlerAnimationPlugin {
130 fn build(&self, app: &mut App) {
131 #[cfg(target_family = "wasm")]
132 bevy::tasks::block_on(async {
133 app.world_mut()
134 .resource_mut::<GltfExtensionHandlers>()
135 .0
136 .write()
137 .await
138 .push(Box::new(GltfExtensionHandlerAnimation::default()))
139 });
140 #[cfg(not(target_family = "wasm"))]
141 app.world_mut()
142 .resource_mut::<GltfExtensionHandlers>()
143 .0
144 .write_blocking()
145 .push(Box::new(GltfExtensionHandlerAnimation::default()));
146 }
147}
148
149#[derive(Default, Clone)]
150struct GltfExtensionHandlerAnimation {
151 animation_root_indices: HashSet<usize>,
152 animation_root_entities: EntityHashSet,
153 clip: Option<Handle<AnimationClip>>,
154}
155
156impl GltfExtensionHandler for GltfExtensionHandlerAnimation {
157 fn dyn_clone(&self) -> Box<dyn ErasedGltfExtensionHandler> {
158 Box::new((*self).clone())
159 }
160
161 #[cfg(feature = "bevy_animation")]
162 fn on_animation(
163 &mut self,
164 _load_context: &mut LoadContext<'_>,
165 gltf_animation: &gltf::Animation,
166 animation_clip: &mut AnimationClip,
167 ) {
168 if gltf_animation.name().is_some_and(|v| v == "Run") {
169 let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
170 let front_left_foot = hip_node.iter().chain(
171 [
172 "b_Spine01_02",
173 "b_Spine02_03",
174 "b_LeftUpperArm_09",
175 "b_LeftForeArm_010",
176 "b_LeftHand_011",
177 ]
178 .iter(),
179 );
180 let front_right_foot = hip_node.iter().chain(
181 [
182 "b_Spine01_02",
183 "b_Spine02_03",
184 "b_RightUpperArm_06",
185 "b_RightForeArm_07",
186 "b_RightHand_08",
187 ]
188 .iter(),
189 );
190 let back_left_foot = hip_node.iter().chain(
191 [
192 "b_LeftLeg01_015",
193 "b_LeftLeg02_016",
194 "b_LeftFoot01_017",
195 "b_LeftFoot02_018",
196 ]
197 .iter(),
198 );
199 let back_right_foot = hip_node.iter().chain(
200 [
201 "b_RightLeg01_019",
202 "b_RightLeg02_020",
203 "b_RightFoot01_021",
204 "b_RightFoot02_022",
205 ]
206 .iter(),
207 );
208 animation_clip.add_event_to_target(
209 AnimationTargetId::from_iter(front_left_foot),
210 0.625,
211 Step,
212 );
213 animation_clip.add_event_to_target(
214 AnimationTargetId::from_iter(front_right_foot),
215 0.5,
216 Step,
217 );
218 animation_clip.add_event_to_target(
219 AnimationTargetId::from_iter(back_left_foot),
220 0.0,
221 Step,
222 );
223 animation_clip.add_event_to_target(
224 AnimationTargetId::from_iter(back_right_foot),
225 0.125,
226 Step,
227 );
228 }
229 }
230 #[cfg(feature = "bevy_animation")]
231 fn on_animations_collected(
232 &mut self,
233 _load_context: &mut LoadContext<'_>,
234 _animations: &[Handle<AnimationClip>],
235 named_animations: &HashMap<Box<str>, Handle<AnimationClip>>,
236 animation_roots: &HashSet<usize>,
237 ) {
238 self.animation_root_indices = animation_roots.clone();
239
240 if let Some(handle) = named_animations.get("Run") {
241 self.clip = Some(handle.clone());
242 }
243 }
244
245 fn on_gltf_node(
246 &mut self,
247 _load_context: &mut LoadContext<'_>,
248 gltf_node: &gltf::Node,
249 entity: &mut EntityWorldMut,
250 ) {
251 if self.animation_root_indices.contains(&gltf_node.index()) {
252 self.animation_root_entities.insert(entity.id());
253 }
254 }
255
256 fn on_scene_completed(
258 &mut self,
259 load_context: &mut LoadContext<'_>,
260 _scene: &gltf::Scene,
261 _world_root_id: Entity,
262 world: &mut World,
263 ) {
264 let (graph, index) = AnimationGraph::from_clip(self.clip.clone().unwrap());
266 let graph_handle =
269 load_context.add_labeled_asset("MyAnimationGraphLabel".to_string(), graph);
270
271 let animation_to_play = AnimationToPlay {
273 graph_handle,
274 index,
275 };
276
277 let mut entity = world.entity_mut(*self.animation_root_entities.iter().next().unwrap());
279 entity.insert(animation_to_play);
280 }
281}
282
283fn simulate_particles(
284 mut commands: Commands,
285 mut query: Query<(Entity, &mut Transform, &mut Particle)>,
286 time: Res<Time>,
287) {
288 for (entity, mut transform, mut particle) in &mut query {
289 if particle.lifetime_timer.tick(time.delta()).just_finished() {
290 commands.entity(entity).despawn();
291 return;
292 }
293
294 transform.translation += particle.velocity * time.delta_secs();
295 transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
296 particle
297 .velocity
298 .smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
299 }
300}
301
302#[derive(Component)]
303struct Particle {
304 lifetime_timer: Timer,
305 size: f32,
306 velocity: Vec3,
307}
308
309#[derive(Resource)]
310struct ParticleAssets {
311 mesh: Handle<Mesh>,
312 material: Handle<StandardMaterial>,
313}
314
315impl FromWorld for ParticleAssets {
316 fn from_world(world: &mut World) -> Self {
317 Self {
318 mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
319 material: world.add_asset::<StandardMaterial>(StandardMaterial {
320 base_color: Color::WHITE,
321 ..Default::default()
322 }),
323 }
324 }
325}
326
327#[derive(AnimationEvent, Reflect, Clone)]
328struct Step;
329
330fn observe_on_step(
331 step: On<Step>,
332 particle: Res<ParticleAssets>,
333 mut commands: Commands,
334 transforms: Query<&GlobalTransform>,
335 mut seeded_rng: ResMut<SeededRng>,
336) -> Result {
337 let translation = transforms.get(step.trigger().target)?.translation();
338 for _ in 0..14 {
340 let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);
341 let vertical = seeded_rng.0.random_range(0.0..4.0);
342 let size = seeded_rng.0.random_range(0.2..1.0);
343
344 commands.spawn((
345 Particle {
346 lifetime_timer: Timer::from_seconds(
347 seeded_rng.0.random_range(0.2..0.6),
348 TimerMode::Once,
349 ),
350 size,
351 velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
352 },
353 Mesh3d(particle.mesh.clone()),
354 MeshMaterial3d(particle.material.clone()),
355 Transform {
356 translation,
357 scale: Vec3::splat(size),
358 ..Default::default()
359 },
360 ));
361 }
362 Ok(())
363}