godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.5k stars 21.07k forks source link

AnimationTree root motion transform is FPS-dependent when using the Idle update mode #53752

Closed kyushu closed 1 year ago

kyushu commented 3 years ago

Godot version

v4.0.dev.20211004.official [2e8cba0bd]

System information

Ubuntu 18.04 RTX2070; Window 10 GTX1070

Issue description

the value of animationTree.get_root_motion_transform() is different when i run the same project with Linux and Windows version of Godot built v4.0-dev.20211004 on corresponding platform

Steps to reproduce

the root motion of walk animation on Linux and Windows are listed as below. By checking the animation movement, it looks like the value on Linux is correct and the value on windows is divided by 2 ?

Linux:

0.0166666675359: root_motion.origin: (0, 0.0, 0.025137)

Windows:

0.0166666675359: root_motion.origin: (0, 0.0, 0.012568)

here is my code

class_name ChBarbarian
extends CharacterBody3D

@onready var _navAgent: NavigationAgent3D = $NavigationAgent3D
@onready var _model: BarbarianModel = $BarbarianModel

var _animationTree: AnimationTree = null
var _animPlayback: AnimationNodeStateMachinePlayback = null

var patrolNetwork: Array:
    get: return patrolNetwork
    set(value):
        patrolNetwork = value

var _curPatrolIndex: int = 0

var _orientation: = Transform3D()
var path = []
var _targetPos = Vector3.ZERO
var isGoToTarget = false
var isPatrol = false
var _velocity = Vector3()
var nextLocation = Vector3()
var alertTimer = Timer.new()

func _ready() -> void:
    alertTimer.set_one_shot(true)
    alertTimer.connect("timeout", goToNextPatrolPoint)
    add_child(alertTimer)

    _animationTree = _model.animationTree
    _animPlayback = _animationTree.get("parameters/playback")

    _navAgent.target_desired_distance = 0.5
    _navAgent.connect("velocity_computed", _on_navAgent_velocity_computed)
    _navAgent.connect("target_reached", _on_navAgent_target_reached)
    var agent_rid: RID = _navAgent.get_rid()

    var map_rid: RID = get_parent().get_world_3d().get_navigation_map()
    NavigationServer3D.agent_set_map(agent_rid, map_rid)
    NavigationServer3D.agent_set_callback(agent_rid, _navAgent, "_avoidance_done")

    _orientation = global_transform
    _orientation.origin = Vector3.ZERO

func _physics_process(delta: float) -> void:
    if isPatrol or isGoToTarget:
        var nextLoc = _navAgent.get_next_location()

        if nextLocation != nextLoc:
            nextLocation = nextLoc

        var targetDir: Vector3 = position - nextLocation
        targetDir = targetDir.normalized()
        # Get current direction in quaternion form
        var q_from = _orientation.basis.get_rotation_quaternion()
        # Get TARGET direction in quaternion form
        var q_to = Transform3D().looking_at(targetDir).basis.get_rotation_quaternion()
        # Set interpolation of rotation to TARGET direction
        _orientation.basis = Basis(q_from.slerp(q_to, delta * 10))

        # ----------------------------------------------------------------------
        # If you don't want rotate up and down, Set Y axis to unit vector
        _orientation.basis.y = Vector3(0,1,0)

        # ----------------------------------------------------------------------
        # Get root motion from Animation Tree
        var root_motion: Transform3D = _animationTree.get_root_motion_transform()
        print("{}: root_motion.origin: {}".format([delta, root_motion.origin], "{}"))

        # ----------------------------------------------------------------------
        # If you don't want the rotation is controlled by root motion
        # you can set the basis of root motion to unit basis
        root_motion.basis = Basis(Vector3(1,0,0), Vector3(0,1,0), Vector3(0,0,1))

        # Apply root motion to current Orientation(in Quaternion)
        _orientation *= root_motion

        # Scale velocity by delta
        var h_velocity = _orientation.origin / delta
        _velocity.x = h_velocity.x
        _velocity.z = h_velocity.z
        # Ask NagationAgent3D to compute safe velocity to avoid dynamic obstacles
        _navAgent.set_velocity(_velocity)

        # Move CharacterBody3D by predefined function
        move_and_slide()

        # Reset accumulated origin
        _orientation.origin = Vector3.ZERO
        _orientation = _orientation.orthonormalized()

        # Apply Orientation (rotation)
        global_transform.basis = _orientation.basis

