Search code examples
godotgdscripteuler-angles

How to get Euler angles from two transforms in Godot


I have a Transform that is rotated by an angle on an axis. Is it possible to get the Euler angles for the rotation using the Transform before rotation and the Transform after rotation? I tried using Transform.basis.get_euler(), to get the Euler angles of the Transform before and after. However I don't think subtracting them would work. How would this be done?


Solution

  • Solution

    I came up with this:

    var A := start.basis.get_rotation_quat()
    var B := end.basis.get_rotation_quat()
    var R := A.inverse() * B
    print(R.get_euler())
    

    It is also recommended to always normalize quaternions to minimize cumulative floating point errors. But I believe it is not a problem here.


    Explanation

    The idea is that you have a rotation quaternion A that after some rotation R becomes B. In other words:

    B = A * R
    

    And if we solve for R, we get this:

    B = A * R
    
    B * R.inverse() = A * R * R.inverse()
    
    B * R.inverse() = A
    
    B.inverse() * B * R.inverse() = B.inverse() * A
    
    R.inverse() = B.inverse() * A
    
    R = (B.inverse() * A).inverse()
    
    R = A.inverse() * B
    

    Testing

    This is what I came up for testing it:

    tool
    extends MeshInstance
    
    
    func _enter_tree() -> void:
        set_notify_transform(true)
    
    
    func _notification(what: int) -> void:
        if what == NOTIFICATION_TRANSFORM_CHANGED:
            var parent := get_parent() as Spatial
            var end := global_transform.basis.get_rotation_quat()
            var start := parent.global_transform.basis.get_rotation_quat()
            var A := start
            var B := end
            var R := A.inverse() * B
            var S := transform.basis.get_rotation_quat()
            if R.w < 0: R = -R
            if S.w < 0: S = -S
            if not S.is_equal_approx(R):
                prints("FAIL", rotation_degrees, R, S)
    

    Attach it to a MeshInstance child of an Spatial.

    The code will run on the editor. It enables a notification for any change in its Transform. When it get the notification, it computes the quaternion from its global_transform and its parent global_transform. Rotate it around and see it if prints.

    By the way, I had initially forgot that there are two ways to represent the same rotation with quaternions. So it wasn't passing the test. These lines fix it:

    if R.w < 0: R = -R
    if S.w < 0: S = -S
    

    No, normalize does not fix this. And the lack of normalize didn't cause the test to fail either.

    I also tested that you don't need to do that to get the correct value from get_euler. I mean, like this it works correctly:

    var end := global_transform.basis.get_rotation_quat()
    var start := parent.global_transform.basis.get_rotation_quat()
    var A := start
    var B := end
    var R := A.inverse() * B
    var S := transform.basis.get_rotation_quat()
    if not S.get_euler().is_equal_approx(R.get_euler()):
        prints("FAIL", rotation_degrees, R, S)
    

    Similarly it works correctly with Transform directly, like this:

    var end := global_transform
    var start := parent.global_transform
    var A := start
    var B := end
    var R := A.affine_inverse() * B
    var S := transform
    if not S.is_equal_approx(R):
        prints("FAIL", rotation_degrees, R, S)