I'm struggling to understand 3D transforms in Godot. (I know, I know...)

I'm trying my best to implement an aircraft control script, and I've gotten myself into gimbal lock hell. My controls work pretty well until the Y direction decides to flip, and now my controls to pull up make the plane go down, etc...

I've read the godot 3D transform documentation, but I still find myself looking for an "Aha!" moment.

I figured a good way for me to learn would be to post what code I have and see how others would implement the movement code using transforms correctly.

Thanks for any help in advance.

extends Spatial

# Declare member variables here. Examples:
# var a = 2
# var b = "text"
onready var camera = $Camera
const SPEED = 7
const ROTATE_SPEED = 1.25
const MIN = 0.9
var rx = 0
var ry = 0
var rz = 0
# Called when the node enters the scene tree for the first time.
func _ready():

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    self.transform.basis = Basis()
    if Input.is_action_pressed("movement_forward"):
        ry -= 1
    if Input.is_action_pressed("movement_backward"):
        ry += 1
    if Input.is_action_pressed("movement_left"):
        rx += 1
    if Input.is_action_pressed("movement_right"):
        rx -= 1
    if Input.is_action_pressed("rotate_left"):
        rz += 1
    if Input.is_action_pressed("rotate_right"):
        rz -= 1
    self.transform.origin += -self.transform.basis.z * SPEED * delta
  • \$\begingroup\$ @DMGregory So my problem with gimbal lock isn’t related to me not using transformations correctly? \$\endgroup\$ Nov 18, 2020 at 15:31
  • \$\begingroup\$ It kind of is. You're treating rotation as though it were translation: "I can just keep accumulating small increments into each component, and it will behave consistently no matter where the current totals are at". But rotation doesn't work like that. The meaning of "+1 degree of pitch", as you've discovered, depends on the current total of pitch, yaw, and roll you've accumulated. So you should think of this less as accumulating into running totals, and more as computing an incremental rotation as a whole, and composing that with your current rotation (eg. as a quaternion or matrix multiply) \$\endgroup\$
    – DMGregory
    Nov 18, 2020 at 15:57
  • \$\begingroup\$ @DMGregory I kinda understand what you mean. Is there any chance you could modify my code to illustrate what you are talking about. Sorry, I know that’s a lot to ask. \$\endgroup\$ Nov 18, 2020 at 16:37
  • 1
    \$\begingroup\$ i think what @DMGregory means, is that as something rotates, its degrees will often change from positive to negative if you go one direction long enough, so if you add 1 to a positive degrees, it will go one way, but if you add it to a negative degree it will go the other way. This is why you are getting reversed controls. \$\endgroup\$
    – Millard
    Nov 19, 2020 at 17:11

1 Answer 1


I'm not fluent in Godot, but it looks like you want to do something like this:

var rotationDegreesPerSecond = 90

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    # don't reset to default orientation every frame. Rotate from where we are.
    # self.transform.basis = Basis()

    var rotationStep = deg2rad(delta * rotationDegreesPerSecond)

    if Input.is_action_pressed("movement_forward"):
        self.rotate_object_local(Vector3.RIGHT, -rotationStep)
    if Input.is_action_pressed("movement_backward"):
        self.rotate_object_local(Vector3.RIGHT, rotationStep)
    if Input.is_action_pressed("movement_left"):
        self.rotate_object_local(Vector3.UP, rotationStep)
    if Input.is_action_pressed("movement_right"):
        self.rotate_object_local(Vector3.DOWN, -rotationStep)
    if Input.is_action_pressed("rotate_left"):
        self.rotate_object_local(Vector3.FORWARD, rotationStep)
    if Input.is_action_pressed("rotate_right"):
        self.rotate_object_local(Vector3.FORWARD, -rotationStep)

    self.transform.origin += -self.transform.basis.z * SPEED * delta

Here, instead of building a new rotation from scratch using 3 angle totals each frame, we apply a small rotational increment relative to wherever our local coordinate axes are currently pointing. That means your "pitch up" control is always lifting the plane's nose in the direction the top of the plane is pointing - even if the plane is vertical or upside-down at the time.

There is a risk with this style of control that you start to accumulate unwanted roll, as shown in the animated examples I linked in the comments. Since here you've given the player direct control over their roll, they can simply counter-roll to level themselves back out. But you might also want to implement a little bit of an auto-pilot or self-leveling feature that gently nudges the craft back to upright when the player isn't actively trying to roll it over.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .