diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 0f54671a0fd0b..1ee8c36df401d 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["bevy"] bevy_app = { path = "../bevy_app", version = "0.13.0" } bevy_asset = { path = "../bevy_asset", version = "0.13.0" } bevy_core = { path = "../bevy_core", version = "0.13.0" } +bevy_log = { path = "../bevy_log", version = "0.13.0" } bevy_math = { path = "../bevy_math", version = "0.13.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.13.0", features = [ "bevy", @@ -24,5 +25,9 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.13.0" } bevy_transform = { path = "../bevy_transform", version = "0.13.0" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.13.0" } +# other +sha1_smol = { version = "1.0" } +uuid = { version = "1.7", features = ["v5"] } + [lints] workspace = true diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9421d247ffa62..bef6c87157c30 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -3,30 +3,41 @@ mod animatable; mod util; -use std::ops::{Add, Deref, Mul}; +use std::hash::{Hash, Hasher}; +use std::iter; +use std::ops::{Add, Mul}; use std::time::Duration; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; use bevy_core::Name; +use bevy_ecs::entity::MapEntities; use bevy_ecs::prelude::*; -use bevy_hierarchy::{Children, Parent}; +use bevy_ecs::reflect::ReflectMapEntities; +use bevy_log::error; use bevy_math::{FloatExt, Quat, Vec3}; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; -use bevy_utils::{tracing::warn, HashMap}; +use bevy_utils::hashbrown::HashMap; +use bevy_utils::{NoOpHash, Uuid}; +use sha1_smol::Sha1; #[allow(missing_docs)] pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation, - Keyframes, VariableCurve, + animatable::*, AnimationClip, AnimationPlayer, AnimationPlugin, Interpolation, Keyframes, + VariableCurve, }; } +/// The [UUID namespace] of animation targets (e.g. bones). +/// +/// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) +pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); + /// List of keyframes for one of the attribute of a [`Transform`]. #[derive(Reflect, Clone, Debug)] pub enum Keyframes { @@ -138,68 +149,121 @@ pub enum Interpolation { CubicSpline, } -/// Path to an entity, with [`Name`]s. Each entity in a path must have a name. -#[derive(Reflect, Clone, Debug, Hash, PartialEq, Eq, Default)] -pub struct EntityPath { - /// Parts of the path - pub parts: Vec, -} - -/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply. +/// A list of [`VariableCurve`]s and the [`AnimationTargetId`]s to which they +/// apply. +/// +/// Because animation clips refer to targets by UUID, they can target any +/// [`AnimationTarget`] with that ID. #[derive(Asset, Reflect, Clone, Debug, Default)] pub struct AnimationClip { - curves: Vec>, - paths: HashMap, + curves: AnimationCurves, duration: f32, } -impl AnimationClip { - #[inline] - /// [`VariableCurve`]s for each bone. Indexed by the bone ID. - pub fn curves(&self) -> &Vec> { - &self.curves +/// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the +/// animation curves. +pub type AnimationCurves = HashMap, NoOpHash>; + +/// A unique [UUID] for an animation target (e.g. bone in a skinned mesh). +/// +/// The [`AnimationClip`] asset and the [`AnimationTarget`] component both use +/// this to refer to targets (e.g. bones in a skinned mesh) to be animated. +/// +/// When importing an armature or an animation clip, asset loaders typically use +/// the full path name from the armature to the bone to generate these UUIDs. +/// The ID is unique to the full path name and based only on the names. So, for +/// example, any imported armature with a bone at the root named `Hips` will +/// assign the same [`AnimationTargetId`] to its root bone. Likewise, any +/// imported animation clip that animates a root bone named `Hips` will +/// reference the same [`AnimationTargetId`]. Any animation is playable on any +/// armature as long as the bone names match, which allows for easy animation +/// retargeting. +/// +/// Note that asset loaders generally use the *full* path name to generate the +/// [`AnimationTargetId`]. Thus a bone named `Chest` directly connected to a +/// bone named `Hips` will have a different ID from a bone named `Chest` that's +/// connected to a bone named `Stomach`. +/// +/// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug)] +pub struct AnimationTargetId(pub Uuid); + +impl Hash for AnimationTargetId { + fn hash(&self, state: &mut H) { + let (hi, lo) = self.0.as_u64_pair(); + state.write_u64(hi ^ lo); } +} - /// Gets the curves for a bone. +/// An entity that can be animated by an [`AnimationPlayer`]. +/// +/// These are frequently referred to as *bones* or *joints*, because they often +/// refer to individually-animatable parts of an armature. +/// +/// Asset loaders for armatures are responsible for adding these as necessary. +/// Typically, they're generated from hashed versions of the entire name path +/// from the root of the armature to the bone. See the [`AnimationTargetId`] +/// documentation for more details. +/// +/// By convention, asset loaders add [`AnimationTarget`] components to the +/// descendants of an [`AnimationPlayer`], as well as to the [`AnimationPlayer`] +/// entity itself, but Bevy doesn't require this in any way. So, for example, +/// it's entirely possible for an [`AnimationPlayer`] to animate a target that +/// it isn't an ancestor of. If you add a new bone to or delete a bone from an +/// armature at runtime, you may want to update the [`AnimationTarget`] +/// component as appropriate, as Bevy won't do this automatically. +/// +/// Note that each entity can only be animated by one animation player at a +/// time. However, you can change [`AnimationTarget`]'s `player` property at +/// runtime to change which player is responsible for animating the entity. +#[derive(Clone, Component, Reflect)] +#[reflect(Component, MapEntities)] +pub struct AnimationTarget { + /// The ID of this animation target. /// - /// Returns `None` if the bone is invalid. + /// Typically, this is derived from the path. + pub id: AnimationTargetId, + + /// The entity containing the [`AnimationPlayer`]. + pub player: Entity, +} + +impl AnimationClip { #[inline] - pub fn get_curves(&self, bone_id: usize) -> Option<&'_ Vec> { - self.curves.get(bone_id) + /// [`VariableCurve`]s for each animation target. Indexed by the [`AnimationTargetId`]. + pub fn curves(&self) -> &AnimationCurves { + &self.curves } - /// Gets the curves by it's [`EntityPath`]. + /// Gets the curves for a single animation target. /// - /// Returns `None` if the bone is invalid. + /// Returns `None` if this clip doesn't animate the target. #[inline] - pub fn get_curves_by_path(&self, path: &EntityPath) -> Option<&'_ Vec> { - self.paths.get(path).and_then(|id| self.curves.get(*id)) + pub fn curves_for_target( + &self, + target_id: AnimationTargetId, + ) -> Option<&'_ Vec> { + self.curves.get(&target_id) } - /// Duration of the clip, represented in seconds + /// Duration of the clip, represented in seconds. #[inline] pub fn duration(&self) -> f32 { self.duration } - /// Add a [`VariableCurve`] to an [`EntityPath`]. - pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) { + /// Adds a [`VariableCurve`] to an [`AnimationTarget`] named by an + /// [`AnimationTargetId`]. + /// + /// If the curve extends beyond the current duration of this clip, this + /// method lengthens this clip to include the entire time span that the + /// curve covers. + pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, 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)); - if let Some(bone_id) = self.paths.get(&path) { - self.curves[*bone_id].push(curve); - } else { - let idx = self.curves.len(); - self.curves.push(vec![curve]); - self.paths.insert(path, idx); - } - } - - /// Whether this animation clip can run on entity with given [`Name`]. - pub fn compatible_with(&self, name: &Name) -> bool { - self.paths.keys().any(|path| &path.parts[0] == name) + self.curves.entry(target_id).or_default().push(curve); } } @@ -228,7 +292,6 @@ struct PlayingAnimation { /// Note: This will always be in the range [0.0, animation clip duration] seek_time: f32, animation_clip: Handle, - path_cache: Vec>>, /// Number of times the animation has completed. /// If the animation is playing in reverse, this increments when the animation passes the start. completions: u32, @@ -242,7 +305,6 @@ impl Default for PlayingAnimation { elapsed: 0.0, seek_time: 0.0, animation_clip: Default::default(), - path_cache: Vec::new(), completions: 0, } } @@ -325,6 +387,16 @@ pub struct AnimationPlayer { transitions: Vec, } +/// The components that we might need to read or write during animation of each +/// animation target. +struct AnimationTargetContext<'a> { + entity: Entity, + target: &'a AnimationTarget, + name: Option<&'a Name>, + transform: Option>, + morph_weights: Option>, +} + impl AnimationPlayer { /// Start playing an animation, resetting state of the player. /// This will use a linear blending between the previous and the new animation to make a smooth transition. @@ -479,172 +551,90 @@ impl AnimationPlayer { } } -fn entity_from_path( - root: Entity, - path: &EntityPath, - children: &Query<&Children>, - names: &Query<&Name>, - path_cache: &mut Vec>, -) -> Option { - // PERF: finding the target entity can be optimised - let mut current_entity = root; - path_cache.resize(path.parts.len(), None); - - let mut parts = path.parts.iter().enumerate(); - - // check the first name is the root node which we already have - let (_, root_name) = parts.next()?; - if names.get(current_entity) != Ok(root_name) { - return None; - } - - for (idx, part) in parts { - let mut found = false; - let children = children.get(current_entity).ok()?; - if let Some(cached) = path_cache[idx] { - if children.contains(&cached) { - if let Ok(name) = names.get(cached) { - if name == part { - current_entity = cached; - found = true; - } - } - } - } - if !found { - 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; - path_cache[idx] = Some(*child); - found = true; - break; - } - } - } - } - if !found { - warn!("Entity not found for path {:?} on part {:?}", path, part); - return None; +/// A system that advances the time for all playing animations. +pub fn advance_animations( + time: Res