//! Plays animations from a skinned glTF. use std::{f32::consts::PI, time::Duration}; use bevy::{ animation::AnimationTargetId, color::palettes::css::WHITE, pbr::CascadeShadowConfigBuilder, prelude::*, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; const FOX_PATH: &str = "models/animated/Fox.glb"; fn main() { App::new() .insert_resource(AmbientLight { color: Color::WHITE, brightness: 2000., ..default() }) .add_plugins(DefaultPlugins) .init_resource::() .init_resource::() .add_systems(Startup, setup) .add_systems(Update, setup_scene_once_loaded) .add_systems(Update, simulate_particles) .add_observer(observe_on_step) .run(); } #[derive(Resource)] struct SeededRng(ChaCha8Rng); #[derive(Resource)] struct Animations { index: AnimationNodeIndex, graph_handle: Handle, } #[derive(Event, Reflect, Clone)] struct OnStep; fn observe_on_step( trigger: Trigger, particle: Res, mut commands: Commands, transforms: Query<&GlobalTransform>, mut seeded_rng: ResMut, ) { let translation = transforms.get(trigger.target()).unwrap().translation(); // Spawn a bunch of particles. for _ in 0..14 { let horizontal = seeded_rng.0.r#gen::() * seeded_rng.0.gen_range(8.0..12.0); let vertical = seeded_rng.0.gen_range(0.0..4.0); let size = seeded_rng.0.gen_range(0.2..1.0); commands.spawn(( Particle { lifetime_timer: Timer::from_seconds( seeded_rng.0.gen_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() }, )); } } fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, mut graphs: ResMut>, ) { // Build the animation graph let (graph, index) = AnimationGraph::from_clip( // We specifically want the "run" animation, which is the third one. asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)), ); // Insert a resource with the current scene information let graph_handle = graphs.add(graph); commands.insert_resource(Animations { index, graph_handle, }); // 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 { shadows_enabled: true, ..default() }, CascadeShadowConfigBuilder { first_cascade_far_bound: 200.0, maximum_distance: 400.0, ..default() } .build(), )); // Fox commands.spawn(SceneRoot( asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)), )); // 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)); } // An `AnimationPlayer` is automatically added to the scene when it's ready. // When the player is added, start the animation. fn setup_scene_once_loaded( mut commands: Commands, animations: Res, feet: Res, graphs: Res>, mut clips: ResMut>, mut players: Query<(Entity, &mut AnimationPlayer), Added>, ) { fn get_clip<'a>( node: AnimationNodeIndex, graph: &AnimationGraph, clips: &'a mut Assets, ) -> &'a mut AnimationClip { let node = graph.get(node).unwrap(); let clip = match &node.node_type { AnimationNodeType::Clip(handle) => clips.get_mut(handle), _ => unreachable!(), }; clip.unwrap() } for (entity, mut player) in &mut players { // Send `OnStep` events once the fox feet hits the ground in the running animation. let graph = graphs.get(&animations.graph_handle).unwrap(); let running_animation = get_clip(animations.index, graph, &mut clips); // You can determine the time an event should trigger if you know witch frame it occurs and // the frame rate of the animation. Let's say we want to trigger an event at frame 15, // and the animation has a frame rate of 24 fps, then time = 15 / 24 = 0.625. running_animation.add_event_to_target(feet.front_left, 0.625, OnStep); running_animation.add_event_to_target(feet.front_right, 0.5, OnStep); running_animation.add_event_to_target(feet.back_left, 0.0, OnStep); running_animation.add_event_to_target(feet.back_right, 0.125, OnStep); // Start the animation let mut transitions = AnimationTransitions::new(); // Make sure to start the animation via the `AnimationTransitions` // component. The `AnimationTransitions` component wants to manage all // the animations and will get confused if the animations are started // directly via the `AnimationPlayer`. transitions .play(&mut player, animations.index, Duration::ZERO) .repeat(); commands .entity(entity) .insert(AnimationGraphHandle(animations.graph_handle.clone())) .insert(transitions); } } fn simulate_particles( mut commands: Commands, mut query: Query<(Entity, &mut Transform, &mut Particle)>, time: Res