animated_mesh_events/
animated_mesh_events.rs

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