func setTarget(newLocation: Vector3, run: bool=false) -> void:
    _targetPos = newLocation
    _navAgent.set_target_location(_targetPos)
#   path = navAgent.get_nav_path() # not working
    if run:
        _animPlayback.travel("run_rm")
    else:
        _animPlayback.travel("walk_rm")
    isGoToTarget = true
    isPatrol = false

func start_patrol() -> void:
    print("start patrol")
    _targetPos = patrolNetwork[_curPatrolIndex].position
    _navAgent.set_target_location(_targetPos)
    _animPlayback.travel("walk_rm")
    isPatrol = true
    isGoToTarget = false

func goToNextPatrolPoint() -> void:
    start_patrol()

func _on_navAgent_velocity_computed(safe_velocity: Vector3) -> void:
    motion_velocity = safe_velocity

func _on_navAgent_target_reached() -> void:
    print("Reach Target")
    isGoToTarget = false
    _animPlayback.travel("idle_alert")
    if isPatrol:
        _curPatrolIndex += 1
        if _curPatrolIndex >= patrolNetwork.size():
            patrolNetwork.shuffle()
            _curPatrolIndex = 0
            var dist: Vector3 = patrolNetwork[_curPatrolIndex].position - position
            if  dist.length() < 1:
                _curPatrolIndex += 1
        alertTimer.start(5)

Minimal reproduction project

No response

fire commented 3 years ago

@TokageItLab You were in this area. Maybe you want to review?

TokageItLab commented 3 years ago

@fire I don't currently have a Linux system, so I need to prepare it. I'll take a look at that later if no one else wants to review it.

TokageItLab commented 3 years ago

@kyushu One thing I'm wondering, is the frame rate correctly the same on both systems?

kyushu commented 3 years ago

@TokageItLab I think it is, I just run the program and print the value on Linux and Windows

Could you point me out which source file should I check? Maybe I can help

I write C++ but for gaming and 3D visual stuff I am newbie

TokageItLab commented 3 years ago

@kyushu The root motion is calculated in animation.cpp with value of time delta, but I think the causes of this issue is that the value of time delta is different.

kyushu commented 3 years ago

@TokageItLab here are partial result of root motion run on 3 platforms, it's weird ?

Ubuntu 20.04, RTX3070

0.0166666675359: root_motion.origin: (0, 0.0, 0.025137)

Windows 10, GTX 1070

0.0166666675359: root_motion.origin: (0, 0.0, 0.012568)

Pop Os 20.04, GTX 1070

0.0166666675359: root_motion.origin: (0, 0.0, 0.010474)
0.0166666675359: root_motion.origin: (0, 0.0, 0.010474)
TokageItLab commented 3 years ago

@kyushu Is the AnimationTree playback mode property set to Physics?

kyushu commented 3 years ago

@TokageItLab Thanks ! It's fixed, i change AnimationTree -> Process Callback to Physics and run the project on 3 platforms again and root_motion.origin are all the same

i think i have to read the document again and Thank you @TokageItLab

AttackButton commented 2 years ago

What would be the problem with leaving AnimationPlayer and AnimationTree in physicsby default instead of Idle?

TokageItLab commented 2 years ago

AFAIK, the Godot document recommend to use Idle for reducing latency with user input.

However, the difference in behavior between Physics and Idle seems to have some fundamental problems, but I can't know without looking into the details.

At least in cases where animation is used, it is almost impossible to get correct results without using Physics.

Calinou commented 2 years ago

What would be the problem with leaving AnimationPlayer and AnimationTree in physicsby default instead of Idle?

Using the Physics update mode for AnimationPlayer and AnimationTree would constrain the update rate to 60 Hz, which looks ugly on high-refresh rate monitors. The Physics mode should only be used for AnimationPlayer and AnimationTree nodes that interact with physics somehow (e.g. by moving a PhysicsBody).

jcarlosrc commented 2 years ago

