Enable robust root motion for character animations
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
## What problem does this solve or what need does it fill?
I didn't see any issues raised about this yet in my search. Not sure if it is already on the radar.
Root motion (extracting movement from an animation to drive a character's world position) is a common requirement for character controllers, but Bevy's current animation system doesn't expose the APIs needed to implement it robustly.
The core issues are:
1. No curve sampling at arbitrary times
- AnimationClip stores curves wrapped in dyn AnimationCurve
- Internal curve types (AnimatableKeyframeCurve, CubicKeyframeCurve) implement Curve with sample() methods, but these are inaccessible
- Can't compute "where is the root bone at time T?" to extract motion deltas
2. No loop detection API
- Internally, ActiveAnimation has just_completed and last_seek_time to track loop transitions
- These fields are private with no public accessors
- Workarounds (tracking elapsed time, heuristics) are fragile, clip-dependent, and work poorly when blending
3. No blend weight access
- When blending animations (crossfades, additive layers), the root bone transform is a weighted blend
- There's no way to get "what's the blended root bone transform?" before it's written to the entity
- This makes root motion with blending essentially impossible without Bevy API changes
Current Workarounds (and why they fail)
- Delta tracking: Track previous frame's root bone position, compute delta each frame. Fails on loop because position snaps back to start (delta becomes huge)
- Heuristic thresholds: Try to detect loops by "if delta > X, skip". Fragile, clip-dependent, error-prone, rarely works with blended clips.
- Pre-baking: Export root motion curves separately in DCC. Works but requires pipeline changes and doesn't work with blending
## What solution would you like?
Proposed Solution: Expose Animation Pipeline APIs
The minimal set of APIs needed to enable robust root motion implementation:
1. Curve Sampling API
Add a method to AnimationClip to sample a property at a specific time:
```rust
impl AnimationClip {
/// Sample an animated property at a specific time.
/// Returns None if the target isn't animated by this clip.
pub fn sample_at_time(
&self,
target: AnimationTargetId,
time: f32,
) -> Option {
// Use internal curve sampling
}
}
```
This allows users to:
- Query root bone transform at seek_time
- Query root bone transform at seek_time - delta
- Compute delta = current - previous (correct root motion!)
2. Loop Detection API
Expose private fields from ActiveAnimation:
```rust
impl ActiveAnimation {
/// Returns the seek time from the previous frame, if available.
pub fn last_seek_time(&self) -> Option { ... }
/// Returns true if the animation completed a loop this frame.
pub fn just_completed(&self) -> bool { ... }
}
```
This enables:
- Explicit loop detection (no heuristics needed)
- Proper handling of wrap-around when computing deltas
3. (Optional) Built-in Root Motion
A higher-level feature for convenience:
```rust
#[derive(Component)]
struct RootMotion {
target_bone: AnimationTargetId,
accumulated_delta: Vec3,
}
impl AnimationPlayer {
/// Configure which bone drives root motion.
pub fn set_root_motion_target(&mut self, target: AnimationTargetId) { ... }
}
```
This would automatically extract motion and apply it to the character entity.
---
## What alternative(s) have you considered?
I have tried the heuristic approach, storing position deltas on the root bone and setting a threshold above which we skip translation for the frame, This can be tuned to work well for individual clips, but it is not a robust solution. It is not guaranteed to work across different clips with different velocities on the root bone, and it doesn't work when blending between clips in the graph. It introduces popping and other artifacts.
Removing all translation tracks and moving the character transform manually is of course a viable alternative to root motion and is what I am doing currently. It is not so easy to make the movement look as natural as it can with root motion though, and some clips are just not conducive to smooth translation at constant velocity, e.g. clips that stop-go-stop-go. In such cases you would have to construct separate translation curves and even still you will have issues blending multiple clips.
## Additional context
None
Contributor guide
Assessment
This issue has not been assessed yet.