Path: blob/main/examples/animation/animated_mesh_events.rs
9395 views
//! Plays animations from a skinned glTF.12use std::{f32::consts::PI, time::Duration};34use bevy::{5animation::{AnimationEvent, AnimationTargetId},6color::palettes::css::WHITE,7light::CascadeShadowConfigBuilder,8prelude::*,9};10use rand::{Rng, SeedableRng};11use rand_chacha::ChaCha8Rng;1213const FOX_PATH: &str = "models/animated/Fox.glb";1415fn main() {16App::new()17.insert_resource(GlobalAmbientLight {18color: Color::WHITE,19brightness: 2000.,20..default()21})22.add_plugins(DefaultPlugins)23.init_resource::<ParticleAssets>()24.init_resource::<FoxFeetTargets>()25.add_systems(Startup, setup)26.add_systems(Update, setup_scene_once_loaded)27.add_systems(Update, simulate_particles)28.add_observer(observe_on_step)29.run();30}3132#[derive(Resource)]33struct SeededRng(ChaCha8Rng);3435#[derive(Resource)]36struct Animations {37index: AnimationNodeIndex,38graph_handle: Handle<AnimationGraph>,39}4041#[derive(AnimationEvent, Reflect, Clone)]42struct Step;4344fn observe_on_step(45step: On<Step>,46particle: Res<ParticleAssets>,47mut commands: Commands,48transforms: Query<&GlobalTransform>,49mut seeded_rng: ResMut<SeededRng>,50) -> Result {51let translation = transforms.get(step.trigger().target)?.translation();52// Spawn a bunch of particles.53for _ in 0..14 {54let horizontal = seeded_rng.0.random::<Dir2>() * seeded_rng.0.random_range(8.0..12.0);55let vertical = seeded_rng.0.random_range(0.0..4.0);56let size = seeded_rng.0.random_range(0.2..1.0);5758commands.spawn((59Particle {60lifetime_timer: Timer::from_seconds(61seeded_rng.0.random_range(0.2..0.6),62TimerMode::Once,63),64size,65velocity: Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,66},67Mesh3d(particle.mesh.clone()),68MeshMaterial3d(particle.material.clone()),69Transform {70translation,71scale: Vec3::splat(size),72..Default::default()73},74));75}76Ok(())77}7879fn setup(80mut commands: Commands,81asset_server: Res<AssetServer>,82mut meshes: ResMut<Assets<Mesh>>,83mut materials: ResMut<Assets<StandardMaterial>>,84mut graphs: ResMut<Assets<AnimationGraph>>,85) {86// Build the animation graph87let (graph, index) = AnimationGraph::from_clip(88// We specifically want the "run" animation, which is the third one.89asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),90);9192// Insert a resource with the current scene information93let graph_handle = graphs.add(graph);94commands.insert_resource(Animations {95index,96graph_handle,97});9899// Camera100commands.spawn((101Camera3d::default(),102Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),103));104105// Plane106commands.spawn((107Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))),108MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),109));110111// Light112commands.spawn((113Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),114DirectionalLight {115shadow_maps_enabled: true,116..default()117},118CascadeShadowConfigBuilder {119first_cascade_far_bound: 200.0,120maximum_distance: 400.0,121..default()122}123.build(),124));125126// Fox127commands.spawn(SceneRoot(128asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),129));130131// We're seeding the PRNG here to make this example deterministic for testing purposes.132// This isn't strictly required in practical use unless you need your app to be deterministic.133let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);134commands.insert_resource(SeededRng(seeded_rng));135}136137// An `AnimationPlayer` is automatically added to the scene when it's ready.138// When the player is added, start the animation.139fn setup_scene_once_loaded(140mut commands: Commands,141animations: Res<Animations>,142feet: Res<FoxFeetTargets>,143graphs: Res<Assets<AnimationGraph>>,144mut clips: ResMut<Assets<AnimationClip>>,145mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,146) {147fn get_clip<'a>(148node: AnimationNodeIndex,149graph: &AnimationGraph,150clips: &'a mut Assets<AnimationClip>,151) -> &'a mut AnimationClip {152let node = graph.get(node).unwrap();153let clip = match &node.node_type {154AnimationNodeType::Clip(handle) => clips.get_mut(handle),155_ => unreachable!(),156};157clip.unwrap().into_inner()158}159160for (entity, mut player) in &mut players {161// Send `OnStep` events once the fox feet hits the ground in the running animation.162163let graph = graphs.get(&animations.graph_handle).unwrap();164let running_animation = get_clip(animations.index, graph, &mut clips);165166// You can determine the time an event should trigger if you know which frame it occurs and167// the frame rate of the animation. Let's say we want to trigger an event at frame 15,168// and the animation has a frame rate of 24 fps, then time = 15 / 24 = 0.625.169running_animation.add_event_to_target(feet.front_left, 0.625, Step);170running_animation.add_event_to_target(feet.front_right, 0.5, Step);171running_animation.add_event_to_target(feet.back_left, 0.0, Step);172running_animation.add_event_to_target(feet.back_right, 0.125, Step);173174// Start the animation175176let mut transitions = AnimationTransitions::new();177178// Make sure to start the animation via the `AnimationTransitions`179// component. The `AnimationTransitions` component wants to manage all180// the animations and will get confused if the animations are started181// directly via the `AnimationPlayer`.182transitions183.play(&mut player, animations.index, Duration::ZERO)184.repeat();185186commands187.entity(entity)188.insert(AnimationGraphHandle(animations.graph_handle.clone()))189.insert(transitions);190}191}192193fn simulate_particles(194mut commands: Commands,195mut query: Query<(Entity, &mut Transform, &mut Particle)>,196time: Res<Time>,197) {198for (entity, mut transform, mut particle) in &mut query {199if particle.lifetime_timer.tick(time.delta()).just_finished() {200commands.entity(entity).despawn();201return;202}203204transform.translation += particle.velocity * time.delta_secs();205transform.scale = Vec3::splat(particle.size.lerp(0.0, particle.lifetime_timer.fraction()));206particle207.velocity208.smooth_nudge(&Vec3::ZERO, 4.0, time.delta_secs());209}210}211212#[derive(Component)]213struct Particle {214lifetime_timer: Timer,215size: f32,216velocity: Vec3,217}218219#[derive(Resource)]220struct ParticleAssets {221mesh: Handle<Mesh>,222material: Handle<StandardMaterial>,223}224225impl FromWorld for ParticleAssets {226fn from_world(world: &mut World) -> Self {227Self {228mesh: world.add_asset::<Mesh>(Sphere::new(10.0)),229material: world.add_asset::<StandardMaterial>(StandardMaterial {230base_color: WHITE.into(),231..Default::default()232}),233}234}235}236237/// Stores the `AnimationTargetId`s of the fox's feet238#[derive(Resource)]239struct FoxFeetTargets {240front_right: AnimationTargetId,241front_left: AnimationTargetId,242back_left: AnimationTargetId,243back_right: AnimationTargetId,244}245246impl Default for FoxFeetTargets {247fn default() -> Self {248let hip_node = ["root", "_rootJoint", "b_Root_00", "b_Hip_01"];249let front_left_foot = hip_node.iter().chain(250[251"b_Spine01_02",252"b_Spine02_03",253"b_LeftUpperArm_09",254"b_LeftForeArm_010",255"b_LeftHand_011",256]257.iter(),258);259let front_right_foot = hip_node.iter().chain(260[261"b_Spine01_02",262"b_Spine02_03",263"b_RightUpperArm_06",264"b_RightForeArm_07",265"b_RightHand_08",266]267.iter(),268);269let back_left_foot = hip_node.iter().chain(270[271"b_LeftLeg01_015",272"b_LeftLeg02_016",273"b_LeftFoot01_017",274"b_LeftFoot02_018",275]276.iter(),277);278let back_right_foot = hip_node.iter().chain(279[280"b_RightLeg01_019",281"b_RightLeg02_020",282"b_RightFoot01_021",283"b_RightFoot02_022",284]285.iter(),286);287Self {288front_left: AnimationTargetId::from_iter(front_left_foot),289front_right: AnimationTargetId::from_iter(front_right_foot),290back_left: AnimationTargetId::from_iter(back_left_foot),291back_right: AnimationTargetId::from_iter(back_right_foot),292}293}294}295296297