From 59fd0f86682ede586f303e6bc1a512fe5c8a7019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sat, 2 Apr 2022 22:36:02 +0000 Subject: [PATCH] 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 --- Cargo.toml | 7 +- src/lib.rs | 65 +++-------------- src/loader.rs | 188 +++++++++++++++++++++++++++++--------------------- 3 files changed, 120 insertions(+), 140 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90e85c8..c29efda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ diff --git a/src/lib.rs b/src/lib.rs index 1710cc5..fa55f10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::() .add_asset::() .add_asset::() - .add_asset::() - .add_asset::() - .register_type::(); + .add_asset::(); } } @@ -41,8 +39,10 @@ pub struct Gltf { pub nodes: Vec>, pub named_nodes: HashMap>, pub default_scene: Option>, - pub animations: Vec>, - pub named_animations: HashMap>, + #[cfg(feature = "bevy_animation")] + pub animations: Vec>, + #[cfg(feature = "bevy_animation")] + pub named_animations: HashMap>, } /// A glTF node with all of its child nodes, its [`GltfMesh`] and @@ -69,52 +69,3 @@ pub struct GltfPrimitive { pub mesh: Handle, pub material: Option>, } - -/// 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, - 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>, -} - -/// Key frames of an animation. -#[derive(Clone, Debug)] -pub enum GltfNodeAnimationKeyframes { - Rotation(Vec), - Translation(Vec), - Scale(Vec), -} - -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, -} diff --git a/src/loader.rs b/src/loader.rs index 8b45290..a4f0aa9 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -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,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 = 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::>::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 = 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(); @@ -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>) { + 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], - animated_nodes: &HashSet, node_index_to_entity_map: &mut HashMap, entity_to_skin_index_map: &mut HashMap, ) -> 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, ) {