animation player (#4375)

# Objective

- Add a basic animation player
  - Single track
  - Not generic, can only animate `Transform`s
  - With plenty of possible optimisations available
  - Close-ish to https://github.com/bevyengine/rfcs/pull/49
- https://discord.com/channels/691052431525675048/774027865020039209/958820063148929064

## Solution

- Can play animations
  - looping or not
- Can pause animations
- Can seek in animation
- Can alter speed of animation
- I also removed the previous gltf animation example

https://user-images.githubusercontent.com/8672791/161051887-e79283f0-9803-448a-93d0-5f7a62acb02d.mp4
This commit is contained in:
François 2022-04-02 22:36:02 +00:00
parent 0ed08d6a15
commit 449a1d223c
18 changed files with 585 additions and 1417 deletions

View file

@ -21,5 +21,6 @@
* Ground tile from [Kenney's Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit) (CC0 1.0 Universal)
* Game icons from [Kenney's Game Icons](https://www.kenney.nl/assets/game-icons) (CC0 1.0 Universal)
* Space ships from [Kenny's Simple Space Kit](https://www.kenney.nl/assets/simple-space) (CC0 1.0 Universal)
* glTF animated triangle from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedTriangle) (CC0 1.0 Universal)
* glTF box animated from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoxAnimated) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - by [Cesium](https://cesium.com))
* glTF animated fox from [glTF Sample Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox)
* Low poly fox [by PixelMannen](https://opengameart.org/content/fox-and-shiba) (CC0 1.0 Universal)
* Rigging and animation [by @tomkranis on Sketchfab](https://sketchfab.com/models/371dea88d7e04a76af5763f2a36866bc) ([CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/))

View file

@ -17,6 +17,7 @@ members = ["crates/*", "examples/ios", "tools/ci", "errors"]
[features]
default = [
"animation",
"bevy_audio",
"bevy_gilrs",
"bevy_winit",
@ -43,6 +44,7 @@ render = [
]
# Optional bevy crates
bevy_animation = ["bevy_internal/bevy_animation"]
bevy_audio = ["bevy_internal/bevy_audio"]
bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"]
@ -98,6 +100,9 @@ bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]
# Enable the "debug asset server" for hot reloading internal assets
debug_asset_server = ["bevy_internal/debug_asset_server"]
# Enable animation support, and glTF animation loading
animation = ["bevy_internal/animation"]
[dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.7.0-dev", default-features = false, optional = true }
bevy_internal = { path = "crates/bevy_internal", version = "0.7.0-dev", default-features = false }
@ -183,10 +188,6 @@ path = "examples/3d/lighting.rs"
name = "load_gltf"
path = "examples/3d/load_gltf.rs"
[[example]]
name = "manual_gltf_animation_player"
path = "examples/3d/manual_gltf_animation_player.rs"
[[example]]
name = "many_cubes"
path = "examples/3d/many_cubes.rs"
@ -236,6 +237,10 @@ name = "wireframe"
path = "examples/3d/wireframe.rs"
# Animation
[[example]]
name = "animated_fox"
path = "examples/animation/animated_fox.rs"
[[example]]
name = "custom_skinned_mesh"
path = "examples/animation/custom_skinned_mesh.rs"

View file

@ -1,118 +0,0 @@
{
"scene" : 0,
"scenes" : [
{
"nodes" : [ 0 ]
}
],
"nodes" : [
{
"mesh" : 0,
"rotation" : [ 0.0, 0.0, 0.0, 1.0 ]
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1
},
"indices" : 0
} ]
}
],
"animations": [
{
"samplers" : [
{
"input" : 2,
"interpolation" : "LINEAR",
"output" : 3
}
],
"channels" : [ {
"sampler" : 0,
"target" : {
"node" : 0,
"path" : "rotation"
}
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
"byteLength" : 44
},
{
"uri" : "data:application/octet-stream;base64,AAAAAAAAgD4AAAA/AABAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAPT9ND/0/TS/AAAAAAAAAAAAAAAAAACAPw==",
"byteLength" : 100
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 6,
"target" : 34963
},
{
"buffer" : 0,
"byteOffset" : 8,
"byteLength" : 36,
"target" : 34962
},
{
"buffer" : 1,
"byteOffset" : 0,
"byteLength" : 100
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
},
{
"bufferView" : 2,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 5,
"type" : "SCALAR",
"max" : [ 1.0 ],
"min" : [ 0.0 ]
},
{
"bufferView" : 2,
"byteOffset" : 20,
"componentType" : 5126,
"count" : 5,
"type" : "VEC4",
"max" : [ 0.0, 0.0, 1.0, 1.0 ],
"min" : [ 0.0, 0.0, 0.0, -0.707 ]
}
],
"asset" : {
"version" : "2.0"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
[package]
name = "bevy_animation"
version = "0.7.0-dev"
edition = "2021"
description = "Provides animation functionality for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.7.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.7.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.7.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.7.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.7.0-dev" }

View file

@ -0,0 +1,284 @@
//! Animation for the game engine Bevy
#![warn(missing_docs)]
use std::ops::Deref;
use bevy_app::{App, CoreStage, Plugin};
use bevy_asset::{AddAsset, Assets, Handle};
use bevy_core::{Name, Time};
use bevy_ecs::{
change_detection::DetectChanges,
entity::Entity,
prelude::Component,
reflect::ReflectComponent,
schedule::ParallelSystemDescriptorCoercion,
system::{Query, Res},
};
use bevy_hierarchy::{Children, HierarchySystem};
use bevy_math::{Quat, Vec3};
use bevy_reflect::{Reflect, TypeUuid};
use bevy_transform::{prelude::Transform, TransformSystem};
use bevy_utils::{tracing::warn, HashMap};
#[allow(missing_docs)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{
AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Keyframes, VariableCurve,
};
}
/// List of keyframes for one of the attribute of a [`Transform`].
#[derive(Clone, Debug)]
pub enum Keyframes {
/// Keyframes for rotation.
Rotation(Vec<Quat>),
/// Keyframes for translation.
Translation(Vec<Vec3>),
/// Keyframes for scale.
Scale(Vec<Vec3>),
}
/// Describes how an attribute of a [`Transform`] should be animated.
///
/// `keyframe_timestamps` and `keyframes` should have the same length.
#[derive(Clone, Debug)]
pub struct VariableCurve {
/// Timestamp for each of the keyframes.
pub keyframe_timestamps: Vec<f32>,
/// List of the keyframes.
pub keyframes: Keyframes,
}
/// Path to an entity, with [`Name`]s. Each entity in a path must have a name.
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
pub struct EntityPath {
/// Parts of the path
pub parts: Vec<Name>,
}
/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply.
#[derive(Clone, TypeUuid, Debug, Default)]
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
pub struct AnimationClip {
curves: HashMap<EntityPath, Vec<VariableCurve>>,
duration: f32,
}
impl AnimationClip {
#[inline]
/// Hashmap of the [`VariableCurve`]s per [`EntityPath`].
pub fn curves(&self) -> &HashMap<EntityPath, Vec<VariableCurve>> {
&self.curves
}
/// Add a [`VariableCurve`] to an [`EntityPath`].
pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) {
// Update the duration of the animation by this curve duration if it's longer
self.duration = self
.duration
.max(*curve.keyframe_timestamps.last().unwrap_or(&0.0));
self.curves.entry(path).or_default().push(curve);
}
}
/// Animation controls
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct AnimationPlayer {
paused: bool,
repeat: bool,
speed: f32,
elapsed: f32,
animation_clip: Handle<AnimationClip>,
}
impl Default for AnimationPlayer {
fn default() -> Self {
Self {
paused: false,
repeat: false,
speed: 1.0,
elapsed: 0.0,
animation_clip: Default::default(),
}
}
}
impl AnimationPlayer {
/// Start playing an animation, resetting state of the player
pub fn play(&mut self, handle: Handle<AnimationClip>) -> &mut Self {
*self = Self {
animation_clip: handle,
..Default::default()
};
self
}
/// Set the animation to repeat
pub fn repeat(&mut self) -> &mut Self {
self.repeat = true;
self
}
/// Stop the animation from repeating
pub fn stop_repeating(&mut self) -> &mut Self {
self.repeat = false;
self
}
/// Pause the animation
pub fn pause(&mut self) {
self.paused = true;
}
/// Unpause the animation
pub fn resume(&mut self) {
self.paused = false;
}
/// Is the animation paused
pub fn is_paused(&self) -> bool {
self.paused
}
/// Speed of the animation playback
pub fn speed(&self) -> f32 {
self.speed
}
/// Set the speed of the animation playback
pub fn set_speed(&mut self, speed: f32) -> &mut Self {
self.speed = speed;
self
}
/// Time elapsed playing the animation
pub fn elapsed(&self) -> f32 {
self.elapsed
}
/// Seek to a specific time in the animation
pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self {
self.elapsed = elapsed;
self
}
}
/// System that will play all animations, using any entity with a [`AnimationPlayer`]
/// and a [`Handle<AnimationClip>`] as an animation root
pub fn animation_player(
time: Res<Time>,
animations: Res<Assets<AnimationClip>>,
mut animation_players: Query<(Entity, &mut AnimationPlayer)>,
names: Query<&Name>,
mut transforms: Query<&mut Transform>,
children: Query<&Children>,
) {
for (entity, mut player) in animation_players.iter_mut() {
if let Some(animation_clip) = animations.get(&player.animation_clip) {
// Continue if paused unless the `AnimationPlayer` was changed
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
if player.paused && !player.is_changed() {
continue;
}
if !player.paused {
player.elapsed += time.delta_seconds() * player.speed;
}
let mut elapsed = player.elapsed;
if player.repeat {
elapsed %= animation_clip.duration;
}
if elapsed < 0.0 {
elapsed += animation_clip.duration;
}
'entity: for (path, curves) in &animation_clip.curves {
// PERF: finding the target entity can be optimised
let mut current_entity = entity;
// Ignore the first name, it is the root node which we already have
for part in path.parts.iter().skip(1) {
let mut found = false;
if let Ok(children) = children.get(current_entity) {
for child in children.deref() {
if let Ok(name) = names.get(*child) {
if name == part {
// Found a children with the right name, continue to the next part
current_entity = *child;
found = true;
break;
}
}
}
}
if !found {
warn!("Entity not found for path {:?} on part {:?}", path, part);
continue 'entity;
}
}
if let Ok(mut transform) = transforms.get_mut(current_entity) {
for curve in curves {
// Find the current keyframe
// PERF: finding the current keyframe can be optimised
let step_start = match curve
.keyframe_timestamps
.binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap())
{
Ok(i) => i,
Err(0) => continue, // this curve isn't started yet
Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
Err(i) => i - 1,
};
let ts_start = curve.keyframe_timestamps[step_start];
let ts_end = curve.keyframe_timestamps[step_start + 1];
let lerp = (elapsed - ts_start) / (ts_end - ts_start);
// Apply the keyframe
match &curve.keyframes {
Keyframes::Rotation(keyframes) => {
let rot_start = keyframes[step_start];
let mut rot_end = keyframes[step_start + 1];
// Choose the smallest angle for the rotation
if rot_end.dot(rot_start) < 0.0 {
rot_end = -rot_end;
}
// Rotations are using a spherical linear interpolation
transform.rotation = Quat::from_array(rot_start.normalize().into())
.slerp(Quat::from_array(rot_end.normalize().into()), lerp);
}
Keyframes::Translation(keyframes) => {
let translation_start = keyframes[step_start];
let translation_end = keyframes[step_start + 1];
let result = translation_start.lerp(translation_end, lerp);
transform.translation = result;
}
Keyframes::Scale(keyframes) => {
let scale_start = keyframes[step_start];
let scale_end = keyframes[step_start + 1];
let result = scale_start.lerp(scale_end, lerp);
transform.scale = result;
}
}
}
}
}
}
}
}
/// Adds animation support to an app
#[derive(Default)]
pub struct AnimationPlugin {}
impl Plugin for AnimationPlugin {
fn build(&self, app: &mut App) {
app.add_asset::<AnimationClip>()
.register_type::<AnimationPlayer>()
.add_system_to_stage(
CoreStage::PostUpdate,
animation_player
.before(TransformSystem::TransformPropagate)
.after(HierarchySystem::ParentUpdate),
);
}
}

View file

@ -10,19 +10,20 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_animation = { path = "../bevy_animation", version = "0.7.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.7.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.7.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.7.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.7.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.7.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
bevy_pbr = { path = "../bevy_pbr", version = "0.7.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.7.0-dev", features = ["bevy"] }
bevy_render = { path = "../bevy_render", version = "0.7.0-dev" }
bevy_scene = { path = "../bevy_scene", version = "0.7.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.7.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.7.0-dev" }
bevy_scene = { path = "../bevy_scene", version = "0.7.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.7.0-dev" }
# other
gltf = { version = "1.0.0", default-features = false, features = [

View file

@ -1,5 +1,5 @@
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_math::{Quat, Vec3};
#[cfg(feature = "bevy_animation")]
use bevy_animation::AnimationClip;
use bevy_utils::HashMap;
mod loader;
@ -8,7 +8,7 @@ pub use loader::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Handle};
use bevy_pbr::StandardMaterial;
use bevy_reflect::{Reflect, TypeUuid};
use bevy_reflect::TypeUuid;
use bevy_render::mesh::Mesh;
use bevy_scene::Scene;
@ -22,9 +22,7 @@ impl Plugin for GltfPlugin {
.add_asset::<Gltf>()
.add_asset::<GltfNode>()
.add_asset::<GltfPrimitive>()
.add_asset::<GltfMesh>()
.add_asset::<GltfAnimation>()
.register_type::<GltfAnimatedNode>();
.add_asset::<GltfMesh>();
}
}
@ -41,8 +39,10 @@ pub struct Gltf {
pub nodes: Vec<Handle<GltfNode>>,
pub named_nodes: HashMap<String, Handle<GltfNode>>,
pub default_scene: Option<Handle<Scene>>,
pub animations: Vec<Handle<GltfAnimation>>,
pub named_animations: HashMap<String, Handle<GltfAnimation>>,
#[cfg(feature = "bevy_animation")]
pub animations: Vec<Handle<AnimationClip>>,
#[cfg(feature = "bevy_animation")]
pub named_animations: HashMap<String, Handle<AnimationClip>>,
}
/// A glTF node with all of its child nodes, its [`GltfMesh`] and
@ -69,52 +69,3 @@ pub struct GltfPrimitive {
pub mesh: Handle<Mesh>,
pub material: Option<Handle<StandardMaterial>>,
}
/// Interpolation method for an animation. Part of a [`GltfNodeAnimation`].
#[derive(Clone, Debug)]
pub enum GltfAnimationInterpolation {
Linear,
Step,
CubicSpline,
}
/// How a property of a glTF node should be animated. The property and its value can be found
/// through the [`GltfNodeAnimationKeyframes`] attribute.
#[derive(Clone, Debug)]
pub struct GltfNodeAnimation {
pub keyframe_timestamps: Vec<f32>,
pub keyframes: GltfNodeAnimationKeyframes,
pub interpolation: GltfAnimationInterpolation,
}
/// A glTF animation, listing how each node (by its index) that is part of it should be animated.
#[derive(Default, Clone, TypeUuid, Debug)]
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
pub struct GltfAnimation {
pub node_animations: HashMap<usize, Vec<GltfNodeAnimation>>,
}
/// Key frames of an animation.
#[derive(Clone, Debug)]
pub enum GltfNodeAnimationKeyframes {
Rotation(Vec<Quat>),
Translation(Vec<Vec3>),
Scale(Vec<Vec3>),
}
impl Default for GltfNodeAnimation {
fn default() -> Self {
Self {
keyframe_timestamps: Default::default(),
keyframes: GltfNodeAnimationKeyframes::Translation(Default::default()),
interpolation: GltfAnimationInterpolation::Linear,
}
}
}
/// A glTF node that is part of an animation, with its index.
#[derive(Component, Debug, Clone, Reflect, Default)]
#[reflect(Component)]
pub struct GltfAnimatedNode {
pub index: usize,
}

View file

@ -1,4 +1,6 @@
use anyhow::Result;
#[cfg(feature = "bevy_animation")]
use bevy_animation::{AnimationClip, AnimationPlayer, EntityPath, Keyframes, VariableCurve};
use bevy_asset::{
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
};
@ -33,15 +35,12 @@ use bevy_utils::{HashMap, HashSet};
use gltf::{
mesh::Mode,
texture::{MagFilter, MinFilter, WrappingMode},
Material, Primitive,
Material, Node, Primitive,
};
use std::{collections::VecDeque, path::Path};
use thiserror::Error;
use crate::{
Gltf, GltfAnimatedNode, GltfAnimation, GltfAnimationInterpolation, GltfNode, GltfNodeAnimation,
GltfNodeAnimationKeyframes,
};
use crate::{Gltf, GltfNode};
/// An error that occurs when loading a glTF file.
#[derive(Error, Debug)]
@ -129,18 +128,30 @@ async fn load_gltf<'a, 'b>(
}
}
#[cfg(feature = "bevy_animation")]
let paths = {
let mut paths = HashMap::<usize, Vec<Name>>::new();
for scene in gltf.scenes() {
for node in scene.nodes() {
paths_recur(node, &[], &mut paths);
}
}
paths
};
#[cfg(feature = "bevy_animation")]
let (animations, named_animations) = {
let mut animations = vec![];
let mut named_animations = HashMap::default();
let mut animated_nodes = HashSet::default();
for animation in gltf.animations() {
let mut gltf_animation = GltfAnimation::default();
let mut animation_clip = AnimationClip::default();
for channel in animation.channels() {
let interpolation = match channel.sampler().interpolation() {
gltf::animation::Interpolation::Linear => GltfAnimationInterpolation::Linear,
gltf::animation::Interpolation::Step => GltfAnimationInterpolation::Step,
gltf::animation::Interpolation::CubicSpline => {
GltfAnimationInterpolation::CubicSpline
}
match channel.sampler().interpolation() {
gltf::animation::Interpolation::Linear => (),
other => warn!(
"Animation interpolation {:?} is not supported, will use linear",
other
),
};
let node = channel.target().node();
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
@ -148,27 +159,25 @@ async fn load_gltf<'a, 'b>(
match inputs {
gltf::accessor::Iter::Standard(times) => times.collect(),
gltf::accessor::Iter::Sparse(_) => {
warn!("sparse accessor not supported for animation sampler input");
warn!("Sparse accessor not supported for animation sampler input");
continue;
}
}
} else {
warn!("animations without a sampler input are not supported");
warn!("Animations without a sampler input are not supported");
return Err(GltfError::MissingAnimationSampler(animation.index()));
};
let keyframes = if let Some(outputs) = reader.read_outputs() {
match outputs {
gltf::animation::util::ReadOutputs::Translations(tr) => {
GltfNodeAnimationKeyframes::Translation(tr.map(Vec3::from).collect())
Keyframes::Translation(tr.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
GltfNodeAnimationKeyframes::Rotation(
rots.into_f32().map(Quat::from_array).collect(),
)
Keyframes::Rotation(rots.into_f32().map(Quat::from_array).collect())
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
GltfNodeAnimationKeyframes::Scale(scale.map(Vec3::from).collect())
Keyframes::Scale(scale.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
@ -176,30 +185,38 @@ async fn load_gltf<'a, 'b>(
}
}
} else {
warn!("animations without a sampler output are not supported");
warn!("Animations without a sampler output are not supported");
return Err(GltfError::MissingAnimationSampler(animation.index()));
};
gltf_animation
.node_animations
.entry(node.index())
.or_default()
.push(GltfNodeAnimation {
if let Some(path) = paths.get(&node.index()) {
animation_clip.add_curve_to_path(
EntityPath {
parts: path.clone(),
},
VariableCurve {
keyframe_timestamps,
keyframes,
interpolation,
});
animated_nodes.insert(node.index());
},
);
} else {
warn!(
"Animation ignored for node {}: part of its hierarchy is missing a name",
node.index()
);
}
}
let handle = load_context.set_labeled_asset(
&format!("Animation{}", animation.index()),
LoadedAsset::new(gltf_animation),
LoadedAsset::new(animation_clip),
);
if let Some(name) = animation.name() {
named_animations.insert(name.to_string(), handle.clone());
}
animations.push(handle);
}
(animations, named_animations)
};
let mut meshes = vec![];
let mut named_meshes = HashMap::default();
@ -436,7 +453,6 @@ async fn load_gltf<'a, 'b>(
parent,
load_context,
&buffer_data,
&animated_nodes,
&mut node_index_to_entity_map,
&mut entity_to_skin_index_map,
);
@ -450,6 +466,13 @@ async fn load_gltf<'a, 'b>(
return Err(err);
}
#[cfg(feature = "bevy_animation")]
if !animations.is_empty() {
world
.entity_mut(*node_index_to_entity_map.get(&0).unwrap())
.insert(AnimationPlayer::default());
}
for (&entity, &skin_index) in &entity_to_skin_index_map {
let mut entity = world.entity_mut(entity);
let skin = gltf.skins().nth(skin_index).unwrap();
@ -486,13 +509,26 @@ async fn load_gltf<'a, 'b>(
named_materials,
nodes,
named_nodes,
#[cfg(feature = "bevy_animation")]
animations,
#[cfg(feature = "bevy_animation")]
named_animations,
}));
Ok(())
}
fn paths_recur(node: Node, current_path: &[Name], paths: &mut HashMap<usize, Vec<Name>>) {
if let Some(name) = node.name() {
let mut path = current_path.to_owned();
path.push(Name::new(name.to_string()));
for child in node.children() {
paths_recur(child, &path, paths);
}
paths.insert(node.index(), path);
}
}
/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
async fn load_texture<'a>(
gltf_texture: gltf::Texture<'a>,
@ -633,7 +669,6 @@ fn load_node(
world_builder: &mut WorldChildBuilder,
load_context: &mut LoadContext,
buffer_data: &[Vec<u8>],
animated_nodes: &HashSet<usize>,
node_index_to_entity_map: &mut HashMap<usize, Entity>,
entity_to_skin_index_map: &mut HashMap<Entity, usize>,
) -> Result<(), GltfError> {
@ -643,12 +678,6 @@ fn load_node(
Mat4::from_cols_array_2d(&transform.matrix()),
)));
if animated_nodes.contains(&gltf_node.index()) {
node.insert(GltfAnimatedNode {
index: gltf_node.index(),
});
}
if let Some(name) = gltf_node.name() {
node.insert(Name::new(name.to_string()));
}
@ -798,7 +827,6 @@ fn load_node(
parent,
load_context,
buffer_data,
animated_nodes,
node_index_to_entity_map,
entity_to_skin_index_map,
) {

View file

@ -53,6 +53,9 @@ webgl = ["bevy_pbr/webgl", "bevy_render/webgl"]
# enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"]
# Enable animation support, and glTF animation loading
animation = ["bevy_animation", "bevy_gltf/bevy_animation"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.7.0-dev" }
@ -72,6 +75,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.7.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.7.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.7.0-dev" }
# bevy (optional)
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.7.0-dev" }
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.7.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.7.0-dev" }
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.7.0-dev" }

View file

@ -68,6 +68,9 @@ impl PluginGroup for DefaultPlugins {
#[cfg(feature = "bevy_gilrs")]
group.add(bevy_gilrs::GilrsPlugin::default());
#[cfg(feature = "bevy_animation")]
group.add(bevy_animation::AnimationPlugin::default());
}
}

View file

@ -85,6 +85,12 @@ pub mod window {
pub use bevy_window::*;
}
#[cfg(feature = "bevy_animation")]
pub mod animation {
//! Provides types and plugins for animations.
pub use bevy_animation::*;
}
#[cfg(feature = "bevy_audio")]
pub mod audio {
//! Provides types and plugins for audio playback.

View file

@ -11,6 +11,10 @@ pub use bevy_derive::{bevy_main, Deref, DerefMut};
#[cfg(feature = "bevy_audio")]
pub use crate::audio::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_animation")]
pub use crate::animation::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_core_pipeline")]
pub use crate::core_pipeline::prelude::*;

View file

@ -1,230 +0,0 @@
use bevy::{
core::FixedTimestep,
gltf::*,
math::{const_quat, const_vec3},
prelude::*,
scene::InstanceId,
};
/// This illustrates loading animations from GLTF files and manually playing them.
/// Note that a higher level animation api is in the works. This exists to illustrate how to
/// read and build a custom GLTF animator, not to illustrate a final Bevy animation api.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.0,
})
.add_startup_system(setup)
.add_system_set(
SystemSet::new()
.with_run_criteria(FixedTimestep::step(10.0))
.with_system(switch_scene),
)
.add_system(setup_scene_once_loaded)
.add_system(gltf_animation_driver)
.run();
}
struct Example {
model_name: &'static str,
camera_transform: Transform,
speed: f32,
}
impl Example {
const fn new(model_name: &'static str, camera_transform: Transform, speed: f32) -> Self {
Self {
model_name,
camera_transform,
speed,
}
}
}
// const ANIMATIONS: [(&str, Transform, f32); 3] = [
const ANIMATIONS: [Example; 3] = [
// https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AnimatedTriangle
Example::new(
"models/animated/AnimatedTriangle.gltf",
Transform {
translation: const_vec3!([0.0, 0.0, 3.0]),
rotation: const_quat!([0.0, 0.0, 0.0, 1.0]),
scale: const_vec3!([1.0; 3]),
},
0.12,
),
// https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoxAnimated
Example::new(
"models/animated/BoxAnimated.gltf",
Transform {
translation: const_vec3!([4.0, 2.0, 4.0]),
rotation: const_quat!([-0.08, 0.38, 0.03, 0.92]),
scale: const_vec3!([1.0; 3]),
},
0.4,
),
Example::new(
"models/animated/animations.gltf",
Transform {
translation: const_vec3!([-10.0, 5.0, -3.0]),
rotation: const_quat!([0.16, 0.69, 0.16, -0.69]),
scale: const_vec3!([1.0; 3]),
},
2.5,
),
];
struct CurrentScene {
instance_id: InstanceId,
animation: Handle<GltfAnimation>,
speed: f32,
}
struct CurrentAnimation {
start_time: f64,
animation: GltfAnimation,
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut scene_spawner: ResMut<SceneSpawner>,
) {
// Insert a resource with the current scene information
commands.insert_resource(CurrentScene {
// Its instance id, to be able to check that it's loaded
instance_id: scene_spawner
.spawn(asset_server.load(&format!("{}#Scene0", ANIMATIONS[0].model_name))),
// The handle to the first animation
animation: asset_server.load(&format!("{}#Animation0", ANIMATIONS[0].model_name)),
// The animation speed modifier
speed: ANIMATIONS[0].speed,
});
// Add a camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: ANIMATIONS[0].camera_transform,
..Default::default()
});
// Add a directional light
commands.spawn_bundle(DirectionalLightBundle::default());
}
// Switch the scene to the next one every 10 seconds
fn switch_scene(
mut commands: Commands,
scene_root: Query<Entity, (Without<Camera>, Without<DirectionalLight>, Without<Parent>)>,
mut camera: Query<&mut Transform, With<Camera>>,
mut current: Local<usize>,
mut current_scene: ResMut<CurrentScene>,
asset_server: Res<AssetServer>,
mut scene_spawner: ResMut<SceneSpawner>,
) {
*current = (*current + 1) % ANIMATIONS.len();
// Despawn the existing scene, then start loading the next one
commands.entity(scene_root.single()).despawn_recursive();
current_scene.instance_id = scene_spawner
.spawn(asset_server.load(&format!("{}#Scene0", ANIMATIONS[*current].model_name)));
current_scene.animation =
asset_server.load(&format!("{}#Animation0", ANIMATIONS[*current].model_name));
current_scene.speed = ANIMATIONS[*current].speed;
// Update the camera position
*camera.single_mut() = ANIMATIONS[*current].camera_transform;
// Reset the current animation
commands.remove_resource::<CurrentAnimation>();
}
// Setup the scene for animation once it is loaded, by adding the animation to a resource with
// the start time.
fn setup_scene_once_loaded(
mut commands: Commands,
scene_spawner: Res<SceneSpawner>,
current_scene: Res<CurrentScene>,
time: Res<Time>,
mut done: Local<bool>,
animations: Res<Assets<GltfAnimation>>,
) {
// If the current scene resource has changed, start waiting for it to be loaded
if current_scene.is_changed() {
*done = false;
}
// Once the scene and the animation are loaded, start the animation
if !*done && scene_spawner.instance_is_ready(current_scene.instance_id) {
if let Some(animation) = animations.get(&current_scene.animation) {
*done = true;
commands.insert_resource(CurrentAnimation {
start_time: time.seconds_since_startup(),
animation: animation.clone(),
})
}
}
}
// This animation driver is not made to work in the general case. It will work with only one
// animation per scene, and will ignore the specified interpolation method to only do linear.
fn gltf_animation_driver(
mut animated: Query<(&mut Transform, &GltfAnimatedNode)>,
current_animation: Option<Res<CurrentAnimation>>,
current_scene: Res<CurrentScene>,
time: Res<Time>,
) {
if let Some(current_animation) = current_animation {
let elapsed = (time.seconds_since_startup() - current_animation.start_time) as f32
* current_scene.speed;
for (mut transform, node) in animated.iter_mut() {
let node_animations = current_animation
.animation
.node_animations
.get(&node.index)
.unwrap();
for node_animation in node_animations.iter() {
let mut keyframe_timestamps = node_animation.keyframe_timestamps.iter().enumerate();
let mut step_start = keyframe_timestamps.next().unwrap();
if elapsed < *step_start.1 {
continue;
}
for next in keyframe_timestamps {
if *next.1 > elapsed {
break;
}
step_start = next;
}
if step_start.0 == node_animation.keyframe_timestamps.len() - 1 {
continue;
}
let step_end = node_animation.keyframe_timestamps[step_start.0 + 1];
let lerp = (elapsed - *step_start.1) / (step_end - step_start.1);
match &node_animation.keyframes {
GltfNodeAnimationKeyframes::Rotation(keyframes) => {
let rot_start = keyframes[step_start.0];
let mut rot_end = keyframes[step_start.0 + 1];
if rot_end.dot(rot_start) < 0.0 {
rot_end = -rot_end;
}
let result = Quat::from_array(rot_start.normalize().into())
.slerp(Quat::from_array(rot_end.normalize().into()), lerp);
transform.rotation = result;
}
GltfNodeAnimationKeyframes::Translation(keyframes) => {
let translation_start = keyframes[step_start.0];
let translation_end = keyframes[step_start.0 + 1];
let result = translation_start.lerp(translation_end, lerp);
transform.translation = result;
}
GltfNodeAnimationKeyframes::Scale(keyframes) => {
let scale_start = keyframes[step_start.0];
let scale_end = keyframes[step_start.0 + 1];
let result = scale_start.lerp(scale_end, lerp);
transform.scale = result;
}
}
}
}
}
}

