Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework animation to be done in two phases. #11707

Merged
merged 35 commits into from
Feb 19, 2024

Conversation

pcwalton
Copy link
Contributor

@pcwalton pcwalton commented Feb 5, 2024

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 AnimationTargets 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 TypeIds, bevy_reflect::utility::NoOpTypeIdHash and bevy_reflect::utility::NoOpTypeIdHasher have been renamed to bevy_reflect::utility::NoOpHash and bevy_reflect::utility::NoOpHasher respectively.

This eliminates all the unsafe code, increases parallelism, and reduces
the number of queries to just two.
Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting aside performance, this is a big win in both safety and maintainability. The documentation still needs to be improved, but it is already so much easier to tell what is going on.

Aside from one new exclusive system I'm not seeing anything (structural) that would make me worry about performance, and even that is a work-around we should eventually be able to remove. I will maybe run some benchmarks next week.

I really really like this.

pcwalton added a commit to pcwalton/bevy that referenced this pull request Feb 5, 2024
This patch places all entity paths into a shared table so that comparing
them is as cheap as a pointer comparison. We don't use the pre-existing
`Interner` type because that leaks all strings added to it, and I was
uncomfortable with leaking names, as Bevy apps might dynamically
generate them. Instead, I reference count all the names and use a linked
list to stitch together paths into a tree. The interner uses a weak hash
set from the [`weak_table`] crate.

This patch is especially helpful for the two-phase animation PR bevyengine#11707,
because two-phase animation gets rid of the cache from name to
animation-specific slot, thus increasing the load on the hash table that
maps paths to bone indices.

Note that the interned table is a global variable behind a `OnceLock`
instead of a resource. This is because it must be accessed from the glTF
`AssetLoader`, which unfortunately has no access to Bevy resources.
pcwalton added a commit to pcwalton/bevy that referenced this pull request Feb 5, 2024
This patch places all entity paths into a shared table so that comparing
them is as cheap as a pointer comparison. We don't use the pre-existing
`Interner` type because that leaks all strings added to it, and I was
uncomfortable with leaking names, as Bevy apps might dynamically
generate them. Instead, I reference count all the names and use a linked
list to stitch together paths into a tree. The interner uses a weak hash
set from the [`weak-table`] crate.

This patch is especially helpful for the two-phase animation PR bevyengine#11707,
because two-phase animation gets rid of the cache from name to
animation-specific slot, thus increasing the load on the hash table that
maps paths to bone indices.

Note that the interned table is a global variable behind a `OnceLock`
instead of a resource. This is because it must be accessed from the glTF
`AssetLoader`, which unfortunately has no access to Bevy resources.

[`weak-table`]: https://crates.io/crates/weak-table
@mockersf
Copy link
Member

mockersf commented Feb 5, 2024

on the many_foxes examples, this PR is slower than main

@pcwalton
Copy link
Contributor Author

pcwalton commented Feb 5, 2024

I would expect the problem to be #11711. I also haven't looked at performance at all.

@pcwalton
Copy link
Contributor Author

pcwalton commented Feb 5, 2024

By the way, I don't know how to implement the Animatable trait, which is an approved RFC, without something like this or an explosion in unsafe code.

@pcwalton
Copy link
Contributor Author

pcwalton commented Feb 5, 2024

I've verified that with #11711 applied on top of this, the many_foxes example is faster than main with this PR. Without #11711, this PR alone is indeed slower because of all the hashing.

Copy link
Contributor

@djeedai djeedai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a few comments but overall looks very good, easier to read and more maintainable. Thanks!

crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_animation/src/lib.rs Outdated Show resolved Hide resolved
@alice-i-cecile alice-i-cecile added C-Performance A change motivated by improving speed, memory usage or compile times C-Code-Quality A section of code that is hard to understand or change A-Animation Make things move and change over time labels Feb 5, 2024
Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Naming could be more consistent. Render target or bone, it's one or the other. Are we going to talk about paths on functions that now accept render target ids?

I'm pretty much sold.

@alice-i-cecile alice-i-cecile added this to the 0.14 milestone Feb 5, 2024
@pcwalton pcwalton marked this pull request as ready for review February 6, 2024 01:09
Copy link
Member

@james7132 james7132 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to leave an early comment that this overall looks good to me now, but I would like to do some profiling to dig into why @NthTensor found little improvement with this change (see here for some potential ideas on what they might be).

@james7132 james7132 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help labels Feb 8, 2024
Copy link
Member

@mockersf mockersf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you restore the behaviour of the animated_transform example?

@mockersf
Copy link
Member

mockersf commented Feb 9, 2024

example morph_targets is not working with this PR. pcwalton#1 fixes it but breaks one of the assumptions of the PR

@mockersf mockersf removed the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Feb 9, 2024
@mockersf
Copy link
Member

mockersf commented Feb 9, 2024

@pcwalton
Copy link
Contributor Author

OK, I allowed AnimationTarget and AnimationPlayer to coexist on the same entity. The fact that they couldn't is a relic of when I was using EntityMut on AnimationTargets, so they would lock out the AnimationPlayers. This is no longer necessary.

@NthTensor
Copy link
Contributor

Both animated_transform and morph_targets seem to be working. @mockersf

@mockersf
Copy link
Member

Both animated_transform and morph_targets seem to be working. @mockersf

animated_transform doesn't behave as before and https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/InterpolationTest doesn't work

@pcwalton
Copy link
Contributor Author

pcwalton commented Feb 19, 2024

@mockersf You were running InterpolationTest through scene_viewer, right? Because the way that scene_viewer plays animations is pretty broken, and the fact that we now use multiple AnimationPlayers in this PR broke it. I'll fix it as part of this PR.

Both of those issues are problems with the examples, not with the Bevy logic.

@pcwalton
Copy link
Contributor Author

pcwalton commented Feb 19, 2024

Those should be working now and should be ready for review and merging afterward now that 0.13 has shipped.

@mockersf
Copy link
Member

@mockersf You were running InterpolationTest through scene_viewer, right?

Yup!

Because the way that scene_viewer plays animations is pretty broken, and the fact that we now use multiple AnimationPlayers in this PR broke it. I'll fix it as part of this PR.

Thanks! It's good practice to fix the examples with the PR that breaks them, it avoids having to investigate without the context.

@mockersf mockersf added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Feb 19, 2024
@mockersf mockersf added this pull request to the merge queue Feb 19, 2024
Merged via the queue into bevyengine:main with commit 5f1dd39 Feb 19, 2024
27 of 28 checks passed
msvbg pushed a commit to msvbg/bevy that referenced this pull request Feb 26, 2024
# 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 bevyengine#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.
msvbg pushed a commit to msvbg/bevy that referenced this pull request Feb 26, 2024
# 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 bevyengine#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Animation Make things move and change over time C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants