godot
godot copied to clipboard
With fading, the result of applying root motion delta is different from the result of not using root motion visually
Tested versions
Reproducible in any 4.x
System information
Godot v4.3.stable - Windows 10.0.19044 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3080 (NVIDIA; 32.0.15.5612) - AMD Ryzen 9 5950X 16-Core Processor (32 Threads)
Issue description
The root motion accumulator system, while a huge step up from 3.5, is still unable to address crossfading.
The problem
When a character is crossfading from one animation to another, they will often transition in such a way that undesired diagonal or negative movement is created from using the blended root rotation accumulator of the two animations against their blended root motion. Practically speaking, only a root-aligned movement vector needs to be considered.
Allow me to explain.
I have a 90 degree walk start, in two directions.
Left | Right |
---|---|
Taking the right animation for instance, because the animation ends with the root motion accumulator facing -X, while my normal walk cycle starts facing -Y (+Z in Godot), what ends up happening is diagonal drift when xfading the two.
To demonstrate this, I have slowed down the start animation with a TimeScale while keeping the walk cycle at normal speed to exaggerate the effect of this drift.
No xfade:
https://github.com/user-attachments/assets/310bcf7d-38bb-48f8-9b5f-323121856774
0.2 xfade:
https://github.com/user-attachments/assets/f0ffa2f5-63e2-4818-a7c9-7eeb49df96d6
Here is how I handle my root motion, for my Auto-Rig Pro character in Blender for a root bone with a Z-up, Y-forward orientation.
root_movement = $AnimationTree.get_root_motion_position() / delta
root_rotation = $AnimationTree.get_root_motion_rotation()
if abs(root_rotation.get_euler().z) > 0:
rotate_y(root_rotation.get_euler().z)
# root direction in animation space
var root_rotation_accumulator = animation_tree.get_root_motion_rotation_accumulator()
# rotating the movement vector to be in root's local space
var root_aligned_movement_vector = -root_movement.rotated(Vector3.UP, -root_rotation_accumulator.get_euler().y)
# rotating vector to be in global space
var vel: Vector3 = global_basis.get_rotation_quaternion() * root_aligned_movement_vector
DebugDraw3D.draw_line(global_position, global_position + vel, Color.RED)
I came up with this independently of the documentation but from my testing should yield an identical result to the documented method.
The solution
- Either correctly interpret, or let us define the forward direction of the root bone in AnimationMixer.
Since the current get_root_motion_position()/get_root_motion_rotation() operates under "global" space,
- Provide a way to get the neutralized root motion position and rotation vectors in the LOCAL space of the root bone, so that crossfading becomes possible without errors. This would mean that for a simple animation like my turn, each frame of get_root_motion_local_position would be largely towards the bone's forward axis, and each get_root_motion_local_rotation would be entirely on the bone's up axis.
Using these "local" values in root motion code would allow for identical results with smooth crossfades. In my mind, this seems to be the most straightforward way to address this, @TokageItLab might have a better idea.
Steps to reproduce
Download the project, play it, press WASD to move at a 90 degree angle from the facing direction and observe the drift. Then, open AnimationTree > move > start and change the fadeout time to 0. Try again, and observe lack of drift.