Skip to content

Commit 8d19045

Browse files
committed
Parallelize forward kinematics animation systems (#6785)
# Objective Speed up animation by leveraging all threads in `ComputeTaskPool`. ## Solution This PR parallelizes animation sampling across all threads. To ensure that this is safely done, all animation is predicated with an ancestor query to ensure that there is no conflicting `AnimationPlayer` above each animated hierarchy that may cause this to alias. Unlike the RFC, this does not add support for reflect based "animate anything", but only extends the existing `AnimationPlayer` to support high numbers of animated characters on screen at once. ## Performance This cuts `many_foxes`'s frame time on my machine by a full millisecond, from 7.49ms to 6.5ms. (yellow is this PR, red is main). ![image](https://user-images.githubusercontent.com/3137680/204219698-ffe0136c-5e9b-436f-b8d9-b23f0b8d7d36.png) --- ## Changelog Changed: Animation sampling now runs fully multi-threaded using threads from `ComputeTaskPool`. Changed: `AnimationPlayer` that are on a child or descendant of another entity with another player will no longer be run.
1 parent df3673f commit 8d19045

File tree

1 file changed

+189
-100
lines changed
  • crates/bevy_animation/src

1 file changed

+189
-100
lines changed

crates/bevy_animation/src/lib.rs

+189-100
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ use bevy_ecs::{
1111
change_detection::DetectChanges,
1212
entity::Entity,
1313
prelude::Component,
14+
query::With,
1415
reflect::ReflectComponent,
1516
schedule::IntoSystemDescriptor,
1617
system::{Query, Res},
1718
};
18-
use bevy_hierarchy::Children;
19+
use bevy_hierarchy::{Children, Parent};
1920
use bevy_math::{Quat, Vec3};
2021
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
2122
use bevy_time::Time;
@@ -63,17 +64,34 @@ pub struct EntityPath {
6364
#[derive(Reflect, FromReflect, Clone, TypeUuid, Debug, Default)]
6465
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
6566
pub struct AnimationClip {
66-
curves: HashMap<EntityPath, Vec<VariableCurve>>,
67+
curves: Vec<Vec<VariableCurve>>,
68+
paths: HashMap<EntityPath, usize>,
6769
duration: f32,
6870
}
6971

7072
impl AnimationClip {
7173
#[inline]
72-
/// Hashmap of the [`VariableCurve`]s per [`EntityPath`].
73-
pub fn curves(&self) -> &HashMap<EntityPath, Vec<VariableCurve>> {
74+
/// [`VariableCurve`]s for each bone. Indexed by the bone ID.
75+
pub fn curves(&self) -> &Vec<Vec<VariableCurve>> {
7476
&self.curves
7577
}
7678

79+
/// Gets the curves for a bone.
80+
///
81+
/// Returns `None` if the bone is invalid.
82+
#[inline]
83+
pub fn get_curves(&self, bone_id: usize) -> Option<&'_ Vec<VariableCurve>> {
84+
self.curves.get(bone_id)
85+
}
86+
87+
/// Gets the curves by it's [`EntityPath`].
88+
///
89+
/// Returns `None` if the bone is invalid.
90+
#[inline]
91+
pub fn get_curves_by_path(&self, path: &EntityPath) -> Option<&'_ Vec<VariableCurve>> {
92+
self.paths.get(path).and_then(|id| self.curves.get(*id))
93+
}
94+
7795
/// Duration of the clip, represented in seconds
7896
#[inline]
7997
pub fn duration(&self) -> f32 {
@@ -86,7 +104,13 @@ impl AnimationClip {
86104
self.duration = self
87105
.duration
88106
.max(*curve.keyframe_timestamps.last().unwrap_or(&0.0));
89-
self.curves.entry(path).or_default().push(curve);
107+
if let Some(bone_id) = self.paths.get(&path) {
108+
self.curves[*bone_id].push(curve);
109+
} else {
110+
let idx = self.curves.len();
111+
self.curves.push(vec![curve]);
112+
self.paths.insert(path, idx);
113+
}
90114
}
91115
}
92116

@@ -99,6 +123,7 @@ pub struct AnimationPlayer {
99123
speed: f32,
100124
elapsed: f32,
101125
animation_clip: Handle<AnimationClip>,
126+
path_cache: Vec<Vec<Option<Entity>>>,
102127
}
103128

104129
impl Default for AnimationPlayer {
@@ -109,6 +134,7 @@ impl Default for AnimationPlayer {
109134
speed: 1.0,
110135
elapsed: 0.0,
111136
animation_clip: Default::default(),
137+
path_cache: Vec::new(),
112138
}
113139
}
114140
}
@@ -181,117 +207,180 @@ impl AnimationPlayer {
181207
}
182208
}
183209

