Skip to main content

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 chacha20::ChaCha8Rng;
12use rand::{RngExt, SeedableRng};
13
14const FOX_PATH: &str = "models/animated/Fox.glb";
15
16fn 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)]
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.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
80fn 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.
140fn 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
194fn 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)]
214struct Particle {
215    lifetime_timer: Timer,
216    size: f32,
217    velocity: Vec3,
218}
219
220#[derive(Resource)]
221struct ParticleAssets {
222    mesh: Handle<Mesh>,
223    material: Handle<StandardMaterial>,
224}
225
226impl 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)]
240struct FoxFeetTargets {
241    front_right: AnimationTargetId,
242    front_left: AnimationTargetId,
243    back_left: AnimationTargetId,
244    back_right: AnimationTargetId,
245}
246
247impl 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}