Why should you accumulate rotations outside of the transform?

I encountered a question on Discord the other day asking for an explanation on why the Godot documentation said the following:

Using Transforms: Setting Information

There are, of course, cases where you want to set information to a transform. Imagine a first person controller or orbiting camera. Those are definitely done using angles, because you do want the transforms to happen in a specific order.

For such cases, keep the angles and rotations outside the transform and set them every frame. Don’t try to retrieve and reuse them because the transform is not meant to be used this way.

The question is: Why does it say “Don’t try to retrieve and reuse them because the transform is not meant to be used this way.”? Are there any explicit reasons for why you should avoid doing this?

This seems to work fine even though it reuses the transforms, for example (code excerpt used with permission from the original author):

@onready var horizontal_pivot: Node3D = $HorizontalPivot
@onready var vertical_pivot: Node3D = $HorizontalPivot/VerticalPivot

func frame_camera_rotation() -> void:
    horizontal_pivot.rotate_y(_look.x)
    vertical_pivot.rotate_x(_look.y)
    
    vertical_pivot.rotation.x = clampf(
        vertical_pivot.rotation.x, 
        deg_to_rad(min_boundary), 
        deg_to_rad(max_boundary)
        )

The reason they say that in the documentation is because if you apply rotations cumulatively over time, you can end up with a different rotation:

extends Node3D

func print_rotation() -> void:
    print(
        rad_to_deg(rotation.x), " ",
        rad_to_deg(rotation.y), " ",
        rad_to_deg(rotation.z)
    )

func _ready() -> void:
    # Accumulate changes over time
    transform.basis = Basis()
    rotate_object_local(Vector3.UP, deg_to_rad(30))
    rotate_object_local(Vector3.RIGHT, deg_to_rad(30))
    rotate_object_local(Vector3.UP, deg_to_rad(30))
    print_rotation()
    # 25.6589050606425 63.690072840454 16.1021146192444

    # Accumulate the change, then apply it in the order you want
    transform.basis = Basis()
    rotate_object_local(Vector3.UP, deg_to_rad(60))
    rotate_object_local(Vector3.RIGHT, deg_to_rad(30))
    print_rotation()
    # 30.0000042499206 60.0000016696521 0.0

Note how the first set of rotations is different from the second set. This first rotation is not normally what you want, like in the case of FPS controllers, because the camera has a non-zero z value now – which means that it now also has roll in addition to yaw and pitch. This is why they recommend you accumulate the rotations, then apply them starting from the identity transform.

The code in the first post side-steps this problem by storing the rotations for each axis on a different transform. This has the same effect as the second set of rotations, because when the transform calculates the global rotation for the node, you’re applying all of the rotation from the vertical pivot first and then all of the rotation from the horizontal pivot in separate steps.

1 Like