glTF / glTF extension AnimationGraph

Back to examples View in GitHub

Uses glTF extension processing to play an animation on a skinned glTF model of a fox.

use std::f32::consts::PI;

use bevy::{
    animation::{AnimationEvent, AnimationTargetId},
    asset::LoadContext,
    ecs::entity::EntityHashSet,
    gltf::extensions::{ErasedGltfExtensionHandler, GltfExtensionHandler, GltfExtensionHandlers},
    light::CascadeShadowConfigBuilder,
    platform::collections::{HashMap, HashSet},
    prelude::*,
    world_serialization::WorldInstanceReady,
};
use chacha20::ChaCha8Rng;
use rand::{RngExt, SeedableRng};

/// An example asset that contains a mesh and animation.
const GLTF_PATH: &str = "models/animated/Fox.glb";

fn main() {
    App::new()
        .insert_resource(GlobalAmbientLight {
            color: Color::WHITE,
            brightness: 2000.,
            ..default()
        })
        .add_plugins((DefaultPlugins, GltfExtensionHandlerAnimationPlugin))
        .init_resource::<ParticleAssets>()
        .add_systems(
            Startup,
            (setup_mesh_and_animation, setup_camera_and_environment),
        )
        .add_systems(Update, simulate_particles)
        .add_observer(observe_on_step)
        .run();
}

#[derive(Resource)]
struct SeededRng(ChaCha8Rng);

/// A component that stores a reference to an animation we want to play. This is
/// created when we start loading the mesh (see `setup_mesh_and_animation`) and
/// read when the mesh has spawned (see `play_animation_once_loaded`).
#[derive(Component, Reflect)]
#[reflect(Component)]
struct AnimationToPlay {
    graph_handle: Handle<AnimationGraph>,
    index: AnimationNodeIndex,
}

fn setup_mesh_and_animation(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn an entity with our components, and connect it to an observer that
    // will trigger when the scene is loaded and spawned.
    commands
        .spawn(WorldAssetRoot(
            asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)),
        ))
        .observe(play_animation_when_ready);
}

fn play_animation_when_ready(
    scene_ready: On<WorldInstanceReady>,
    mut commands: Commands,
    children: Query<&Children>,
    mut players: Query<(&mut AnimationPlayer, &AnimationToPlay)>,
) {
    for child in children.iter_descendants(scene_ready.entity) {
        let Ok((mut player, animation_to_play)) = players.get_mut(child) else {
            continue;
        };

        // Tell the animation player to start the animation and keep
        // repeating it.
        //
        // If you want to try stopping and switching animations, see the
        // `animated_mesh_control.rs` example.
        player.play(animation_to_play.index).repeat();

        // Add the animation graph. This only needs to be done once to
        // connect the animation player to the mesh.
        commands
            .entity(child)
            .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
    }
}

/// Spawn a camera and a simple environment with a ground plane and light.
fn setup_camera_and_environment(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // Camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
    ));

    // Plane
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
    ));

    // Light
    commands.spawn((
        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
        DirectionalLight {
            shadow_maps_enabled: true,
            ..default()
        },
        CascadeShadowConfigBuilder {
            first_cascade_far_bound: 200.0,
            maximum_distance: 400.0,
            ..default()
        }
        .build(),
    ));

    // We're seeding the PRNG here to make this example deterministic for testing purposes.
    // This isn't strictly required in practical use unless you need your app to be deterministic.
    let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
    commands.insert_resource(SeededRng(seeded_rng));
}

struct GltfExtensionHandlerAnimationPlugin;

impl Plugin for GltfExtensionHandlerAnimationPlugin {
    fn build(&self, app: &mut App) {
        #[cfg(target_family = "wasm")]
        bevy::tasks::block_on(async {
            app.world_mut()
                .resource_mut::<GltfExtensionHandlers>()
                .0
                .write()
                .await
                .push(Box::new(GltfExtensionHandlerAnimation::default()))
        });
        #[cfg(not(target_family = "wasm"))]
        app.world_mut()
            .resource_mut::<GltfExtensionHandlers>()
            .0
            .write_blocking()
            .push(Box::new(GltfExtensionHandlerAnimation::default()));
    }
}

#[derive(Default, Clone)]
struct GltfExtensionHandlerAnimation {
    animation_root_indices: HashSet<usize>,
    animation_root_entities: EntityHashSet,
    clip: Option<Handle<AnimationClip>>,
}

impl GltfExtensionHandler for GltfExtensionHandlerAnimation {
    fn dyn_clone(&self) -> Box<dyn ErasedGltfExtensionHandler> {
        Box::new((*self).clone())
    }

