Search code examples
3dquaternionsgodoteuler-angles

3D rotation in Godot (GDScript) using Euler angles


I'm trying to create a 3D camera for a simulation game based on this video that shows how to do it in Unity. When translating the code to GDScript, I ran into a problem: I am unable to get the camera to rotate. Here is my GDScript interpretation of the rotation part of script from the video (timestamps: 8:50-10:10):

export(float) var rotation_amount = 1
export(Quat) var new_rotation

func _ready():
    new_rotation = rotation

func _process(delta):
    handle_movement_input(delta)

func handle_movement_input(delta):
    if(Input.is_action_pressed("cam_rotate_left")):
        new_rotation *= Quat(Vector3.UP * rotation_amount).get_euler()
    if(Input.is_action_pressed("cam_rotate_right")):
        new_rotation *= Quat(Vector3.UP * -rotation_amount).get_euler()

    rotation = rotation.linear_interpolate(new_rotation, delta * movement_time)

Solution

  • Types

    You claim new_rotation is a Quat (export(Quat)). But you don't only override it right away (in _ready), you override it with a Vector3 (rotation is a Vector3 that represent euler angles):

    func _ready():
        new_rotation = rotation
    

    If you made a typed export variable, Godot would have told you about that problem:

    export var new_rotation:Quat
    

    Yes, GDScript has typed variables. Use them.


    Angle representation

    If you wanted to work with quaternions (as in the video), you can get the rotation Quat like this:

    var new_rotation:Quat
    
    func _ready():
        new_rotation = transform.basis.get_rotation_quat()
    

    Then you can compose quaternions by multiplication, interpolate them with slerp and use get_euler() at the end.

    By the way, when you create a Quat passing a Vector3, it is understood as euler angles. Which is equivalent to Unity's Quaternion.Euler (which they use in the video). Also, calling to_euler converts the Quat to euler angles, which makes using Quat pointless since it was just created from euler angles (and it is not at all what they do on the video).


    If you wanted to work with euler angles, you should be adding them instead of multiplying them.


    However, since you only care about rotation around the vertical, I'm here to tell you: use a float. Keep it simple. The key where do you put the code.


    A note on interpolation

    The weight of Vector3.linear_interpolate (or of Quat.slerp or of lerp) is a value between 0 and 1. If it is 0, you get the original value. If it is 1, you get the new value.

    Adding interpolation will smooth the movement, but also gives you a deceleration/slowing down effect. So you lose sharp control.

    If you smooth rotation and stop sharply, multiply the angle by delta and some scaling constant. That is, instead of doing this: Quat(Vector3.UP * rotation_amount) you do this: Quat(Vector3.UP * rotation_amount * delta). And do no interpolation.


    Where do you put the code?

    If you are writing that code in the Camera node it is not going to work. Because rotation changes the direction the camera is looking. And that is precisely what we don't want. Instead we want to rotate around some other point in space.

    Thus, set this up in the scene tree:

    Pivot:Spatial
    └ Camera
    

    You would place the Pivot (an Spatial node) on the ground, then position the Camera looking at it from the air. And you put the rotation logic in the Pivot. And since that Pivot is only in charge of that rotation, you can use a float for the angle. And life is easy and simple (use lerp to interpolate float).


    Can you do it in a single node? Yes. I have an over-engineered solution that does exactly that elsewhere (including the explanation of why it isn't the best idea).