Maybe you will need to get root_motion_transform in the _process(delta) function because the animation tree uses the Idle update mode. Then you can apply the orientation transform in the physics process function.

JoanPotatoes2021 commented 2 years ago

I am facing breaking perfomance loss if I use physics in process_callback of animation tree, which I wanted because I wanted to have a constant root_motion velocity despite the fps, which points to this issue.

However I didn't wanted to spam another issue because it might be related to alpha perfomance? I didn't saw anything about optmizing animation trees or perfomance issues related to animations though, here's a gif showing my problem,

AnimationTree_SeverePerfomanceLoss_onProcessCallback_Physics -note that gif was rendered to 20 fps

I might not be using animation trees correctly, but this seems how it was supposed to work? Here's how is structured, it was designed for a third person shooter. I saw this type of animation tree being done for other projects, is it that costly to blend multiple animations? I am a little worried to be honest,

2

Why I am getting so much perfomance loss setting the process callback to physics? I would expect just like Calinou pointed out, to limit the update rate of the animations,

That gif was on an complete empty project, my skeleton have 107 bones, the tree is very complex and I have a lot of animations but by no means should it drop so low, something's going on. On my full project it drops from 240 fps to ~5-20 fps with just 5 characters with meshs and this very same animation tree and skeleton.

REF v4.0.alpha15.official [432b25d36] Windows 10, Nvidia Geforce GTX 1050 TI, Vulkan

fire commented 2 years ago

Are you able to release your code or assist our future performance testing suite for debugging and optimization?

Like for example under mit license or cc-by.

TokageItLab commented 2 years ago

In this case, obviously having duplicated AnimationTree is the cause of the performance degradation. Blending is performed on all AnimationTrees. If it is not necessary to have separate AnimationTrees, I recommend writing a script that only transfers Pose.

TokageItLab commented 2 years ago

However, I know of a few parts that could be optimized in blending, but that is scheduled for the after released the beta (during beta) optimization period. So as @fire said, it would be helpful to send a performance test project.

JoanPotatoes2021 commented 2 years ago

Fire, I am uploading a test project, all the animations and the test model are done by me, so they are royalty free for Godot, plus if anyone buys my game in the future the game assets are easily exctractable so I am not worried about them, and honestly mixamo has better animations than me 🌜 ,

Running this test project I get to ~5 fps, the characters were duplicated using Ctrl+D in the editor and are saved as a separate scene "Sample_Character", this is the same way I work on my WIP third person project I mentioned previously, I am running a script to randomize oneshots of the animation tree to give more chaos to the tests, don't know if this helps the optmizations tests or not,

AnimTree_PerfomanceTest.zip

AnimationTree_perfTest

In my game I plan to have 20 to 30 characters at the same time, mind you not all of them would be human, which might be the most expensible characters in the game, more if possible would be also great, so having this perfomance with only 5-6, I will need to explore alternatives to improve it.

My current workflow for this project is to have the rig separated from the characters, this is done exporting a animation rig from blender, with all it's animations in the NLA, and a linked duplicated rig with no animations in the NLA used to hold the skinning information for any meshes, this way in Godot I import the .gltf mesh and change the skeleton to the one that has all animations. This allows me to group assets into separate .gltf files, which helps managing assets from blender,

TokageItLab, the animation_tree node is duplicated, but the tree_root is saved as a resource .tres, I tried to create a animation_tree node in a autoload and pass to the actors but couldn't resolve assigning the animation player in the actors, it failed silently. Now I am interested in what you mentioned with transfer only poses,

One of the optmizations I plan to work on is to reduce the amount of oneshots I have using transition nodes to group them, this in theory should reduce the amount of inactive one shots mixed in the tree, and maybe I can work something out with the blend nodes as well,

animation_tree_grouping

Another idea I tried was to edit the tree_root nodes itself, reusing the same structure, swapping animations using gdscript, but this requires each tree_root to be unique or changing one tree_root node all character using that tree_root would also change that animation node, so in theory this can also work with a specialized small tree unique for the player, if the player needs way more animations than npcs,

TokageItLab commented 2 years ago

@fire @JoanPotatoes2021 Since those performance issues are not directly related to this issue, so please move on https://github.com/godotengine/godot/issues/65199.