mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
Rework animation to be done in two phases. (#11707)
# Objective Bevy's animation system currently does tree traversals based on `Name` that aren't necessary. Not only do they require in unsafe code because tree traversals are awkward with parallelism, but they are also somewhat slow, brittle, and complex, which manifested itself as way too many queries in #11670. # Solution Divide animation into two phases: animation *advancement* and animation *evaluation*, which run after one another. *Advancement* operates on the `AnimationPlayer` and sets the current animation time to match the game time. *Evaluation* operates on all animation bones in the scene in parallel and sets the transforms and/or morph weights based on the time and the clip. To do this, we introduce a new component, `AnimationTarget`, which the asset loader places on every bone. It contains the ID of the entity containing the `AnimationPlayer`, as well as a UUID that identifies which bone in the animation the target corresponds to. In the case of glTF, the UUID is derived from the full path name to the bone. The rule that `AnimationTarget`s are descendants of the entity containing `AnimationPlayer` is now just a convention, not a requirement; this allows us to eliminate the unsafe code. # Migration guide * `AnimationClip` now uses UUIDs instead of hierarchical paths based on the `Name` component to refer to bones. This has several consequences: - A new component, `AnimationTarget`, should be placed on each bone that you wish to animate, in order to specify its UUID and the associated `AnimationPlayer`. The glTF loader automatically creates these components as necessary, so most uses of glTF rigs shouldn't need to change. - Moving a bone around the tree, or renaming it, no longer prevents an `AnimationPlayer` from affecting it. - Dynamically changing the `AnimationPlayer` component will likely require manual updating of the `AnimationTarget` components. * Entities with `AnimationPlayer` components may now possess descendants that also have `AnimationPlayer` components. They may not, however, animate the same bones. * As they aren't specific to `TypeId`s, `bevy_reflect::utility::NoOpTypeIdHash` and `bevy_reflect::utility::NoOpTypeIdHasher` have been renamed to `bevy_reflect::utility::NoOpHash` and `bevy_reflect::utility::NoOpHasher` respectively.
This commit is contained in:
parent
f1fcf6932b
commit
5f1dd3918b
9 changed files with 757 additions and 498 deletions
|
@ -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
|
||||
|
|
|
@ -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<Name>,
|
||||
}
|
||||
|
||||
/// 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<Vec<VariableCurve>>,
|
||||
paths: HashMap<EntityPath, usize>,
|
||||
curves: AnimationCurves,
|
||||
duration: f32,
|
||||
}
|
||||
|
||||
/// A mapping from [`AnimationTargetId`] (e.g. bone in a skinned mesh) to the
|
||||
/// animation curves.
|
||||
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, 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<H: Hasher>(&self, state: &mut H) {
|
||||
let (hi, lo) = self.0.as_u64_pair();
|
||||
state.write_u64(hi ^ lo);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Typically, this is derived from the path.
|
||||
pub id: AnimationTargetId,
|
||||
|
||||
/// The entity containing the [`AnimationPlayer`].
|
||||
pub player: Entity,
|
||||
}
|
||||
|
||||
impl AnimationClip {
|
||||
#[inline]
|
||||
/// [`VariableCurve`]s for each bone. Indexed by the bone ID.
|
||||
pub fn curves(&self) -> &Vec<Vec<VariableCurve>> {
|
||||
/// [`VariableCurve`]s for each animation target. Indexed by the [`AnimationTargetId`].
|
||||
pub fn curves(&self) -> &AnimationCurves {
|
||||
&self.curves
|
||||
}
|
||||
|
||||
/// Gets the curves for a bone.
|
||||
/// 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(&self, bone_id: usize) -> Option<&'_ Vec<VariableCurve>> {
|
||||
self.curves.get(bone_id)
|
||||
pub fn curves_for_target(
|
||||
&self,
|
||||
target_id: AnimationTargetId,
|
||||
) -> Option<&'_ Vec<VariableCurve>> {
|
||||
self.curves.get(&target_id)
|
||||
}
|
||||
|
||||
/// Gets the curves by it's [`EntityPath`].
|
||||
///
|
||||
/// Returns `None` if the bone is invalid.
|
||||
#[inline]
|
||||
pub fn get_curves_by_path(&self, path: &EntityPath) -> Option<&'_ Vec<VariableCurve>> {
|
||||
self.paths.get(path).and_then(|id| self.curves.get(*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<AnimationClip>,
|
||||
path_cache: Vec<Vec<Option<Entity>>>,
|
||||
/// 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<AnimationTransition>,
|
||||
}
|
||||
|
||||
/// 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<Mut<'a, Transform>>,
|
||||
morph_weights: Option<Mut<'a, MorphWeights>>,
|
||||
}
|
||||
|
||||
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<Entity>>,
|
||||
) -> Option<Entity> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Some(current_entity)
|
||||
}
|
||||
|
||||
/// Verify that there are no ancestors of a given entity that have an [`AnimationPlayer`].
|
||||
fn verify_no_ancestor_player(
|
||||
player_parent: Option<&Parent>,
|
||||
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
|
||||
) -> bool {
|
||||
let Some(mut current) = player_parent.map(Parent::get) else {
|
||||
return true;
|
||||
};
|
||||
loop {
|
||||
let Ok((has_player, parent)) = parents.get(current) else {
|
||||
return true;
|
||||
};
|
||||
if has_player {
|
||||
return false;
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
current = parent.get();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System that will play all animations, using any entity with a [`AnimationPlayer`]
|
||||
/// and a [`Handle<AnimationClip>`] as an animation root
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn animation_player(
|
||||
/// A system that advances the time for all playing animations.
|
||||
pub fn advance_animations(
|
||||
time: Res<Time>,
|
||||
animations: Res<Assets<AnimationClip>>,
|
||||
children: Query<&Children>,
|
||||
names: Query<&Name>,
|
||||
transforms: Query<&mut Transform>,
|
||||
morphs: Query<&mut MorphWeights>,
|
||||
parents: Query<(Has<AnimationPlayer>, Option<&Parent>)>,
|
||||
mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>,
|
||||
animation_clips: Res<Assets<AnimationClip>>,
|
||||
mut players: Query<&mut AnimationPlayer>,
|
||||
) {
|
||||
animation_players
|
||||
.par_iter_mut()
|
||||
.for_each(|(root, maybe_parent, mut player)| {
|
||||
update_transitions(&mut player, &time);
|
||||
run_animation_player(
|
||||
root,
|
||||
player,
|
||||
&time,
|
||||
&animations,
|
||||
&names,
|
||||
&transforms,
|
||||
&morphs,
|
||||
maybe_parent,
|
||||
&parents,
|
||||
&children,
|
||||
);
|
||||
for mut player in players.iter_mut() {
|
||||
let paused = player.paused;
|
||||
if paused {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advance the main animation.
|
||||
if let Some(animation_clip) = animation_clips.get(&player.animation.animation_clip) {
|
||||
player
|
||||
.animation
|
||||
.update(time.delta_seconds(), animation_clip.duration);
|
||||
};
|
||||
|
||||
// Advance transition animations.
|
||||
player.transitions.retain_mut(|transition| {
|
||||
// Decrease weight. Expire the transition if necessary.
|
||||
transition.current_weight -= transition.weight_decline_per_sec * time.delta_seconds();
|
||||
if transition.current_weight <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(animation_clip) = animation_clips.get(&transition.animation.animation_clip)
|
||||
{
|
||||
transition
|
||||
.animation
|
||||
.update(time.delta_seconds(), animation_clip.duration);
|
||||
};
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_animation_player(
|
||||
root: Entity,
|
||||
mut player: Mut<AnimationPlayer>,
|
||||
time: &Time,
|
||||
animations: &Assets<AnimationClip>,
|
||||
names: &Query<&Name>,
|
||||
transforms: &Query<&mut Transform>,
|
||||
morphs: &Query<&mut MorphWeights>,
|
||||
maybe_parent: Option<&Parent>,
|
||||
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
|
||||
children: &Query<&Children>,
|
||||
/// A system that modifies animation targets (e.g. bones in a skinned mesh)
|
||||
/// according to the currently-playing animation.
|
||||
pub fn animate_targets(
|
||||
clips: Res<Assets<AnimationClip>>,
|
||||
players: Query<&AnimationPlayer>,
|
||||
mut targets: Query<(
|
||||
Entity,
|
||||
&AnimationTarget,
|
||||
Option<&Name>,
|
||||
AnyOf<(&mut Transform, &mut MorphWeights)>,
|
||||
)>,
|
||||
) {
|
||||
let paused = player.paused;
|
||||
// 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 paused && !player.is_changed() {
|
||||
return;
|
||||
}
|
||||
// We use two queries here: one read-only query for animation players and
|
||||
// one read-write query for animation targets (e.g. bones). The
|
||||
// `AnimationPlayer` query is read-only shared memory accessible from all
|
||||
// animation targets, which are evaluated in parallel.
|
||||
|
||||
// Apply the main animation
|
||||
apply_animation(
|
||||
1.0,
|
||||
&mut player.animation,
|
||||
paused,
|
||||
root,
|
||||
time,
|
||||
animations,
|
||||
names,
|
||||
transforms,
|
||||
morphs,
|
||||
maybe_parent,
|
||||
parents,
|
||||
children,
|
||||
);
|
||||
// Iterate over all animation targets in parallel.
|
||||
targets
|
||||
.par_iter_mut()
|
||||
.for_each(|(id, target, name, (transform, morph_weights))| {
|
||||
let mut target_context = AnimationTargetContext {
|
||||
entity: id,
|
||||
target,
|
||||
name,
|
||||
transform,
|
||||
morph_weights,
|
||||
};
|
||||
|
||||
// Apply any potential fade-out transitions from previous animations
|
||||
for AnimationTransition {
|
||||
current_weight,
|
||||
animation,
|
||||
..
|
||||
} in &mut player.transitions
|
||||
{
|
||||
apply_animation(
|
||||
*current_weight,
|
||||
animation,
|
||||
paused,
|
||||
root,
|
||||
time,
|
||||
animations,
|
||||
names,
|
||||
transforms,
|
||||
morphs,
|
||||
maybe_parent,
|
||||
parents,
|
||||
children,
|
||||
);
|
||||
}
|
||||
let Ok(player) = players.get(target.player) else {
|
||||
error!(
|
||||
"Couldn't find the animation player {:?} for the target entity {:?} ({:?})",
|
||||
target.player, target_context.entity, target_context.name,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
player.animation.apply(&clips, 1.0, &mut target_context);
|
||||
|
||||
for transition in &player.transitions {
|
||||
transition
|
||||
.animation
|
||||
.apply(&clips, transition.current_weight, &mut target_context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update `weights` based on weights in `keyframe` with a linear interpolation
|
||||
|
@ -689,217 +679,259 @@ where
|
|||
+ tangent_in_end * step_duration * (lerp.powi(3) - lerp.powi(2))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn apply_animation(
|
||||
weight: f32,
|
||||
animation: &mut PlayingAnimation,
|
||||
paused: bool,
|
||||
root: Entity,
|
||||
time: &Time,
|
||||
animations: &Assets<AnimationClip>,
|
||||
names: &Query<&Name>,
|
||||
transforms: &Query<&mut Transform>,
|
||||
morphs: &Query<&mut MorphWeights>,
|
||||
maybe_parent: Option<&Parent>,
|
||||
parents: &Query<(Has<AnimationPlayer>, Option<&Parent>)>,
|
||||
children: &Query<&Children>,
|
||||
) {
|
||||
let Some(animation_clip) = animations.get(&animation.animation_clip) else {
|
||||
return;
|
||||
};
|
||||
/// Adds animation support to an app
|
||||
#[derive(Default)]
|
||||
pub struct AnimationPlugin;
|
||||
|
||||
// We don't return early because seek_to() may have been called on the animation player.
|
||||
animation.update(
|
||||
if paused { 0.0 } else { time.delta_seconds() },
|
||||
animation_clip.duration,
|
||||
);
|
||||
|
||||
if animation.path_cache.len() != animation_clip.paths.len() {
|
||||
let new_len = animation_clip.paths.len();
|
||||
animation.path_cache.iter_mut().for_each(|v| v.clear());
|
||||
animation.path_cache.resize_with(new_len, Vec::new);
|
||||
}
|
||||
if !verify_no_ancestor_player(maybe_parent, parents) {
|
||||
warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root);
|
||||
return;
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset::<AnimationClip>()
|
||||
.register_asset_reflect::<AnimationClip>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.register_type::<VariableCurve>()
|
||||
.register_type::<Vec<VariableCurve>>()
|
||||
.register_type::<Interpolation>()
|
||||
.register_type::<Keyframes>()
|
||||
.register_type::<AnimationTarget>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(advance_animations, animate_targets)
|
||||
.chain()
|
||||
.before(TransformSystem::TransformPropagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut any_path_found = false;
|
||||
for (path, bone_id) in &animation_clip.paths {
|
||||
let cached_path = &mut animation.path_cache[*bone_id];
|
||||
let curves = animation_clip.get_curves(*bone_id).unwrap();
|
||||
let Some(target) = entity_from_path(root, path, children, names, cached_path) else {
|
||||
continue;
|
||||
impl PlayingAnimation {
|
||||
fn apply(
|
||||
&self,
|
||||
clips: &Assets<AnimationClip>,
|
||||
weight: f32,
|
||||
target_context: &mut AnimationTargetContext,
|
||||
) {
|
||||
let Some(clip) = clips.get(&self.animation_clip) else {
|
||||
// The clip probably hasn't loaded yet. Bail.
|
||||
return;
|
||||
};
|
||||
any_path_found = true;
|
||||
// SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias
|
||||
// any of their descendant Transforms.
|
||||
//
|
||||
// The system scheduler prevents any other system from mutating Transforms at the same time,
|
||||
// so the only way this fetch can alias is if two AnimationPlayers are targeting the same bone.
|
||||
// This can only happen if there are two or more AnimationPlayers are ancestors to the same
|
||||
// entities. By verifying that there is no other AnimationPlayer in the ancestors of a
|
||||
// running AnimationPlayer before animating any entity, this fetch cannot alias.
|
||||
//
|
||||
// This means only the AnimationPlayers closest to the root of the hierarchy will be able
|
||||
// to run their animation. Any players in the children or descendants will log a warning
|
||||
// and do nothing.
|
||||
let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else {
|
||||
continue;
|
||||
|
||||
let Some(curves) = clip.curves_for_target(target_context.target.id) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: As above, there can't be other AnimationPlayers with this target so this fetch can't alias
|
||||
let mut morphs = unsafe { morphs.get_unchecked(target) }.ok();
|
||||
|
||||
for curve in curves {
|
||||
// Some curves have only one keyframe used to set a transform
|
||||
if curve.keyframe_timestamps.len() == 1 {
|
||||
match &curve.keyframes {
|
||||
Keyframes::Rotation(keyframes) => {
|
||||
transform.rotation = transform.rotation.slerp(keyframes[0], weight);
|
||||
}
|
||||
Keyframes::Translation(keyframes) => {
|
||||
transform.translation = transform.translation.lerp(keyframes[0], weight);
|
||||
}
|
||||
Keyframes::Scale(keyframes) => {
|
||||
transform.scale = transform.scale.lerp(keyframes[0], weight);
|
||||
}
|
||||
Keyframes::Weights(keyframes) => {
|
||||
if let Some(morphs) = &mut morphs {
|
||||
let target_count = morphs.weights().len();
|
||||
lerp_morph_weights(
|
||||
morphs.weights_mut(),
|
||||
get_keyframe(target_count, keyframes, 0).iter().copied(),
|
||||
weight,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
self.apply_single_keyframe(curve, weight, target_context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the current keyframe
|
||||
let Some(step_start) = curve.find_current_keyframe(animation.seek_time) else {
|
||||
continue;
|
||||
let Some(step_start) = curve.find_current_keyframe(self.seek_time) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let timestamp_start = curve.keyframe_timestamps[step_start];
|
||||
let timestamp_end = curve.keyframe_timestamps[step_start + 1];
|
||||
// Compute how far we are through the keyframe, normalized to [0, 1]
|
||||
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, animation.seek_time);
|
||||
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, self.seek_time);
|
||||
|
||||
apply_keyframe(
|
||||
self.apply_tweened_keyframe(
|
||||
curve,
|
||||
step_start,
|
||||
weight,
|
||||
lerp,
|
||||
timestamp_end - timestamp_start,
|
||||
&mut transform,
|
||||
&mut morphs,
|
||||
target_context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !any_path_found {
|
||||
warn!("Animation player on {root:?} did not match any entity paths.");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn apply_keyframe(
|
||||
curve: &VariableCurve,
|
||||
step_start: usize,
|
||||
weight: f32,
|
||||
lerp: f32,
|
||||
duration: f32,
|
||||
transform: &mut Mut<Transform>,
|
||||
morphs: &mut Option<Mut<MorphWeights>>,
|
||||
) {
|
||||
match (&curve.interpolation, &curve.keyframes) {
|
||||
(Interpolation::Step, Keyframes::Rotation(keyframes)) => {
|
||||
transform.rotation = transform.rotation.slerp(keyframes[step_start], weight);
|
||||
}
|
||||
(Interpolation::Linear, 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;
|
||||
fn apply_single_keyframe(
|
||||
&self,
|
||||
curve: &VariableCurve,
|
||||
weight: f32,
|
||||
target_context: &mut AnimationTargetContext,
|
||||
) {
|
||||
match &curve.keyframes {
|
||||
Keyframes::Rotation(keyframes) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.rotation = transform.rotation.slerp(keyframes[0], weight);
|
||||
}
|
||||
}
|
||||
|
||||
Keyframes::Translation(keyframes) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.translation = transform.translation.lerp(keyframes[0], weight);
|
||||
}
|
||||
}
|
||||
|
||||
Keyframes::Scale(keyframes) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.scale = transform.scale.lerp(keyframes[0], weight);
|
||||
}
|
||||
}
|
||||
|
||||
Keyframes::Weights(keyframes) => {
|
||||
let Some(ref mut morphs) = target_context.morph_weights else {
|
||||
error!(
|
||||
"Tried to animate morphs on {:?} ({:?}), but no `MorphWeights` was found",
|
||||
target_context.entity, target_context.name,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let target_count = morphs.weights().len();
|
||||
lerp_morph_weights(
|
||||
morphs.weights_mut(),
|
||||
get_keyframe(target_count, keyframes, 0).iter().copied(),
|
||||
weight,
|
||||
);
|
||||
}
|
||||
// Rotations are using a spherical linear interpolation
|
||||
let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp);
|
||||
transform.rotation = transform.rotation.slerp(rot, weight);
|
||||
}
|
||||
(Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => {
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.rotation = transform.rotation.slerp(result.normalize(), weight);
|
||||
}
|
||||
(Interpolation::Step, Keyframes::Translation(keyframes)) => {
|
||||
transform.translation = transform.translation.lerp(keyframes[step_start], weight);
|
||||
}
|
||||
(Interpolation::Linear, 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 = transform.translation.lerp(result, weight);
|
||||
}
|
||||
(Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => {
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.translation = transform.translation.lerp(result, weight);
|
||||
}
|
||||
(Interpolation::Step, Keyframes::Scale(keyframes)) => {
|
||||
transform.scale = transform.scale.lerp(keyframes[step_start], weight);
|
||||
}
|
||||
(Interpolation::Linear, 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 = transform.scale.lerp(result, weight);
|
||||
}
|
||||
(Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => {
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.scale = transform.scale.lerp(result, weight);
|
||||
}
|
||||
(Interpolation::Step, Keyframes::Weights(keyframes)) => {
|
||||
if let Some(morphs) = morphs {
|
||||
}
|
||||
|
||||
fn apply_tweened_keyframe(
|
||||
&self,
|
||||
curve: &VariableCurve,
|
||||
step_start: usize,
|
||||
weight: f32,
|
||||
lerp: f32,
|
||||
duration: f32,
|
||||
target_context: &mut AnimationTargetContext,
|
||||
) {
|
||||
match (&curve.interpolation, &curve.keyframes) {
|
||||
(Interpolation::Step, Keyframes::Rotation(keyframes)) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.rotation = transform.rotation.slerp(keyframes[step_start], weight);
|
||||
}
|
||||
}
|
||||
|
||||
(Interpolation::Linear, Keyframes::Rotation(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
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
|
||||
let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp);
|
||||
transform.rotation = transform.rotation.slerp(rot, weight);
|
||||
}
|
||||
|
||||
(Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.rotation = transform.rotation.slerp(result.normalize(), weight);
|
||||
}
|
||||
|
||||
(Interpolation::Step, Keyframes::Translation(keyframes)) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.translation =
|
||||
transform.translation.lerp(keyframes[step_start], weight);
|
||||
}
|
||||
}
|
||||
|
||||
(Interpolation::Linear, Keyframes::Translation(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
let translation_start = keyframes[step_start];
|
||||
let translation_end = keyframes[step_start + 1];
|
||||
let result = translation_start.lerp(translation_end, lerp);
|
||||
transform.translation = transform.translation.lerp(result, weight);
|
||||
}
|
||||
|
||||
(Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.translation = transform.translation.lerp(result, weight);
|
||||
}
|
||||
|
||||
(Interpolation::Step, Keyframes::Scale(keyframes)) => {
|
||||
if let Some(ref mut transform) = target_context.transform {
|
||||
transform.scale = transform.scale.lerp(keyframes[step_start], weight);
|
||||
}
|
||||
}
|
||||
|
||||
(Interpolation::Linear, Keyframes::Scale(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
let scale_start = keyframes[step_start];
|
||||
let scale_end = keyframes[step_start + 1];
|
||||
let result = scale_start.lerp(scale_end, lerp);
|
||||
transform.scale = transform.scale.lerp(result, weight);
|
||||
}
|
||||
|
||||
(Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => {
|
||||
let Some(ref mut transform) = target_context.transform else {
|
||||
return;
|
||||
};
|
||||
|
||||
let value_start = keyframes[step_start * 3 + 1];
|
||||
let tangent_out_start = keyframes[step_start * 3 + 2];
|
||||
let tangent_in_end = keyframes[(step_start + 1) * 3];
|
||||
let value_end = keyframes[(step_start + 1) * 3 + 1];
|
||||
let result = cubic_spline_interpolation(
|
||||
value_start,
|
||||
tangent_out_start,
|
||||
tangent_in_end,
|
||||
value_end,
|
||||
lerp,
|
||||
duration,
|
||||
);
|
||||
transform.scale = transform.scale.lerp(result, weight);
|
||||
}
|
||||
|
||||
(Interpolation::Step, Keyframes::Weights(keyframes)) => {
|
||||
let Some(ref mut morphs) = target_context.morph_weights else {
|
||||
return;
|
||||
};
|
||||
|
||||
let target_count = morphs.weights().len();
|
||||
let morph_start = get_keyframe(target_count, keyframes, step_start);
|
||||
lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight);
|
||||
}
|
||||
}
|
||||
(Interpolation::Linear, Keyframes::Weights(keyframes)) => {
|
||||
if let Some(morphs) = morphs {
|
||||
|
||||
(Interpolation::Linear, Keyframes::Weights(keyframes)) => {
|
||||
let Some(ref mut morphs) = target_context.morph_weights else {
|
||||
return;
|
||||
};
|
||||
|
||||
let target_count = morphs.weights().len();
|
||||
let morph_start = get_keyframe(target_count, keyframes, step_start);
|
||||
let morph_end = get_keyframe(target_count, keyframes, step_start + 1);
|
||||
|
@ -909,9 +941,12 @@ fn apply_keyframe(
|
|||
.map(|(a, b)| a.lerp(*b, lerp));
|
||||
lerp_morph_weights(morphs.weights_mut(), result, weight);
|
||||
}
|
||||
}
|
||||
(Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => {
|
||||
if let Some(morphs) = morphs {
|
||||
|
||||
(Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => {
|
||||
let Some(ref mut morphs) = target_context.morph_weights else {
|
||||
return;
|
||||
};
|
||||
|
||||
let target_count = morphs.weights().len();
|
||||
let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1);
|
||||
let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2);
|
||||
|
@ -940,30 +975,34 @@ fn apply_keyframe(
|
|||
}
|
||||
}
|
||||
|
||||
fn update_transitions(player: &mut AnimationPlayer, time: &Time) {
|
||||
player.transitions.retain_mut(|animation| {
|
||||
animation.current_weight -= animation.weight_decline_per_sec * time.delta_seconds();
|
||||
animation.current_weight > 0.0
|
||||
});
|
||||
impl AnimationTargetId {
|
||||
/// Creates a new [`AnimationTargetId`] by hashing a list of names.
|
||||
///
|
||||
/// Typically, this will be the path from the animation root to the
|
||||
/// animation target (e.g. bone) that is to be animated.
|
||||
pub fn from_names<'a>(names: impl Iterator<Item = &'a Name>) -> Self {
|
||||
let mut sha1 = Sha1::new();
|
||||
sha1.update(ANIMATION_TARGET_NAMESPACE.as_bytes());
|
||||
names.for_each(|name| sha1.update(name.as_bytes()));
|
||||
let hash = sha1.digest().bytes()[0..16].try_into().unwrap();
|
||||
Self(*uuid::Builder::from_sha1_bytes(hash).as_uuid())
|
||||
}
|
||||
|
||||
/// Creates a new [`AnimationTargetId`] by hashing a single name.
|
||||
pub fn from_name(name: &Name) -> Self {
|
||||
Self::from_names(iter::once(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds animation support to an app
|
||||
#[derive(Default)]
|
||||
pub struct AnimationPlugin;
|
||||
impl From<&Name> for AnimationTargetId {
|
||||
fn from(name: &Name) -> Self {
|
||||
AnimationTargetId::from_name(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset::<AnimationClip>()
|
||||
.register_asset_reflect::<AnimationClip>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.register_type::<VariableCurve>()
|
||||
.register_type::<Vec<VariableCurve>>()
|
||||
.register_type::<Interpolation>()
|
||||
.register_type::<Keyframes>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
animation_player.before(TransformSystem::TransformPropagate),
|
||||
);
|
||||
impl MapEntities for AnimationTarget {
|
||||
fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
|
||||
self.player = entity_mapper.map_entity(self.player);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{vertex_attributes::convert_attribute, Gltf, GltfExtras, GltfNode};
|
||||
use bevy_animation::{AnimationTarget, AnimationTargetId};
|
||||
use bevy_asset::{
|
||||
io::Reader, AssetLoadError, AssetLoader, AsyncReadExt, Handle, LoadContext, ReadAssetBytesError,
|
||||
};
|
||||
|
@ -34,7 +35,10 @@ use bevy_scene::Scene;
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy_tasks::IoTaskPool;
|
||||
use bevy_transform::components::Transform;
|
||||
use bevy_utils::{HashMap, HashSet};
|
||||
use bevy_utils::{
|
||||
smallvec::{smallvec, SmallVec},
|
||||
HashMap, HashSet,
|
||||
};
|
||||
use gltf::{
|
||||
accessor::Iter,
|
||||
mesh::{util::ReadIndices, Mode},
|
||||
|
@ -259,11 +263,9 @@ async fn load_gltf<'a, 'b, 'c>(
|
|||
};
|
||||
|
||||
if let Some((root_index, path)) = paths.get(&node.index()) {
|
||||
animation_roots.insert(root_index);
|
||||
animation_clip.add_curve_to_path(
|
||||
bevy_animation::EntityPath {
|
||||
parts: path.clone(),
|
||||
},
|
||||
animation_roots.insert(*root_index);
|
||||
animation_clip.add_curve_to_target(
|
||||
AnimationTargetId::from_names(path.iter()),
|
||||
bevy_animation::VariableCurve {
|
||||
keyframe_timestamps,
|
||||
keyframes,
|
||||
|
@ -590,6 +592,8 @@ async fn load_gltf<'a, 'b, 'c>(
|
|||
&mut entity_to_skin_index_map,
|
||||
&mut active_camera_found,
|
||||
&Transform::default(),
|
||||
&animation_roots,
|
||||
None,
|
||||
);
|
||||
if result.is_err() {
|
||||
err = Some(result);
|
||||
|
@ -934,6 +938,8 @@ fn load_node(
|
|||
entity_to_skin_index_map: &mut EntityHashMap<usize>,
|
||||
active_camera_found: &mut bool,
|
||||
parent_transform: &Transform,
|
||||
animation_roots: &HashSet<usize>,
|
||||
mut animation_context: Option<AnimationContext>,
|
||||
) -> Result<(), GltfError> {
|
||||
let mut gltf_error = None;
|
||||
let transform = node_transform(gltf_node);
|
||||
|
@ -947,7 +953,27 @@ fn load_node(
|
|||
let is_scale_inverted = world_transform.scale.is_negative_bitmask().count_ones() & 1 == 1;
|
||||
let mut node = world_builder.spawn(SpatialBundle::from(transform));
|
||||
|
||||
node.insert(node_name(gltf_node));
|
||||
let name = node_name(gltf_node);
|
||||
node.insert(name.clone());
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
if animation_context.is_none() && animation_roots.contains(&gltf_node.index()) {
|
||||
// This is an animation root. Make a new animation context.
|
||||
animation_context = Some(AnimationContext {
|
||||
root: node.id(),
|
||||
path: smallvec![],
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
if let Some(ref mut animation_context) = animation_context {
|
||||
animation_context.path.push(name);
|
||||
|
||||
node.insert(AnimationTarget {
|
||||
id: AnimationTargetId::from_names(animation_context.path.iter()),
|
||||
player: animation_context.root,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(extras) = gltf_node.extras() {
|
||||
node.insert(GltfExtras {
|
||||
|
@ -1161,6 +1187,8 @@ fn load_node(
|
|||
entity_to_skin_index_map,
|
||||
active_camera_found,
|
||||
&world_transform,
|
||||
animation_roots,
|
||||
animation_context.clone(),
|
||||
) {
|
||||
gltf_error = Some(err);
|
||||
return;
|
||||
|
@ -1517,6 +1545,22 @@ struct MorphTargetNames {
|
|||
pub target_names: Vec<String>,
|
||||
}
|
||||
|
||||
// A helper structure for `load_node` that contains information about the
|
||||
// nearest ancestor animation root.
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
#[derive(Clone)]
|
||||
struct AnimationContext {
|
||||
// The nearest ancestor animation root.
|
||||
root: Entity,
|
||||
// The path to the animation root. This is used for constructing the
|
||||
// animation target UUIDs.
|
||||
path: SmallVec<[Name; 8]>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "bevy_animation"))]
|
||||
#[derive(Clone)]
|
||||
struct AnimationContext;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
|
|
@ -149,7 +149,7 @@ impl Plugin for IgnoreAmbiguitiesPlugin {
|
|||
#[cfg(all(feature = "bevy_animation", feature = "bevy_ui"))]
|
||||
app.ignore_ambiguity(
|
||||
bevy_app::PostUpdate,
|
||||
bevy_animation::animation_player,
|
||||
bevy_animation::advance_animations,
|
||||
bevy_ui::ui_layout_system,
|
||||
);
|
||||
|
||||
|
|
|
@ -617,6 +617,7 @@ impl_type_path!(::std::collections::HashMap<K, V, S>);
|
|||
|
||||
impl_reflect_for_hashmap!(bevy_utils::hashbrown::HashMap<K, V, S>);
|
||||
impl_type_path!(::bevy_utils::hashbrown::hash_map::DefaultHashBuilder);
|
||||
impl_type_path!(::bevy_utils::NoOpHash);
|
||||
impl_type_path!(::bevy_utils::hashbrown::HashMap<K, V, S>);
|
||||
|
||||
impl<T: Reflect + TypePath, const N: usize> Array for [T; N] {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Helpers for working with Bevy reflection.
|
||||
|
||||
use crate::TypeInfo;
|
||||
use bevy_utils::{FixedState, NoOpTypeIdHash, TypeIdMap};
|
||||
use bevy_utils::{FixedState, NoOpHash, TypeIdMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
hash::BuildHasher,
|
||||
|
@ -205,7 +205,7 @@ pub type GenericTypePathCell = GenericTypeCell<TypePathComponent>;
|
|||
impl<T: TypedProperty> GenericTypeCell<T> {
|
||||
/// Initialize a [`GenericTypeCell`] for generic types.
|
||||
pub const fn new() -> Self {
|
||||
Self(RwLock::new(TypeIdMap::with_hasher(NoOpTypeIdHash)))
|
||||
Self(RwLock::new(TypeIdMap::with_hasher(NoOpHash)))
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`TypedProperty`] stored in the cell.
|
||||
|
|
|
@ -265,6 +265,125 @@ impl<K: Hash + Eq + PartialEq + Clone, V> PreHashMapExt<K, V> for PreHashMap<K,
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`BuildHasher`] that results in a [`EntityHasher`].
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EntityHash;
|
||||
|
||||
impl BuildHasher for EntityHash {
|
||||
type Hasher = EntityHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
EntityHasher::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A very fast hash that is only designed to work on generational indices
|
||||
/// like `Entity`. It will panic if attempting to hash a type containing
|
||||
/// non-u64 fields.
|
||||
///
|
||||
/// This is heavily optimized for typical cases, where you have mostly live
|
||||
/// entities, and works particularly well for contiguous indices.
|
||||
///
|
||||
/// If you have an unusual case -- say all your indices are multiples of 256
|
||||
/// or most of the entities are dead generations -- then you might want also to
|
||||
/// try [`AHasher`] for a slower hash computation but fewer lookup conflicts.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EntityHasher {
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl Hasher for EntityHasher {
|
||||
#[inline]
|
||||
fn finish(&self) -> u64 {
|
||||
self.hash
|
||||
}
|
||||
|
||||
fn write(&mut self, _bytes: &[u8]) {
|
||||
panic!("can only hash u64 using EntityHasher");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_u64(&mut self, bits: u64) {
|
||||
// SwissTable (and thus `hashbrown`) cares about two things from the hash:
|
||||
// - H1: low bits (masked by `2ⁿ-1`) to pick the slot in which to store the item
|
||||
// - H2: high 7 bits are used to SIMD optimize hash collision probing
|
||||
// For more see <https://abseil.io/about/design/swisstables#metadata-layout>
|
||||
|
||||
// This hash function assumes that the entity ids are still well-distributed,
|
||||
// so for H1 leaves the entity id alone in the low bits so that id locality
|
||||
// will also give memory locality for things spawned together.
|
||||
// For H2, take advantage of the fact that while multiplication doesn't
|
||||
// spread entropy to the low bits, it's incredibly good at spreading it
|
||||
// upward, which is exactly where we need it the most.
|
||||
|
||||
// While this does include the generation in the output, it doesn't do so
|
||||
// *usefully*. H1 won't care until you have over 3 billion entities in
|
||||
// the table, and H2 won't care until something hits generation 33 million.
|
||||
// Thus the comment suggesting that this is best for live entities,
|
||||
// where there won't be generation conflicts where it would matter.
|
||||
|
||||
// The high 32 bits of this are ⅟φ for Fibonacci hashing. That works
|
||||
// particularly well for hashing for the same reason as described in
|
||||
// <https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/>
|
||||
// It loses no information because it has a modular inverse.
|
||||
// (Specifically, `0x144c_bc89_u32 * 0x9e37_79b9_u32 == 1`.)
|
||||
//
|
||||
// The low 32 bits make that part of the just product a pass-through.
|
||||
const UPPER_PHI: u64 = 0x9e37_79b9_0000_0001;
|
||||
|
||||
// This is `(MAGIC * index + generation) << 32 + index`, in a single instruction.
|
||||
self.hash = bits.wrapping_mul(UPPER_PHI);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`HashMap`] pre-configured to use [`EntityHash`] hashing.
|
||||
/// Iteration order only depends on the order of insertions and deletions.
|
||||
pub type EntityHashMap<K, V> = hashbrown::HashMap<K, V, EntityHash>;
|
||||
|
||||
/// A [`HashSet`] pre-configured to use [`EntityHash`] hashing.
|
||||
/// Iteration order only depends on the order of insertions and deletions.
|
||||
pub type EntityHashSet<T> = hashbrown::HashSet<T, EntityHash>;
|
||||
|
||||
/// A specialized hashmap type with Key of [`TypeId`]
|
||||
/// Iteration order only depends on the order of insertions and deletions.
|
||||
pub type TypeIdMap<V> = hashbrown::HashMap<TypeId, V, NoOpHash>;
|
||||
|
||||
/// [`BuildHasher`] for types that already contain a high-quality hash.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct NoOpHash;
|
||||
|
||||
impl BuildHasher for NoOpHash {
|
||||
type Hasher = NoOpHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
NoOpHasher(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct NoOpHasher(u64);
|
||||
|
||||
// This is for types that already contain a high-quality hash and want to skip
|
||||
// re-hashing that hash.
|
||||
impl std::hash::Hasher for NoOpHasher {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
// This should never be called by consumers. Prefer to call `write_u64` instead.
|
||||
// Don't break applications (slower fallback, just check in test):
|
||||
self.0 = bytes.iter().fold(self.0, |hash, b| {
|
||||
hash.rotate_left(8).wrapping_add(*b as u64)
|
||||
});
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_u64(&mut self, i: u64) {
|
||||
self.0 = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which calls a function when dropped.
|
||||
/// This can be used to ensure that cleanup code is run even in case of a panic.
|
||||
///
|
||||
|
@ -320,45 +439,6 @@ impl<F: FnOnce()> Drop for OnDrop<F> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A specialized hashmap type with Key of [`TypeId`]
|
||||
/// Iteration order only depends on the order of insertions and deletions.
|
||||
pub type TypeIdMap<V> = hashbrown::HashMap<TypeId, V, NoOpTypeIdHash>;
|
||||
|
||||
/// [`BuildHasher`] for [`TypeId`]s.
|
||||
#[derive(Default)]
|
||||
pub struct NoOpTypeIdHash;
|
||||
|
||||
impl BuildHasher for NoOpTypeIdHash {
|
||||
type Hasher = NoOpTypeIdHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
NoOpTypeIdHasher(0)
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[derive(Default)]
|
||||
pub struct NoOpTypeIdHasher(pub u64);
|
||||
|
||||
// TypeId already contains a high-quality hash, so skip re-hashing that hash.
|
||||
impl std::hash::Hasher for NoOpTypeIdHasher {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
// This will never be called: TypeId always just calls write_u64 once!
|
||||
// This is a known trick and unlikely to change, but isn't officially guaranteed.
|
||||
// Don't break applications (slower fallback, just check in test):
|
||||
self.0 = bytes.iter().fold(self.0, |hash, b| {
|
||||
hash.rotate_left(8).wrapping_add(*b as u64)
|
||||
});
|
||||
}
|
||||
|
||||
fn write_u64(&mut self, i: u64) {
|
||||
self.0 = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the [`tracing::info!`] macro on a value.
|
||||
pub fn info<T: Debug>(data: T) {
|
||||
tracing::info!("{:?}", data);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use bevy::animation::{AnimationTarget, AnimationTargetId};
|
||||
use bevy::prelude::*;
|
||||
|
||||
fn main() {
|
||||
|
@ -27,7 +28,8 @@ fn setup(
|
|||
..default()
|
||||
});
|
||||
|
||||
// The animation API uses the `Name` component to target entities
|
||||
// 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");
|
||||
|
@ -35,10 +37,9 @@ fn setup(
|
|||
// Creating the animation
|
||||
let mut animation = AnimationClip::default();
|
||||
// A curve can modify a single part of a transform, here the translation
|
||||
animation.add_curve_to_path(
|
||||
EntityPath {
|
||||
parts: vec![planet.clone()],
|
||||
},
|
||||
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![
|
||||
|
@ -56,10 +57,10 @@ fn setup(
|
|||
// 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
|
||||
animation.add_curve_to_path(
|
||||
EntityPath {
|
||||
parts: vec![planet.clone(), orbit_controller.clone()],
|
||||
},
|
||||
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![
|
||||
|
@ -75,10 +76,11 @@ fn setup(
|
|||
// 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
|
||||
animation.add_curve_to_path(
|
||||
EntityPath {
|
||||
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
|
||||
},
|
||||
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![
|
||||
|
@ -96,10 +98,10 @@ fn setup(
|
|||
},
|
||||
);
|
||||
// There can be more than one curve targeting the same entity path
|
||||
animation.add_curve_to_path(
|
||||
EntityPath {
|
||||
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
|
||||
},
|
||||
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![
|
||||
|
@ -119,23 +121,33 @@ fn setup(
|
|||
|
||||
// Create the scene that will be animated
|
||||
// First entity is the planet
|
||||
commands
|
||||
let planet_entity = commands
|
||||
.spawn((
|
||||
PbrBundle {
|
||||
mesh: meshes.add(Sphere::default()),
|
||||
material: materials.add(Color::rgb(0.8, 0.7, 0.6)),
|
||||
..default()
|
||||
},
|
||||
// Add the Name component, and the animation player
|
||||
// Add the animation player
|
||||
planet,
|
||||
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,
|
||||
// Add the Name component
|
||||
orbit_controller,
|
||||
AnimationTarget {
|
||||
id: orbit_controller_animation_target_id,
|
||||
player: planet_entity,
|
||||
},
|
||||
))
|
||||
.with_children(|p| {
|
||||
// The satellite, placed at a distance of the planet
|
||||
|
@ -146,7 +158,10 @@ fn setup(
|
|||
material: materials.add(Color::rgb(0.3, 0.9, 0.3)),
|
||||
..default()
|
||||
},
|
||||
// Add the Name component
|
||||
AnimationTarget {
|
||||
id: satellite_animation_target_id,
|
||||
player: planet_entity,
|
||||
},
|
||||
satellite,
|
||||
));
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! Control animations of entities in the loaded scene.
|
||||
use bevy::{gltf::Gltf, prelude::*};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bevy::{animation::AnimationTarget, ecs::entity::EntityHashMap, gltf::Gltf, prelude::*};
|
||||
|
||||
use crate::scene_viewer_plugin::SceneHandle;
|
||||
|
||||
|
@ -24,14 +26,14 @@ impl Clips {
|
|||
}
|
||||
}
|
||||
|
||||
/// Read [`AnimationClip`]s from the loaded [`Gltf`] and assign them to the
|
||||
/// entities they control. [`AnimationClip`]s control specific entities, and
|
||||
/// trying to play them on an [`AnimationPlayer`] controlling a different
|
||||
/// entities will result in odd animations, we take extra care to store
|
||||
/// animation clips for given entities in the [`Clips`] component we defined
|
||||
/// earlier in this file.
|
||||
/// Automatically assign [`AnimationClip`]s to [`AnimationPlayer`] and play
|
||||
/// them, if the clips refer to descendants of the animation player (which is
|
||||
/// the common case).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn assign_clips(
|
||||
mut players: Query<(Entity, &mut AnimationPlayer, &Name)>,
|
||||
mut players: Query<&mut AnimationPlayer>,
|
||||
targets: Query<(Entity, &AnimationTarget)>,
|
||||
parents: Query<&Parent>,
|
||||
scene_handle: Res<SceneHandle>,
|
||||
clips: Res<Assets<AnimationClip>>,
|
||||
gltf_assets: Res<Assets<Gltf>>,
|
||||
|
@ -44,24 +46,97 @@ fn assign_clips(
|
|||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let gltf = gltf_assets.get(&scene_handle.gltf_handle).unwrap();
|
||||
let animations = &gltf.animations;
|
||||
if !animations.is_empty() {
|
||||
let count = animations.len();
|
||||
let plural = if count == 1 { "" } else { "s" };
|
||||
info!("Found {} animation{plural}", animations.len());
|
||||
let names: Vec<_> = gltf.named_animations.keys().collect();
|
||||
info!("Animation names: {names:?}");
|
||||
if animations.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (entity, mut player, name) in &mut players {
|
||||
let clips = clips
|
||||
.iter()
|
||||
.filter_map(|(k, v)| v.compatible_with(name).then_some(k))
|
||||
.map(|id| assets.get_id_handle(id).unwrap())
|
||||
.collect();
|
||||
|
||||
let count = animations.len();
|
||||
let plural = if count == 1 { "" } else { "s" };
|
||||
info!("Found {} animation{plural}", animations.len());
|
||||
let names: Vec<_> = gltf.named_animations.keys().collect();
|
||||
info!("Animation names: {names:?}");
|
||||
|
||||
// Map animation target IDs to entities.
|
||||
let animation_target_id_to_entity: HashMap<_, _> = targets
|
||||
.iter()
|
||||
.map(|(entity, target)| (target.id, entity))
|
||||
.collect();
|
||||
|
||||
// Build up a list of all animation clips that belong to each player. A clip
|
||||
// is considered to belong to an animation player if all targets of the clip
|
||||
// refer to entities whose nearest ancestor player is that animation player.
|
||||
|
||||
let mut player_to_clips: EntityHashMap<Vec<_>> = EntityHashMap::default();
|
||||
|
||||
for (clip_id, clip) in clips.iter() {
|
||||
let mut ancestor_player = None;
|
||||
for target_id in clip.curves().keys() {
|
||||
// If the animation clip refers to entities that aren't present in
|
||||
// the scene, bail.
|
||||
let Some(&target) = animation_target_id_to_entity.get(target_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Find the nearest ancestor animation player.
|
||||
let mut current = Some(target);
|
||||
while let Some(entity) = current {
|
||||
if players.contains(entity) {
|
||||
match ancestor_player {
|
||||
None => {
|
||||
// If we haven't found a player yet, record the one
|
||||
// we found.
|
||||
ancestor_player = Some(entity);
|
||||
}
|
||||
Some(ancestor) => {
|
||||
// If we have found a player, then make sure it's
|
||||
// the same player we located before.
|
||||
if ancestor != entity {
|
||||
// It's a different player. Bail.
|
||||
ancestor_player = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go to the next parent.
|
||||
current = parents.get(entity).ok().map(|parent| parent.get());
|
||||
}
|
||||
}
|
||||
|
||||
let Some(ancestor_player) = ancestor_player else {
|
||||
warn!(
|
||||
"Unexpected animation hierarchy for animation clip {:?}; ignoring.",
|
||||
clip_id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(clip_handle) = assets.get_id_handle(clip_id) else {
|
||||
warn!("Clip {:?} wasn't loaded.", clip_id);
|
||||
continue;
|
||||
};
|
||||
|
||||
player_to_clips
|
||||
.entry(ancestor_player)
|
||||
.or_default()
|
||||
.push(clip_handle);
|
||||
}
|
||||
|
||||
// Now that we've built up a list of all clips that belong to each player,
|
||||
// package them up into a `Clips` component, play the first such animation,
|
||||
// and add that component to the player.
|
||||
for (player_entity, clips) in player_to_clips {
|
||||
let Ok(mut player) = players.get_mut(player_entity) else {
|
||||
warn!("Animation targets referenced a nonexistent player. This shouldn't happen.");
|
||||
continue;
|
||||
};
|
||||
let animations = Clips::new(clips);
|
||||
player.play(animations.current()).repeat();
|
||||
commands.entity(entity).insert(animations);
|
||||
commands.entity(player_entity).insert(animations);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue