mirror of
https://github.com/bevyengine/bevy
synced 2024-12-22 02:53:07 +00:00
dfdf2b9ea4
This is an implementation of RFC #51: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md Note that the implementation strategy is different from the one outlined in that RFC, because two-phase animation has now landed. # Objective Bevy needs animation blending. The RFC for this is [RFC 51]. ## Solution This is an implementation of the RFC. Note that the implementation strategy is different from the one outlined there, because two-phase animation has now landed. This is just a draft to get the conversation started. Currently we're missing a few things: - [x] A fully-fleshed-out mechanism for transitions - [x] A serialization format for `AnimationGraph`s - [x] Examples are broken, other than `animated_fox` - [x] Documentation --- ## Changelog ### Added * The `AnimationPlayer` has been reworked to support blending multiple animations together through an `AnimationGraph`, and as such will no longer function unless a `Handle<AnimationGraph>` has been added to the entity containing the player. See [RFC 51] for more details. * Transition functionality has moved from the `AnimationPlayer` to a new component, `AnimationTransitions`, which works in tandem with the `AnimationGraph`. ## Migration Guide * `AnimationPlayer`s can no longer play animations by themselves and need to be paired with a `Handle<AnimationGraph>`. Code that was using `AnimationPlayer` to play animations will need to create an `AnimationGraph` asset first, add a node for the clip (or clips) you want to play, and then supply the index of that node to the `AnimationPlayer`'s `play` method. * The `AnimationPlayer::play_with_transition()` method has been removed and replaced with the `AnimationTransitions` component. If you were previously using `AnimationPlayer::play_with_transition()`, add all animations that you were playing to the `AnimationGraph`, and create an `AnimationTransitions` component to manage the blending between them. [RFC 51]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md --------- Co-authored-by: Rob Parrett <robparrett@gmail.com>
331 lines
9.6 KiB
Rust
331 lines
9.6 KiB
Rust
//! Loads animations from a skinned glTF, spawns many of them, and plays the
|
|
//! animation to stress test skinned meshes.
|
|
|
|
use std::f32::consts::PI;
|
|
use std::time::Duration;
|
|
|
|
use argh::FromArgs;
|
|
use bevy::{
|
|
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
|
pbr::CascadeShadowConfigBuilder,
|
|
prelude::*,
|
|
window::{PresentMode, WindowResolution},
|
|
winit::{UpdateMode, WinitSettings},
|
|
};
|
|
|
|
#[derive(FromArgs, Resource)]
|
|
/// `many_foxes` stress test
|
|
struct Args {
|
|
/// whether all foxes run in sync.
|
|
#[argh(switch)]
|
|
sync: bool,
|
|
|
|
/// total number of foxes.
|
|
#[argh(option, default = "1000")]
|
|
count: usize,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct Foxes {
|
|
count: usize,
|
|
speed: f32,
|
|
moving: bool,
|
|
sync: bool,
|
|
}
|
|
|
|
fn main() {
|
|
// `from_env` panics on the web
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
let args: Args = argh::from_env();
|
|
#[cfg(target_arch = "wasm32")]
|
|
let args = Args::from_args(&[], &[]).unwrap();
|
|
|
|
App::new()
|
|
.add_plugins((
|
|
DefaultPlugins.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
|
|
present_mode: PresentMode::AutoNoVsync,
|
|
resolution: WindowResolution::new(1920.0, 1080.0)
|
|
.with_scale_factor_override(1.0),
|
|
..default()
|
|
}),
|
|
..default()
|
|
}),
|
|
FrameTimeDiagnosticsPlugin,
|
|
LogDiagnosticsPlugin::default(),
|
|
))
|
|
.insert_resource(WinitSettings {
|
|
focused_mode: UpdateMode::Continuous,
|
|
unfocused_mode: UpdateMode::Continuous,
|
|
})
|
|
.insert_resource(Foxes {
|
|
count: args.count,
|
|
speed: 2.0,
|
|
moving: true,
|
|
sync: args.sync,
|
|
})
|
|
.add_systems(Startup, setup)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
setup_scene_once_loaded,
|
|
keyboard_animation_control,
|
|
update_fox_rings.after(keyboard_animation_control),
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct Animations {
|
|
node_indices: Vec<AnimationNodeIndex>,
|
|
graph: Handle<AnimationGraph>,
|
|
}
|
|
|
|
const RING_SPACING: f32 = 2.0;
|
|
const FOX_SPACING: f32 = 2.0;
|
|
|
|
#[derive(Component, Clone, Copy)]
|
|
enum RotationDirection {
|
|
CounterClockwise,
|
|
Clockwise,
|
|
}
|
|
|
|
impl RotationDirection {
|
|
fn sign(&self) -> f32 {
|
|
match self {
|
|
RotationDirection::CounterClockwise => 1.0,
|
|
RotationDirection::Clockwise => -1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Ring {
|
|
radius: f32,
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
|
foxes: Res<Foxes>,
|
|
) {
|
|
warn!(include_str!("warning_string.txt"));
|
|
|
|
// Insert a resource with the current scene information
|
|
let animation_clips = [
|
|
asset_server.load("models/animated/Fox.glb#Animation2"),
|
|
asset_server.load("models/animated/Fox.glb#Animation1"),
|
|
asset_server.load("models/animated/Fox.glb#Animation0"),
|
|
];
|
|
let mut animation_graph = AnimationGraph::new();
|
|
let node_indices = animation_graph
|
|
.add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root)
|
|
.collect();
|
|
commands.insert_resource(Animations {
|
|
node_indices,
|
|
graph: animation_graphs.add(animation_graph),
|
|
});
|
|
|
|
// Foxes
|
|
// Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
|
|
// The foxes in each ring are spaced at least 2m apart around its circumference.'
|
|
|
|
// NOTE: This fox model faces +z
|
|
let fox_handle = asset_server.load("models/animated/Fox.glb#Scene0");
|
|
|
|
let ring_directions = [
|
|
(
|
|
Quat::from_rotation_y(PI),
|
|
RotationDirection::CounterClockwise,
|
|
),
|
|
(Quat::IDENTITY, RotationDirection::Clockwise),
|
|
];
|
|
|
|
let mut ring_index = 0;
|
|
let mut radius = RING_SPACING;
|
|
let mut foxes_remaining = foxes.count;
|
|
|
|
info!("Spawning {} foxes...", foxes.count);
|
|
|
|
while foxes_remaining > 0 {
|
|
let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
|
|
let ring_parent = commands
|
|
.spawn((
|
|
SpatialBundle::INHERITED_IDENTITY,
|
|
ring_direction,
|
|
Ring { radius },
|
|
))
|
|
.id();
|
|
|
|
let circumference = PI * 2. * radius;
|
|
let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
|
|
let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
|
|
|
|
for fox_i in 0..foxes_in_ring {
|
|
let fox_angle = fox_i as f32 * fox_spacing_angle;
|
|
let (s, c) = fox_angle.sin_cos();
|
|
let (x, z) = (radius * c, radius * s);
|
|
|
|
commands.entity(ring_parent).with_children(|builder| {
|
|
builder.spawn(SceneBundle {
|
|
scene: fox_handle.clone(),
|
|
transform: Transform::from_xyz(x, 0.0, z)
|
|
.with_scale(Vec3::splat(0.01))
|
|
.with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
|
|
..default()
|
|
});
|
|
});
|
|
}
|
|
|
|
foxes_remaining -= foxes_in_ring;
|
|
radius += RING_SPACING;
|
|
ring_index += 1;
|
|
}
|
|
|
|
// Camera
|
|
let zoom = 0.8;
|
|
let translation = Vec3::new(
|
|
radius * 1.25 * zoom,
|
|
radius * 0.5 * zoom,
|
|
radius * 1.5 * zoom,
|
|
);
|
|
commands.spawn(Camera3dBundle {
|
|
transform: Transform::from_translation(translation)
|
|
.looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
|
|
..default()
|
|
});
|
|
|
|
// Plane
|
|
commands.spawn(PbrBundle {
|
|
mesh: meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0)),
|
|
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
|
|
..default()
|
|
});
|
|
|
|
// Light
|
|
commands.spawn(DirectionalLightBundle {
|
|
transform: Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
|
|
directional_light: DirectionalLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
cascade_shadow_config: CascadeShadowConfigBuilder {
|
|
first_cascade_far_bound: 0.9 * radius,
|
|
maximum_distance: 2.8 * radius,
|
|
..default()
|
|
}
|
|
.into(),
|
|
..default()
|
|
});
|
|
|
|
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!(" - return: change animation");
|
|
}
|
|
|
|
// Once the scene is loaded, start the animation
|
|
fn setup_scene_once_loaded(
|
|
animations: Res<Animations>,
|
|
foxes: Res<Foxes>,
|
|
mut commands: Commands,
|
|
mut player: Query<(Entity, &mut AnimationPlayer)>,
|
|
mut done: Local<bool>,
|
|
) {
|
|
if !*done && player.iter().len() == foxes.count {
|
|
for (entity, mut player) in &mut player {
|
|
commands
|
|
.entity(entity)
|
|
.insert(animations.graph.clone())
|
|
.insert(AnimationTransitions::new());
|
|
|
|
let playing_animation = player.play(animations.node_indices[0]).repeat();
|
|
if !foxes.sync {
|
|
playing_animation.seek_to(entity.index() as f32 / 10.0);
|
|
}
|
|
}
|
|
*done = true;
|
|
}
|
|
}
|
|
|
|
fn update_fox_rings(
|
|
time: Res<Time>,
|
|
foxes: Res<Foxes>,
|
|
mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
|
|
) {
|
|
if !foxes.moving {
|
|
return;
|
|
}
|
|
|
|
let dt = time.delta_seconds();
|
|
for (ring, rotation_direction, mut transform) in &mut rings {
|
|
let angular_velocity = foxes.speed / ring.radius;
|
|
transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
|
|
}
|
|
}
|
|
|
|
fn keyboard_animation_control(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
|
animations: Res<Animations>,
|
|
mut current_animation: Local<usize>,
|
|
mut foxes: ResMut<Foxes>,
|
|
) {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
foxes.moving = !foxes.moving;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
|
foxes.speed *= 1.25;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
|
foxes.speed *= 0.8;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
|
*current_animation = (*current_animation + 1) % animations.node_indices.len();
|
|
}
|
|
|
|
for (mut player, mut transitions) in &mut animation_player {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
if player.all_paused() {
|
|
player.resume_all();
|
|
} else {
|
|
player.pause_all();
|
|
}
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
|
player.adjust_speeds(1.25);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
|
player.adjust_speeds(0.8);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
|
player.seek_all_by(-0.1);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
|
player.seek_all_by(0.1);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
|
transitions
|
|
.play(
|
|
&mut player,
|
|
animations.node_indices[*current_animation],
|
|
Duration::from_millis(250),
|
|
)
|
|
.repeat();
|
|
}
|
|
}
|
|
}
|