godot icon indicating copy to clipboard operation
godot copied to clipboard

With fading, the result of applying root motion delta is different from the result of not using root motion visually

Open vaner-org opened this issue 6 months ago • 4 comments

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
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

  1. Either correctly interpret, or let us define the forward direction of the root bone in AnimationMixer.

Godot_v4 3-stable_win64_lRwkMSHG2q - Copy

Since the current get_root_motion_position()/get_root_motion_rotation() operates under "global" space,

  1. 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.

Minimal reproduction project (MRP)

root_motion_drift_MRP.zip

vaner-org avatar Aug 17 '24 10:08 vaner-org