Split up animated_fox
example (#17191)
# Objective Our `animated_fox` example used to be a bare-bones example of how to spawn an animated gltf and play a single animation. I think that's a valuable example, and the current `animated_fox` example is doing way too much. Users who are trying to understand how our animation system are presented with an enormous amount of information that may not be immediately relevant. Over the past few releases, I've been migrating a simple app of mine where the only animation I need is a single gltf that starts playing a single animation when it is loaded. It has been a slight struggle to wade through changes to the animation system to figure out the minimal amount of things required to accomplish this. Somewhat motivated by this [recent reddit thread](https://www.reddit.com/r/rust/comments/1ht93vl/comment/m5c0nc9/?utm_source=share&utm_medium=mweb3x&utm_name=mweb3xcss&utm_term=1) where Bevy and animation got a mention. ## Solution - Split `animated_fox` into three separate examples - `animated_fox` - Loads and immediately plays a single animation - `animated_fox_control` - Shows how to control animations - `animated_fox_events` - Shows fancy particles when the fox's feet hit the ground - Some minor drive-by tidying of these examples I have created this PR after playing around with the idea and liking how it turned out, but the duplication isn't totally ideal and there's some slight overlap with other examples and inconsistencies: - `animation_events` is simplified and not specific to "loaded animated scenes" and seems valuable on its own - `animation_graph` also uses a fox I am happy to close this if there's no consensus that it's a good idea / step forward for these examples. ## Testing `cargo run --example animated_fox` `cargo run --example animated_fox_control` `cargo run --example animated_fox_events`
This commit is contained in:
parent
94596d2bbf
commit
6f68776eac
22
Cargo.toml
22
Cargo.toml
@ -1291,6 +1291,28 @@ description = "Plays an animation from a skinned glTF"
|
||||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "animated_fox_control"
|
||||
path = "examples/animation/animated_fox_control.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.animated_fox_control]
|
||||
name = "Animated Fox Control"
|
||||
description = "Plays an animation from a skinned glTF with keyboard controls"
|
||||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "animated_fox_events"
|
||||
path = "examples/animation/animated_fox_events.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.animated_fox_events]
|
||||
name = "Animated Fox Events"
|
||||
description = "Plays an animation from a skinned glTF with events"
|
||||
category = "Animation"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "animation_graph"
|
||||
path = "examples/animation/animation_graph.rs"
|
||||
|
@ -195,6 +195,8 @@ Example | Description
|
||||
Example | Description
|
||||
--- | ---
|
||||
[Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF
|
||||
[Animated Fox Control](../examples/animation/animated_fox_control.rs) | Plays an animation from a skinned glTF with keyboard controls
|
||||
[Animated Fox Events](../examples/animation/animated_fox_events.rs) | Plays an animation from a skinned glTF with events
|
||||
[Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component
|
||||
[Animated UI](../examples/animation/animated_ui.rs) | Shows how to use animation clips to animate UI properties
|
||||
[Animation Events](../examples/animation/animation_events.rs) | Demonstrate how to use animation events
|
||||
|
@ -2,14 +2,7 @@
|
||||
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
|
||||
use bevy::{
|
||||
animation::{AnimationTargetId, RepeatAnimation},
|
||||
color::palettes::css::WHITE,
|
||||
pbr::CascadeShadowConfigBuilder,
|
||||
prelude::*,
|
||||
};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*};
|
||||
|
||||
const FOX_PATH: &str = "models/animated/Fox.glb";
|
||||
|
||||
@ -21,49 +14,15 @@ fn main() {
|
||||
..default()
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.init_resource::<ParticleAssets>()
|
||||
.init_resource::<FoxFeetTargets>()
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, setup_scene_once_loaded)
|
||||
.add_systems(Update, (keyboard_animation_control, simulate_particles))
|
||||
.add_observer(observe_on_step)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct SeededRng(ChaCha8Rng);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Animations {
|
||||
animations: Vec<AnimationNodeIndex>,
|
||||
graph: Handle<AnimationGraph>,
|
||||
}
|
||||
|
||||
#[derive(Event, Reflect, Clone)]
|
||||
struct OnStep;
|
||||
|
||||
fn observe_on_step(
|
||||
trigger: Trigger<OnStep>,
|
||||
particle: Res<ParticleAssets>,
|
||||
mut commands: Commands,
|
||||
transforms: Query<&GlobalTransform>,
|
||||
mut seeded_rng: ResMut<SeededRng>,
|
||||
) {
|
||||
let translation = transforms.get(trigger.target()).unwrap().translation();
|
||||
// Spawn a bunch of particles.
|
||||
for _ in 0..14 {
|
||||
let horizontal = seeded_rng.0.gen::<Dir2>() * 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.queue(spawn_particle(
|
||||
particle.mesh.clone(),
|
||||
particle.material.clone(),
|
||||
translation.reject_from_normalized(Vec3::Y),
|
||||
seeded_rng.0.gen_range(0.2..0.6),
|
||||
size,
|
||||
Vec3::new(horizontal.x, vertical, horizontal.y) * 10.0,
|
||||
));
|
||||
}
|
||||
graph_handle: Handle<AnimationGraph>,
|
||||
index: AnimationNodeIndex,
|
||||
}
|
||||
|
||||
fn setup(
|
||||
@ -74,17 +33,17 @@ fn setup(
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
// Build the animation graph
|
||||
let (graph, node_indices) = AnimationGraph::from_clips([
|
||||
asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
|
||||
asset_server.load(GltfAssetLabel::Animation(1).from_asset(FOX_PATH)),
|
||||
let (graph, index) = AnimationGraph::from_clip(
|
||||
// We specifically want the "walk" animation, which is the first one.
|
||||
asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_PATH)),
|
||||
]);
|
||||
);
|
||||
|
||||
// Insert a resource with the current scene information
|
||||
// Keep our animation graph in a Resource so that it can be inserted onto
|
||||
// the correct entity once the scene actually loads.
|
||||
let graph_handle = graphs.add(graph);
|
||||
commands.insert_resource(Animations {
|
||||
animations: node_indices,
|
||||
graph: graph_handle,
|
||||
graph_handle: graph_handle.clone(),
|
||||
index,
|
||||
});
|
||||
|
||||
// Camera
|
||||
@ -115,60 +74,19 @@ fn setup(
|
||||
));
|
||||
|
||||
// Fox
|
||||
commands.spawn(SceneRoot(
|
||||
asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH)),
|
||||
commands.spawn((
|
||||
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH))),
|
||||
AnimationGraphHandle(graph_handle),
|
||||
));
|
||||
|
||||
println!("Animation controls:");
|
||||
println!(" - spacebar: play / pause");
|
||||
println!(" - arrow up / down: speed up / slow down animation playback");
|
||||
println!(" - arrow left / right: seek backward / forward");
|
||||
println!(" - digit 1 / 3 / 5: play the animation <digit> times");
|
||||
println!(" - L: loop the animation forever");
|
||||
println!(" - return: change animation");
|
||||
|
||||
// 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.
|
||||
// Once the scene is loaded, start the animation
|
||||
fn setup_scene_once_loaded(
|
||||
mut commands: Commands,
|
||||
animations: Res<Animations>,
|
||||
feet: Res<FoxFeetTargets>,
|
||||
graphs: Res<Assets<AnimationGraph>>,
|
||||
mut clips: ResMut<Assets<AnimationClip>>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
) {
|
||||
fn get_clip<'a>(
|
||||
node: AnimationNodeIndex,
|
||||
graph: &AnimationGraph,
|
||||
clips: &'a mut Assets<AnimationClip>,
|
||||
) -> &'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 {
|
||||
let graph = graphs.get(&animations.graph).unwrap();
|
||||
|
||||
// Send `OnStep` events once the fox feet hits the ground in the running animation.
|
||||
let running_animation = get_clip(animations.animations[0], 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);
|
||||
|
||||
let mut transitions = AnimationTransitions::new();
|
||||
|
||||
// Make sure to start the animation via the `AnimationTransitions`
|
||||
@ -176,227 +94,12 @@ fn setup_scene_once_loaded(
|
||||
// the animations and will get confused if the animations are started
|
||||
// directly via the `AnimationPlayer`.
|
||||
transitions
|
||||
.play(&mut player, animations.animations[0], Duration::ZERO)
|
||||
.play(&mut player, animations.index, Duration::ZERO)
|
||||
.repeat();
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(AnimationGraphHandle(animations.graph.clone()))
|
||||
.insert(transitions);
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_animation_control(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
animations: Res<Animations>,
|
||||
mut current_animation: Local<usize>,
|
||||
) {
|
||||
for (mut player, mut transitions) in &mut animation_players {
|
||||
let Some((&playing_animation_index, _)) = player.playing_animations().next() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
if playing_animation.is_paused() {
|
||||
playing_animation.resume();
|
||||
} else {
|
||||
playing_animation.pause();
|
||||
}
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 1.2);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 0.8);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed - 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed + 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
*current_animation = (*current_animation + 1) % animations.animations.len();
|
||||
|
||||
transitions
|
||||
.play(
|
||||
&mut player,
|
||||
animations.animations[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit1) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(1))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit3) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(3))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit5) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(5))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::KeyL) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation.set_repeat(RepeatAnimation::Forever);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_particle<M: Material>(
|
||||
mesh: Handle<Mesh>,
|
||||
material: Handle<M>,
|
||||
translation: Vec3,
|
||||
lifetime: f32,
|
||||
size: f32,
|
||||
velocity: Vec3,
|
||||
) -> impl Command {
|
||||
move |world: &mut World| {
|
||||
world.spawn((
|
||||
Particle {
|
||||
lifetime_timer: Timer::from_seconds(lifetime, TimerMode::Once),
|
||||
size,
|
||||
velocity,
|
||||
},
|
||||
Mesh3d(mesh),
|
||||
MeshMaterial3d(material),
|
||||
Transform {
|
||||
translation,
|
||||
scale: Vec3::splat(size),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[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.resource_mut::<Assets<Mesh>>().add(Sphere::new(10.0)),
|
||||
material: world
|
||||
.resource_mut::<Assets<StandardMaterial>>()
|
||||
.add(StandardMaterial {
|
||||
base_color: WHITE.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct FoxFeetTargets {
|
||||
front_right: AnimationTargetId,
|
||||
front_left: AnimationTargetId,
|
||||
back_left: AnimationTargetId,
|
||||
back_right: AnimationTargetId,
|
||||
}
|
||||
|
||||
impl Default for FoxFeetTargets {
|
||||
fn default() -> Self {
|
||||
// Get the id's of the feet and store them in a resource.
|
||||
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(),
|
||||
);
|
||||
Self {
|
||||
front_left: AnimationTargetId::from_iter(front_left_foot),
|
||||
front_right: AnimationTargetId::from_iter(front_right_foot),
|
||||
back_left: AnimationTargetId::from_iter(back_left_foot),
|
||||
back_right: AnimationTargetId::from_iter(back_right_foot),
|
||||
}
|
||||
.insert(transitions)
|
||||
.insert(AnimationGraphHandle(animations.graph_handle.clone()));
|
||||
}
|
||||
}
|
||||
|
210
examples/animation/animated_fox_control.rs
Normal file
210
examples/animation/animated_fox_control.rs
Normal file
@ -0,0 +1,210 @@
|
||||
//! Plays animations from a skinned glTF.
|
||||
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
|
||||
use bevy::{animation::RepeatAnimation, pbr::CascadeShadowConfigBuilder, prelude::*};
|
||||
|
||||
const FOX_PATH: &str = "models/animated/Fox.glb";
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 2000.,
|
||||
..default()
|
||||
})
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, setup_scene_once_loaded)
|
||||
.add_systems(Update, keyboard_control)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Animations {
|
||||
animations: Vec<AnimationNodeIndex>,
|
||||
graph_handle: Handle<AnimationGraph>,
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
// Build the animation graph
|
||||
let (graph, node_indices) = AnimationGraph::from_clips([
|
||||
asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)),
|
||||
asset_server.load(GltfAssetLabel::Animation(1).from_asset(FOX_PATH)),
|
||||
asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_PATH)),
|
||||
]);
|
||||
|
||||
// Keep our animation graph in a Resource so that it can be inserted onto
|
||||
// the correct entity once the scene actually loads.
|
||||
let graph_handle = graphs.add(graph);
|
||||
commands.insert_resource(Animations {
|
||||
animations: node_indices,
|
||||
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)),
|
||||
));
|
||||
|
||||
// Instructions
|
||||
|
||||
commands.spawn((
|
||||
Text::new(concat!(
|
||||
"space: play / pause\n",
|
||||
"up / down: playback speed\n",
|
||||
"left / right: seek\n",
|
||||
"1-3: play N times\n",
|
||||
"L: loop forever\n",
|
||||
"return: change animation\n",
|
||||
)),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(12.0),
|
||||
left: Val::Px(12.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// 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<Animations>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
) {
|
||||
for (entity, mut player) in &mut players {
|
||||
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.animations[0], Duration::ZERO)
|
||||
.repeat();
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(AnimationGraphHandle(animations.graph_handle.clone()))
|
||||
.insert(transitions);
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_control(
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
||||
animations: Res<Animations>,
|
||||
mut current_animation: Local<usize>,
|
||||
) {
|
||||
for (mut player, mut transitions) in &mut animation_players {
|
||||
let Some((&playing_animation_index, _)) = player.playing_animations().next() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
if playing_animation.is_paused() {
|
||||
playing_animation.resume();
|
||||
} else {
|
||||
playing_animation.pause();
|
||||
}
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 1.2);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let speed = playing_animation.speed();
|
||||
playing_animation.set_speed(speed * 0.8);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed - 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
let elapsed = playing_animation.seek_time();
|
||||
playing_animation.seek_to(elapsed + 0.1);
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Enter) {
|
||||
*current_animation = (*current_animation + 1) % animations.animations.len();
|
||||
|
||||
transitions
|
||||
.play(
|
||||
&mut player,
|
||||
animations.animations[*current_animation],
|
||||
Duration::from_millis(250),
|
||||
)
|
||||
.repeat();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit1) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(1))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit2) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(2))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::Digit3) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation
|
||||
.set_repeat(RepeatAnimation::Count(3))
|
||||
.replay();
|
||||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::KeyL) {
|
||||
let playing_animation = player.animation_mut(playing_animation_index).unwrap();
|
||||
playing_animation.set_repeat(RepeatAnimation::Forever);
|
||||
}
|
||||
}
|
||||
}
|
292
examples/animation/animated_fox_events.rs
Normal file
292
examples/animation/animated_fox_events.rs
Normal file
@ -0,0 +1,292 @@
|
||||
//! 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::<ParticleAssets>()
|
||||
.init_resource::<FoxFeetTargets>()
|
||||
.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<AnimationGraph>,
|
||||
}
|
||||
|
||||
#[derive(Event, Reflect, Clone)]
|
||||
struct OnStep;
|
||||
|
||||
fn observe_on_step(
|
||||
trigger: Trigger<OnStep>,
|
||||
particle: Res<ParticleAssets>,
|
||||
mut commands: Commands,
|
||||
transforms: Query<&GlobalTransform>,
|
||||
mut seeded_rng: ResMut<SeededRng>,
|
||||
) {
|
||||
let translation = transforms.get(trigger.target()).unwrap().translation();
|
||||
// Spawn a bunch of particles.
|
||||
for _ in 0..14 {
|
||||
let horizontal = seeded_rng.0.gen::<Dir2>() * 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<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut graphs: ResMut<Assets<AnimationGraph>>,
|
||||
) {
|
||||
// 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<Animations>,
|
||||
feet: Res<FoxFeetTargets>,
|
||||
graphs: Res<Assets<AnimationGraph>>,
|
||||
mut clips: ResMut<Assets<AnimationClip>>,
|
||||
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
|
||||
) {
|
||||
fn get_clip<'a>(
|
||||
node: AnimationNodeIndex,
|
||||
graph: &AnimationGraph,
|
||||
clips: &'a mut Assets<AnimationClip>,
|
||||
) -> &'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<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: WHITE.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the `AnimationTargetId`s of the fox's feet
|
||||
#[derive(Resource)]
|
||||
struct FoxFeetTargets {
|
||||
front_right: AnimationTargetId,
|
||||
front_left: AnimationTargetId,
|
||||
back_left: AnimationTargetId,
|
||||
back_right: AnimationTargetId,
|
||||
}
|
||||
|
||||
impl Default for FoxFeetTargets {
|
||||
fn default() -> Self {
|
||||
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(),
|
||||
);
|
||||
Self {
|
||||
front_left: AnimationTargetId::from_iter(front_left_foot),
|
||||
front_right: AnimationTargetId::from_iter(front_right_foot),
|
||||
back_left: AnimationTargetId::from_iter(back_left_foot),
|
||||
back_right: AnimationTargetId::from_iter(back_right_foot),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user