View file

@ -105,7 +105,6 @@ Example | File | Description
`3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
`lighting` | [`3d/lighting.rs`](./3d/lighting.rs) | Illustrates various lighting options in a simple scene
`load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene
`manual_gltf_animation_player` | [`3d/manual_gltf_animation_player.rs`](./3d/manual_gltf_animation_player.rs) | Loads and manually renders a gltf file with animations
`many_cubes` | [`3d/many_cubes.rs`](./3d/many_cubes.rs) | Simple benchmark to test per-entity draw overhead
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
`orthographic` | [`3d/orthographic.rs`](./3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
@ -123,6 +122,7 @@ Example | File | Description
Example | File | Description
--- | --- | ---
`animated_fox` | [`animation/animated_fox.rs`](./animation/animated_fox.rs) | Plays an animation from a skinned glTF.
`custom_skinned_mesh` | [`animation/custom_skinned_mesh.rs`](./animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code.
`gltf_skinned_mesh` | [`animation/gltf_skinned_mesh.rs`](./animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file.

View file

@ -0,0 +1,127 @@
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.0,
})
.add_startup_system(setup)
.add_system(setup_scene_once_loaded)
.add_system(keyboard_animation_control)
.run();
}
struct Animations(Vec<Handle<AnimationClip>>);
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut scene_spawner: ResMut<SceneSpawner>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Insert a resource with the current scene information
commands.insert_resource(Animations(vec![
asset_server.load("models/animated/Fox.glb#Animation2"),
asset_server.load("models/animated/Fox.glb#Animation1"),
asset_server.load("models/animated/Fox.glb#Animation0"),
]));
// Camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(100.0, 100.0, 150.0)
.looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y),
..Default::default()
});
// Plane
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 500000.0 })),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
..default()
});
// Light
commands.spawn_bundle(DirectionalLightBundle {
transform: Transform::from_rotation(Quat::from_euler(
EulerRot::ZYX,
0.0,
1.0,
-std::f32::consts::FRAC_PI_4,
)),
directional_light: DirectionalLight {
shadows_enabled: true,
..default()
},
..default()
});
// Fox
scene_spawner.spawn(asset_server.load("models/animated/Fox.glb#Scene0"));
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>,
mut player: Query<&mut AnimationPlayer>,
mut done: Local<bool>,
) {
if !*done {
if let Ok(mut player) = player.get_single_mut() {
player.play(animations.0[0].clone_weak()).repeat();
*done = true;
}
}
}
fn keyboard_animation_control(
keyboard_input: Res<Input<KeyCode>>,
mut animation_player: Query<&mut AnimationPlayer>,
animations: Res<Animations>,
mut current_animation: Local<usize>,
) {
if let Ok(mut player) = animation_player.get_single_mut() {
if keyboard_input.just_pressed(KeyCode::Space) {
if player.is_paused() {
player.resume();
} else {
player.pause();
}
}
if keyboard_input.just_pressed(KeyCode::Up) {
let speed = player.speed();
player.set_speed(speed * 1.2);
}
if keyboard_input.just_pressed(KeyCode::Down) {
let speed = player.speed();
player.set_speed(speed * 0.8);
}
if keyboard_input.just_pressed(KeyCode::Left) {
let elapsed = player.elapsed();
player.set_elapsed(elapsed - 0.1);
}
if keyboard_input.just_pressed(KeyCode::Right) {
let elapsed = player.elapsed();
player.set_elapsed(elapsed + 0.1);
}
if keyboard_input.just_pressed(KeyCode::Return) {
*current_animation = (*current_animation + 1) % animations.0.len();
player
.play(animations.0[*current_animation].clone_weak())
.repeat();
}
}
}