    #[cfg(feature = "bevy_animation")]
    fn on_animation(
        &mut self,
        _load_context: &mut LoadContext<'_>,
        gltf_animation: &gltf::Animation,
        animation_clip: &mut AnimationClip,
    ) {
        if gltf_animation.name().is_some_and(|v| v == "Run") {
            let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];
            let front_left_foot = hip_node.iter().chain(
                [
                    "b_Spine01_02",
                    "b_Spine02_03",
                    "b_LeftUpperArm_09",
                    "b_LeftForeArm_010",
                    "b_LeftHand_011",
                ]
                .iter(),
            );
            let front_right_foot = hip_node.iter().chain(
                [
                    "b_Spine01_02",
                    "b_Spine02_03",
                    "b_RightUpperArm_06",
                    "b_RightForeArm_07",
                    "b_RightHand_08",
                ]
                .iter(),
            );
            let back_left_foot = hip_node.iter().chain(
                [
                    "b_LeftLeg01_015",
                    "b_LeftLeg02_016",
                    "b_LeftFoot01_017",
                    "b_LeftFoot02_018",
                ]
                .iter(),
            );
            let back_right_foot = hip_node.iter().chain(
                [
                    "b_RightLeg01_019",
                    "b_RightLeg02_020",
                    "b_RightFoot01_021",
                    "b_RightFoot02_022",
                ]
                .iter(),
            );
            animation_clip.add_event_to_target(
                AnimationTargetId::from_iter(front_left_foot),
                0.625,
                Step,
            );
            animation_clip.add_event_to_target(
                AnimationTargetId::from_iter(front_right_foot),
                0.5,
                Step,
            );
            animation_clip.add_event_to_target(
                AnimationTargetId::from_iter(back_left_foot),
                0.0,
                Step,
            );
            animation_clip.add_event_to_target(
                AnimationTargetId::from_iter(back_right_foot),
                0.125,
                Step,
            );
        }
    }
    #[cfg(feature = "bevy_animation")]
    fn on_animations_collected(
        &mut self,
        _load_context: &mut LoadContext<'_>,
        _animations: &[Handle<AnimationClip>],
        named_animations: &HashMap<Box<str>, Handle<AnimationClip>>,
        animation_roots: &HashSet<usize>,
    ) {
        self.animation_root_indices = animation_roots.clone();

        if let Some(handle) = named_animations.get("Run") {
            self.clip = Some(handle.clone());
        }
    }

    fn on_gltf_node(
        &mut self,
        _load_context: &mut LoadContext<'_>,
        gltf_node: &gltf::Node,
        entity: &mut EntityWorldMut,
    ) {
        if self.animation_root_indices.contains(&gltf_node.index()) {
            self.animation_root_entities.insert(entity.id());
        }
    }

    /// Called when an individual Scene is done processing
    fn on_scene_completed(
        &mut self,
        load_context: &mut LoadContext<'_>,
        _scene: &gltf::Scene,
        _world_root_id: Entity,
        world: &mut World,
    ) {
        // Create an AnimationGraph from the desired clip
        let (graph, index) = AnimationGraph::from_clip(self.clip.clone().unwrap());
        // Store the animation graph as an asset with an arbitrary label
        // We only have one graph, so this label will be unique
        let graph_handle =
            load_context.add_labeled_asset("MyAnimationGraphLabel".to_string(), graph);

        // Create a component that stores a reference to our animation
        let animation_to_play = AnimationToPlay {
            graph_handle,
            index,
        };

        // Insert the `AnimationToPlay` component on the first animation root
        let mut entity = world.entity_mut(*self.animation_root_entities.iter().next().unwrap());
        entity.insert(animation_to_play);
    }
}

fn simulate_particles(
    mut commands: Commands,
    mut query: Query<(Entity, &mut Transform, &mut Particle)>,
    time: Res<Time>,
) {
    for (entity, mut transform, mut particle) in &mut query {
        if particle.lifetime_timer.tick(time.delta()).just_finished() {
            commands.entity(entity).despawn();
            return;
        }

        transform.translation += particle.velocity * time.delta_secs();
        transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));
        particle
            .velocity
            .smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());
    }
}

#[derive(Component)]
struct Particle {
    lifetime_timer: Timer,
    size: f32,
    velocity: Vec3,
}

#[derive(Resource)]
struct ParticleAssets {
    mesh: Handle<Mesh>,
    material: Handle<StandardMaterial>,
}

impl FromWorld for ParticleAssets {
    fn from_world(world: &mut World) -> Self {
        Self {
            mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),
            material: world.add_asset::<StandardMaterial>(StandardMaterial {
                base_color: Color::WHITE,
                ..Default::default()
            }),
        }
    }
}

#[derive(AnimationEvent, Reflect, Clone)]
struct Step;

fn observe_on_step(
    step: On<Step>,
    particle: Res<ParticleAssets>,
    mut commands: Commands,
    transforms: Query<&GlobalTransform>,
    mut seeded_rng: ResMut<SeededRng>,
) -> Result {
    let translation = transforms.get(step.trigger().target)?.translation();
    // Spawn a bunch of particles.
    for _ in 0..14 {
        let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);
        let vertical = seeded_rng.0.random_range(0.0..4.0);
        let size = seeded_rng.0.random_range(0.2..1.0);

        commands.spawn((
            Particle {
                lifetime_timer: Timer::from_seconds(
                    seeded_rng.0.random_range(0.2..0.6),
                    TimerMode::Once,
                ),
                size,
                velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
            },
            Mesh3d(particle.mesh.clone()),
            MeshMaterial3d(particle.material.clone()),
            Transform {
                translation,
                scale: Vec3::splat(size),
                ..Default::default()
            },
        ));
    }
    Ok(())
}