Skip to content

Commit

Permalink
animation player (#4375)
Browse files Browse the repository at this point in the history
# Objective

- Add a basic animation player
  - Single track
  - Not generic, can only animate `Transform`s
  - With plenty of possible optimisations available
  - Close-ish to bevyengine/rfcs#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
  • Loading branch information
mockersf committed Apr 2, 2022
1 parent 17afa62 commit 59fd0f8
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 140 deletions.
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
65 changes: 8 additions & 57 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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>();
}
}

Expand All @@ -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
Expand All @@ -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,
}
188 changes: 108 additions & 80 deletions src/loader.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -129,77 +128,95 @@ async fn load_gltf<'a, 'b>(
}
}

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();
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
}
};
let node = channel.target().node();
let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));
let keyframe_timestamps: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
match inputs {
gltf::accessor::Iter::Standard(times) => times.collect(),
gltf::accessor::Iter::Sparse(_) => {
warn!("sparse accessor not supported for animation sampler input");
continue;
}
}
} else {
warn!("animations without a sampler input are not supported");
return Err(GltfError::MissingAnimationSampler(animation.index()));
};
#[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
};

let keyframes = if let Some(outputs) = reader.read_outputs() {
match outputs {
gltf::animation::util::ReadOutputs::Translations(tr) => {
GltfNodeAnimationKeyframes::Translation(tr.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
GltfNodeAnimationKeyframes::Rotation(
rots.into_f32().map(Quat::from_array).collect(),
)
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
GltfNodeAnimationKeyframes::Scale(scale.map(Vec3::from).collect())
#[cfg(feature = "bevy_animation")]
let (animations, named_animations) = {
let mut animations = vec![];
let mut named_animations = HashMap::default();
for animation in gltf.animations() {
let mut animation_clip = AnimationClip::default();
for channel in animation.channels() {
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()]));
let keyframe_timestamps: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
match inputs {
gltf::accessor::Iter::Standard(times) => times.collect(),
gltf::accessor::Iter::Sparse(_) => {
warn!("Sparse accessor not supported for animation sampler input");
continue;
}
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
continue;
} else {
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) => {
Keyframes::Translation(tr.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::Rotations(rots) => {
Keyframes::Rotation(rots.into_f32().map(Quat::from_array).collect())
}
gltf::animation::util::ReadOutputs::Scales(scale) => {
Keyframes::Scale(scale.map(Vec3::from).collect())
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
warn!("Morph animation property not yet supported");
continue;
}
}
}
} else {
warn!("animations without a sampler output are not supported");
return Err(GltfError::MissingAnimationSampler(animation.index()));
};
} else {
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 {
keyframe_timestamps,
keyframes,
interpolation,
});
animated_nodes.insert(node.index());
}
let handle = load_context.set_labeled_asset(
&format!("Animation{}", animation.index()),
LoadedAsset::new(gltf_animation),
);
if let Some(name) = animation.name() {
named_animations.insert(name.to_string(), handle.clone());
if let Some(path) = paths.get(&node.index()) {
animation_clip.add_curve_to_path(
EntityPath {
parts: path.clone(),
},
VariableCurve {
keyframe_timestamps,
keyframes,
},
);
} 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(animation_clip),
);
if let Some(name) = animation.name() {
named_animations.insert(name.to_string(), handle.clone());
}
animations.push(handle);
}
animations.push(handle);
}
(animations, named_animations)
};

let mut meshes = vec![];
let mut named_meshes = HashMap::default();
Expand Down Expand Up @@ -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,
);
Expand All @@ -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();
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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> {
Expand All @@ -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()));
}
Expand Down Expand Up @@ -798,7 +827,6 @@ fn load_node(
parent,
load_context,
buffer_data,
animated_nodes,
node_index_to_entity_map,
entity_to_skin_index_map,
) {
Expand Down

0 comments on commit 59fd0f8

Please sign in to comment.