210+
fn find_bone(
211+
root: Entity,
212+
path: &EntityPath,
213+
children: &Query<&Children>,
214+
names: &Query<&Name>,
215+
path_cache: &mut Vec<Option<Entity>>,
216+
) -> Option<Entity> {
217+
// PERF: finding the target entity can be optimised
218+
let mut current_entity = root;
219+
path_cache.resize(path.parts.len(), None);
220+
// Ignore the first name, it is the root node which we already have
221+
for (idx, part) in path.parts.iter().enumerate().skip(1) {
222+
let mut found = false;
223+
let children = children.get(current_entity).ok()?;
224+
if let Some(cached) = path_cache[idx] {
225+
if children.contains(&cached) {
226+
if let Ok(name) = names.get(cached) {
227+
if name == part {
228+
current_entity = cached;
229+
found = true;
230+
}
231+
}
232+
}
233+
}
234+
if !found {
235+
for child in children.deref() {
236+
if let Ok(name) = names.get(*child) {
237+
if name == part {
238+
// Found a children with the right name, continue to the next part
239+
current_entity = *child;
240+
path_cache[idx] = Some(*child);
241+
found = true;
242+
break;
243+
}
244+
}
245+
}
246+
}
247+
if !found {
248+
warn!("Entity not found for path {:?} on part {:?}", path, part);
249+
return None;
250+
}
251+
}
252+
Some(current_entity)
253+
}
254+
255+
/// Verify that there are no ancestors of a given entity that have an `AnimationPlayer`.
256+
fn verify_no_ancestor_player(
257+
player_parent: Option<&Parent>,
258+
parents: &Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
259+
) -> bool {
260+
let Some(mut current) = player_parent.map(Parent::get) else { return true };
261+
loop {
262+
let Ok((maybe_player, parent)) = parents.get(current) else { return true };
263+
if maybe_player.is_some() {
264+
return false;
265+
}
266+
if let Some(parent) = parent {
267+
current = parent.get();
268+
} else {
269+
return true;
270+
}
271+
}
272+
}
273+
184274
/// System that will play all animations, using any entity with a [`AnimationPlayer`]
185275
/// and a [`Handle<AnimationClip>`] as an animation root
186276
pub fn animation_player(
187277
time: Res<Time>,
188278
animations: Res<Assets<AnimationClip>>,
189-
mut animation_players: Query<(Entity, &mut AnimationPlayer)>,
190-
names: Query<&Name>,
191-
mut transforms: Query<&mut Transform>,
192279
children: Query<&Children>,
280+
names: Query<&Name>,
281+
transforms: Query<&mut Transform>,
282+
parents: Query<(Option<With<AnimationPlayer>>, Option<&Parent>)>,
283+
mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>,
193284
) {
194-
for (entity, mut player) in &mut animation_players {
195-
if let Some(animation_clip) = animations.get(&player.animation_clip) {
196-
// Continue if paused unless the `AnimationPlayer` was changed
197-
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
198-
if player.paused && !player.is_changed() {
199-
continue;
200-
}
201-
if !player.paused {
202-
player.elapsed += time.delta_seconds() * player.speed;
203-
}
204-
let mut elapsed = player.elapsed;
205-
if player.repeat {
206-
elapsed %= animation_clip.duration;
207-
}
208-
if elapsed < 0.0 {
209-
elapsed += animation_clip.duration;
210-
}
211-
'entity: for (path, curves) in &animation_clip.curves {
212-
// PERF: finding the target entity can be optimised
213-
let mut current_entity = entity;
214-
// Ignore the first name, it is the root node which we already have
215-
for part in path.parts.iter().skip(1) {
216-
let mut found = false;
217-
if let Ok(children) = children.get(current_entity) {
218-
for child in children.deref() {
219-
if let Ok(name) = names.get(*child) {
220-
if name == part {
221-
// Found a children with the right name, continue to the next part
222-
current_entity = *child;
223-
found = true;
224-
break;
225-
}
226-
}
285+
animation_players.par_for_each_mut(10, |(root, maybe_parent, mut player)| {
286+
let Some(animation_clip) = animations.get(&player.animation_clip) else { return };
287+
// Continue if paused unless the `AnimationPlayer` was changed
288+
// This allow the animation to still be updated if the player.elapsed field was manually updated in pause
289+
if player.paused && !player.is_changed() {
290+
return;
291+
}
292+
if !player.paused {
293+
player.elapsed += time.delta_seconds() * player.speed;
294+
}
295+
let mut elapsed = player.elapsed;
296+
if player.repeat {
297+
elapsed %= animation_clip.duration;
298+
}
299+
if elapsed < 0.0 {
300+
elapsed += animation_clip.duration;
301+
}
302+
if player.path_cache.len() != animation_clip.paths.len() {
303+
player.path_cache = vec![Vec::new(); animation_clip.paths.len()];
304+
}
305+
if !verify_no_ancestor_player(maybe_parent, &parents) {
306+
warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root);
307+
return;
308+
}
309+
for (path, bone_id) in &animation_clip.paths {
310+
let cached_path = &mut player.path_cache[*bone_id];
311+
let curves = animation_clip.get_curves(*bone_id).unwrap();
312+
let Some(target) = find_bone(root, path, &children, &names, cached_path) else { continue };
313+
// SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias
314+
// any of their descendant Transforms.
315+
//
316+
// The system scheduler prevents any other system from mutating Transforms at the same time,
317+
// so the only way this fetch can alias is if two AnimationPlayers are targetting the same bone.
318+
// This can only happen if there are two or more AnimationPlayers are ancestors to the same
319+
// entities. By verifying that there is no other AnimationPlayer in the ancestors of a
320+
// running AnimationPlayer before animating any entity, this fetch cannot alias.
321+
//
322+
// This means only the AnimationPlayers closest to the root of the hierarchy will be able
323+
// to run their animation. Any players in the children or descendants will log a warning
324+
// and do nothing.
325+
let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { continue };
326+
for curve in curves {
327+
// Some curves have only one keyframe used to set a transform
328+
if curve.keyframe_timestamps.len() == 1 {
329+
match &curve.keyframes {
330+
Keyframes::Rotation(keyframes) => transform.rotation = keyframes[0],
331+
Keyframes::Translation(keyframes) => {
332+
transform.translation = keyframes[0];
227333
}
334+
Keyframes::Scale(keyframes) => transform.scale = keyframes[0],
228335
}
229-
if !found {
230-
warn!("Entity not found for path {:?} on part {:?}", path, part);
231-
continue 'entity;
232-
}
336+
continue;
233337
}
234-
if let Ok(mut transform) = transforms.get_mut(current_entity) {
235-
for curve in curves {
236-
// Some curves have only one keyframe used to set a transform
237-
if curve.keyframe_timestamps.len() == 1 {
238-
match &curve.keyframes {
239-
Keyframes::Rotation(keyframes) => transform.rotation = keyframes[0],
240-
Keyframes::Translation(keyframes) => {
241-
transform.translation = keyframes[0];
242-
}
243-
Keyframes::Scale(keyframes) => transform.scale = keyframes[0],
244-
}
245-
continue;
246-
}
247338

248-
// Find the current keyframe
249-
// PERF: finding the current keyframe can be optimised
250-
let step_start = match curve
251-
.keyframe_timestamps
252-
.binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap())
253-
{
254-
Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
255-
Ok(i) => i,
256-
Err(0) => continue, // this curve isn't started yet
257-
Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
258-
Err(i) => i - 1,
259-
};
260-
let ts_start = curve.keyframe_timestamps[step_start];
261-
let ts_end = curve.keyframe_timestamps[step_start + 1];
262-
let lerp = (elapsed - ts_start) / (ts_end - ts_start);
263-
264-
// Apply the keyframe
265-
match &curve.keyframes {
266-
Keyframes::Rotation(keyframes) => {
267-
let rot_start = keyframes[step_start];
268-
let mut rot_end = keyframes[step_start + 1];
269-
// Choose the smallest angle for the rotation
270-
if rot_end.dot(rot_start) < 0.0 {
271-
rot_end = -rot_end;
272-
}
273-
// Rotations are using a spherical linear interpolation
274-
transform.rotation =
275-
rot_start.normalize().slerp(rot_end.normalize(), lerp);
276-
}
277-
Keyframes::Translation(keyframes) => {
278-
let translation_start = keyframes[step_start];
279-
let translation_end = keyframes[step_start + 1];
280-
let result = translation_start.lerp(translation_end, lerp);
281-
transform.translation = result;
282-
}
283-
Keyframes::Scale(keyframes) => {
284-
let scale_start = keyframes[step_start];
285-
let scale_end = keyframes[step_start + 1];
286-
let result = scale_start.lerp(scale_end, lerp);
287-
transform.scale = result;
288-
}
339+
// Find the current keyframe
340+
// PERF: finding the current keyframe can be optimised
341+
let step_start = match curve
342+
.keyframe_timestamps
343+
.binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap())
344+
{
345+
Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
346+
Ok(i) => i,
347+
Err(0) => continue, // this curve isn't started yet
348+
Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished
349+
Err(i) => i - 1,
350+
};
351+
let ts_start = curve.keyframe_timestamps[step_start];
352+
let ts_end = curve.keyframe_timestamps[step_start + 1];
353+
let lerp = (elapsed - ts_start) / (ts_end - ts_start);
354+
355+
// Apply the keyframe
356+
match &curve.keyframes {
357+
Keyframes::Rotation(keyframes) => {
358+
let rot_start = keyframes[step_start];
359+
let mut rot_end = keyframes[step_start + 1];
360+
// Choose the smallest angle for the rotation
361+
if rot_end.dot(rot_start) < 0.0 {
362+
rot_end = -rot_end;
289363
}
364+
// Rotations are using a spherical linear interpolation
365+
transform.rotation =
366+
rot_start.normalize().slerp(rot_end.normalize(), lerp);
367+
}
368+
Keyframes::Translation(keyframes) => {
369+
let translation_start = keyframes[step_start];
370+
let translation_end = keyframes[step_start + 1];
371+
let result = translation_start.lerp(translation_end, lerp);
372+
transform.translation = result;
373+
}
374+
Keyframes::Scale(keyframes) => {
375+
let scale_start = keyframes[step_start];
376+
let scale_end = keyframes[step_start + 1];
377+
let result = scale_start.lerp(scale_end, lerp);
378+
transform.scale = result;
290379
}
291380
}
292381
}
293382
}
294-
}
383+
});
295384
}
296385

297386
/// Adds animation support to an app

0 commit comments

Comments
 (0)