Skip to main content

gltf_extension_animation_graph/
gltf_extension_animation_graph.rs

1//! Uses glTF extension processing to play an animation on a skinned glTF model of a fox.
2
3use 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
18/// An example asset that contains a mesh and animation.
19const 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/// A component that stores a reference to an animation we want to play. This is
43/// created when we start loading the mesh (see `setup_mesh_and_animation`) and
44/// read when the mesh has spawned (see `play_animation_once_loaded`).
45#[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    // Spawn an entity with our components, and connect it to an observer that
54    // will trigger when the scene is loaded and spawned.
55    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        // Tell the animation player to start the animation and keep
74        // repeating it.
75        //
76        // If you want to try stopping and switching animations, see the
77        // `animated_mesh_control.rs` example.
78        player.play(animation_to_play.index).repeat();
79
80        // Add the animation graph. This only needs to be done once to
81        // connect the animation player to the mesh.
82        commands
83            .entity(child)
84            .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
85    }
86}
87
88/// Spawn a camera and a simple environment with a ground plane and light.
89fn setup_camera_and_environment(
90    mut commands: Commands,
91    mut meshes: ResMut<Assets<Mesh>>,
92    mut materials: ResMut<Assets<StandardMaterial>>,
93) {
94    // Camera
95    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    // Plane
101    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    // Light
107    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    // We're seeding the PRNG here to make this example deterministic for testing purposes.
122    // This isn't strictly required in practical use unless you need your app to be deterministic.
123    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    /// Called when an individual Scene is done processing
257    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        // Create an AnimationGraph from the desired clip
265        let (graph, index) = AnimationGraph::from_clip(self.clip.clone().unwrap());
266        // Store the animation graph as an asset with an arbitrary label
267        // We only have one graph, so this label will be unique
268        let graph_handle =
269            load_context.add_labeled_asset("MyAnimationGraphLabel".to_string(), graph);
270
271        // Create a component that stores a reference to our animation
272        let animation_to_play = AnimationToPlay {
273            graph_handle,
274            index,
275        };
276
277        // Insert the `AnimationToPlay` component on the first animation root
278        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    // Spawn a bunch of particles.
339    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}