mirror of
https://github.com/bevyengine/bevy
synced 2024-12-02 09:29:12 +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>
184 lines
6.6 KiB
Rust
184 lines
6.6 KiB
Rust
//! Create and play an animation defined by code that operates on the [`Transform`] component.
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::animation::{AnimationTarget, AnimationTargetId};
|
|
use bevy::prelude::*;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.insert_resource(AmbientLight {
|
|
color: Color::WHITE,
|
|
brightness: 150.0,
|
|
})
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut animations: ResMut<Assets<AnimationClip>>,
|
|
mut graphs: ResMut<Assets<AnimationGraph>>,
|
|
) {
|
|
// Camera
|
|
commands.spawn(Camera3dBundle {
|
|
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
..default()
|
|
});
|
|
|
|
// Light
|
|
commands.spawn(PointLightBundle {
|
|
point_light: PointLight {
|
|
intensity: 500_000.0,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(0.0, 2.5, 0.0),
|
|
..default()
|
|
});
|
|
|
|
// Let's use the `Name` component to target entities. We can use anything we
|
|
// like, but names are convenient.
|
|
let planet = Name::new("planet");
|
|
let orbit_controller = Name::new("orbit_controller");
|
|
let satellite = Name::new("satellite");
|
|
|
|
// Creating the animation
|
|
let mut animation = AnimationClip::default();
|
|
// A curve can modify a single part of a transform, here the translation
|
|
let planet_animation_target_id = AnimationTargetId::from_name(&planet);
|
|
animation.add_curve_to_target(
|
|
planet_animation_target_id,
|
|
VariableCurve {
|
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
|
keyframes: Keyframes::Translation(vec![
|
|
Vec3::new(1.0, 0.0, 1.0),
|
|
Vec3::new(-1.0, 0.0, 1.0),
|
|
Vec3::new(-1.0, 0.0, -1.0),
|
|
Vec3::new(1.0, 0.0, -1.0),
|
|
// in case seamless looping is wanted, the last keyframe should
|
|
// be the same as the first one
|
|
Vec3::new(1.0, 0.0, 1.0),
|
|
]),
|
|
interpolation: Interpolation::Linear,
|
|
},
|
|
);
|
|
// Or it can modify the rotation of the transform.
|
|
// To find the entity to modify, the hierarchy will be traversed looking for
|
|
// an entity with the right name at each level
|
|
let orbit_controller_animation_target_id =
|
|
AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
|
|
animation.add_curve_to_target(
|
|
orbit_controller_animation_target_id,
|
|
VariableCurve {
|
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
|
keyframes: Keyframes::Rotation(vec![
|
|
Quat::IDENTITY,
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
|
Quat::IDENTITY,
|
|
]),
|
|
interpolation: Interpolation::Linear,
|
|
},
|
|
);
|
|
// If a curve in an animation is shorter than the other, it will not repeat
|
|
// until all other curves are finished. In that case, another animation should
|
|
// be created for each part that would have a different duration / period
|
|
let satellite_animation_target_id = AnimationTargetId::from_names(
|
|
[planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
|
|
);
|
|
animation.add_curve_to_target(
|
|
satellite_animation_target_id,
|
|
VariableCurve {
|
|
keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
|
|
keyframes: Keyframes::Scale(vec![
|
|
Vec3::splat(0.8),
|
|
Vec3::splat(1.2),
|
|
Vec3::splat(0.8),
|
|
Vec3::splat(1.2),
|
|
Vec3::splat(0.8),
|
|
Vec3::splat(1.2),
|
|
Vec3::splat(0.8),
|
|
Vec3::splat(1.2),
|
|
Vec3::splat(0.8),
|
|
]),
|
|
interpolation: Interpolation::Linear,
|
|
},
|
|
);
|
|
// There can be more than one curve targeting the same entity path
|
|
animation.add_curve_to_target(
|
|
AnimationTargetId::from_names(
|
|
[planet.clone(), orbit_controller.clone(), satellite.clone()].iter(),
|
|
),
|
|
VariableCurve {
|
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
|
keyframes: Keyframes::Rotation(vec![
|
|
Quat::IDENTITY,
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2.),
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
|
|
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
|
|
Quat::IDENTITY,
|
|
]),
|
|
interpolation: Interpolation::Linear,
|
|
},
|
|
);
|
|
|
|
// Create the animation graph
|
|
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
|
|
|
|
// Create the animation player, and set it to repeat
|
|
let mut player = AnimationPlayer::default();
|
|
player.play(animation_index).repeat();
|
|
|
|
// Create the scene that will be animated
|
|
// First entity is the planet
|
|
let planet_entity = commands
|
|
.spawn((
|
|
PbrBundle {
|
|
mesh: meshes.add(Sphere::default()),
|
|
material: materials.add(Color::srgb(0.8, 0.7, 0.6)),
|
|
..default()
|
|
},
|
|
// Add the animation graph and player
|
|
planet,
|
|
graphs.add(graph),
|
|
player,
|
|
))
|
|
.id();
|
|
commands
|
|
.entity(planet_entity)
|
|
.insert(AnimationTarget {
|
|
id: planet_animation_target_id,
|
|
player: planet_entity,
|
|
})
|
|
.with_children(|p| {
|
|
// This entity is just used for animation, but doesn't display anything
|
|
p.spawn((
|
|
SpatialBundle::INHERITED_IDENTITY,
|
|
orbit_controller,
|
|
AnimationTarget {
|
|
id: orbit_controller_animation_target_id,
|
|
player: planet_entity,
|
|
},
|
|
))
|
|
.with_children(|p| {
|
|
// The satellite, placed at a distance of the planet
|
|
p.spawn((
|
|
PbrBundle {
|
|
transform: Transform::from_xyz(1.5, 0.0, 0.0),
|
|
mesh: meshes.add(Cuboid::new(0.5, 0.5, 0.5)),
|
|
material: materials.add(Color::srgb(0.3, 0.9, 0.3)),
|
|
..default()
|
|
},
|
|
AnimationTarget {
|
|
id: satellite_animation_target_id,
|
|
player: planet_entity,
|
|
},
|
|
satellite,
|
|
));
|
|
});
|
|
});
|
|
}
|