Using 3D transforms

Introduction

“Oh, it’s just like rotating in 2D, except now rotations happen in X, Y and Z”. At first, this seems easy. For simple games, this way of thinking may even be enough. Unfortunately, it’s often incorrect. Angles in three dimensions are most commonly referred to as “Euler Angles”. Euler angles were introduced by mathematician Leonhard Euler in the early 1700s. This way of representing 3D rotations was groundbreaking at the time, but it has several shortcomings when used in game development (which is to be expected from a guy with a funny hat). The idea of this document is to explain why, as well as outlining best practices for dealing with transforms when programming 3D games.

Problems of Euler angles

While it may seem intuitive that each axis has a rotation, the truth is that it’s just not practical.

Axis order

unique way to construct an orientation from the angles. There isn’t a standard mathematical function that takes all the angles together and produces an actual 3D rotation. The only way an orientation can be produced from angles is to rotate the object angle by angle, in an arbitrary order. X, then Y and then in Z. Alternatively, you could first rotate in Y, then in Z and finally in X. Anything works, but depending on the order, the final orientation of the object will not necessarily be the same. Indeed, this means that there are several ways to construct an orientation from 3 different angles, depending on the order of the rotations. Following is a visualization of rotation axes (in X, Y, Z order) in a gimbal (from Wikipedia). As you can see, the orientation of each axis depends on the rotation of the previous one: You may be wondering how this affects you. Let’s look at a practical example: Imagine you are working on a first-person controller (e.g. an FPS game). Moving the mouse left and right controls your view angle parallel to the ground, while moving it up and down moves the player’s view up and down. Y axis (“up” in this case, since Godot uses a “Y-Up” orientation), followed by rotation in the X axis. X axis first, and then in Y, the effect would be undesired: rotation order.

Interpolation

Another problem with using Euler angles is interpolation. Imagine you want to transition between two different camera or enemy positions (including rotations). One logical way to approach this is to interpolate the angles from one position to the next. One would expect it to look like this: But this does not always have the expected effect when using angles: The camera actually rotated the opposite direction! There are a few reasons this may happen:

  • 270 to 0 degrees is not the same as going from 270 to 360, even though the angles are equivalent).
  • Wikipedia’s page on Gimbal Lock for a detailed explanation of this problem.

    Say no to Euler angles

    not use the rotation property of Spatial nodes in Godot for games. It’s there to be used mainly in the editor, for coherence with the 2D engine, and for simple rotations (generally just one axis, or even two in limited cases). As much as you may be tempted, don’t use it. Instead, there is a better way to solve your rotation problems.

    Introducing transforms

    Transform datatype for orientations. Each Spatial node contains a transform property which is relative to the parent’s transform, if the parent is a Spatial-derived type. global_transform property. Basis (transform.basis sub-property), which consists of three Vector3 vectors. These are accessed via the transform.basis property and can be accessed directly by transform.basis.x, transform.basis.y, and transform.basis.z. Each vector points in the direction its axis has been rotated, so they effectively describe the node’s total rotation. The scale (as long as it’s uniform) can also be inferred from the length of the axes. A basis can also be interpreted as a 3x3 matrix and used as transform.basis[x][y]. A default basis (unmodified) is akin to: GDScript C#
    1. var basis = Basis()# Contains the following default values:basis.x = Vector3(1, 0, 0) # Vector pointing along the X axisbasis.y = Vector3(0, 1, 0) # Vector pointing along the Y axisbasis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis
    1. // Due to technical limitations on structs in C# the default// constructor will contain zero values for all fields.var defaultBasis = new Basis();GD.Print(defaultBasis); // prints: ((0, 0, 0), (0, 0, 0), (0, 0, 0))// Instead we can use the Identity property.var identityBasis = Basis.Identity;GD.Print(identityBasis.x); // prints: (1, 0, 0)GD.Print(identityBasis.y); // prints: (0, 1, 0)GD.Print(identityBasis.z); // prints: (0, 0, 1)// The Identity basis is equivalent to:var basis = new Basis(Vector3.Right, Vector3.Up, Vector3.Back);GD.Print(basis); // prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))
    This is also an analog of a 3x3 identity matrix. X is the Right axis, Y is the Up axis and Z is the Forward axis. basis, a transform also has an origin. This is a Vector3 specifying how far away from the actual origin (0, 0, 0) this transform is. Combining the basis with the origin, a transform efficiently represents a unique translation, rotation, and scale in space. One way to visualize a transform is to look at an object’s 3D gizmo while in “local space” mode. X, Y, and Z axes (in red, green, and blue respectively) of the basis, while the gizmo’s center is at the object’s origin. Vector math tutorials.

    Manipulating transforms

    Of course, transforms are not as straightforward to manipulate as angles and have problems of their own. It is possible to rotate a transform, either by multiplying its basis by another (this is called accumulation), or by using the rotation methods. GDScript C#
    1. var axis = Vector3(1, 0, 0) # Or Vector3.RIGHTvar rotation_amount = 0.1# Rotate the transform around the X axis by 0.1 radians.transform.basis = Basis(axis, rotation_amount) * transform.basis# shortenedtransform.basis = transform.basis.rotated(axis, rotation_amount)
    1. Vector3 axis = new Vector3(1, 0, 0); // Or Vector3.Rightfloat rotationAmount = 0.1f;// Rotate the transform around the X axis by 0.1 radians.transform.basis = new Basis(axis, rotationAmount) * transform.basis;// shortenedtransform.basis = transform.basis.Rotated(axis, rotationAmount);
    A method in Spatial simplifies this: GDScript C#
    1. # Rotate the transform around the X axis by 0.1 radians.rotate(Vector3(1, 0, 0), 0.1)# shortenedrotate_x(0.1)
    1. // Rotate the transform around the X axis by 0.1 radians.Rotate(new Vector3(1, 0, 0), 0.1f);// shortenedRotateX(0.1f);
    This rotates the node relative to the parent node. To rotate relative to object space (the node’s own transform), use the following: GDScript C#
    1. # Rotate around the object's local X axis by 0.1 radians.rotate_object_local(Vector3(1, 0, 0), 0.1)
    1. // Rotate around the object's local X axis by 0.1 radians.RotateObjectLocal(new Vector3(1, 0, 0), 0.1f);

    Precision errors

    1.0, and they may not be exactly 90 degrees from each other. If a transform is rotated every frame, it will eventually start deforming over time. This is unavoidable. orthonormalize the transform after some time (maybe once per frame if you modify it every frame): GDScript C#
    1. transform = transform.orthonormalized()
    1. transform = transform.Orthonormalized();
    1.0 length again and be 90 degrees from each other. However, any scale applied to the transform will be lost. It is recommended you not scale nodes that are going to be manipulated; scale their children nodes instead (such as MeshInstance). If you absolutely must scale the node, then re-apply it at the end: GDScript C#
    1. transform = transform.orthonormalized()transform = transform.scaled(scale)
    1. transform = transform.Orthonormalized();transform = transform.Scaled(scale);

    Obtaining information

    “Ok, but how do I get angles from a transform?”. The answer again is: you don’t. You must do your best to stop thinking in angles. Z or -Z). GDScript C#
    1. bullet.transform = transformbullet.speed = transform.basis.z * BULLET_SPEED
    1. bullet.Transform = transform;bullet.LinearVelocity = transform.basis.z * BulletSpeed;
    Vector math tutorial for an explanation of the dot product): GDScript C#
    1. # Get the direction vector from player to enemyvar direction = enemy.transform.origin - player.transform.originif direction.dot(enemy.transform.basis.z) > 0: enemy.im_watching_you(player)
    1. // Get the direction vector from player to enemyVector3 direction = enemy.Transform.origin - player.Transform.origin;if (direction.Dot(enemy.Transform.basis.z) > 0){ enemy.ImWatchingYou(player);}
    Strafe left: GDScript C#
    1. # Remember that +X is rightif Input.is_action_pressed("strafe_left"): translate_object_local(-transform.basis.x)
    1. // Remember that +X is rightif (Input.IsActionPressed("strafe_left")){ TranslateObjectLocal(-Transform.basis.x);}
    Jump: GDScript C#
    1. # Keep in mind Y is up-axisif Input.is_action_just_pressed("jump"): velocity.y = JUMP_SPEEDvelocity = move_and_slide(velocity)
    1. // Keep in mind Y is up-axisif (Input.IsActionJustPressed("jump")) velocity.y = JumpSpeed;velocity = MoveAndSlide(velocity);
    All common behaviors and logic can be done with just vectors.

    Setting information

    do want the transforms to happen in a specific order. outside the transform and set them every frame. Don’t try to retrieve and re-use them because the transform is not meant to be used this way. Example of looking around, FPS style: GDScript C#
    1. # accumulatorsvar rot_x = 0var rot_y = 0func _input(event): if event is InputEventMouseMotion and event.button_mask & 1: # modify accumulated mouse rotation rot_x += event.relative.x * LOOKAROUND_SPEED rot_y += event.relative.y * LOOKAROUND_SPEED transform.basis = Basis() # reset rotation rotate_object_local(Vector3(0, 1, 0), rot_x) # first rotate in Y rotate_object_local(Vector3(1, 0, 0), rot_y) # then rotate in X
    1. // accumulatorsprivate float _rotationX = 0f;private float _rotationY = 0f;public override void _Input(InputEvent @event){ if (@event is InputEventMouseMotion mouseMotion) { // modify accumulated mouse rotation _rotationX += mouseMotion.Relative.x * LookAroundSpeed; _rotationY += mouseMotion.Relative.y * LookAroundSpeed; // reset rotation Transform transform = Transform; transform.basis = Basis.Identity; Transform = transform; RotateObjectLocal(Vector3.Up, _rotationX); // first rotate about Y RotateObjectLocal(Vector3.Right, _rotationY); // then rotate about X }}
    final orientation.

    Interpolating with quaternions

    Interpolating between two transforms can efficiently be done with quaternions. More information about how quaternions work can be found in other places around the Internet. For practical use, it’s enough to understand that pretty much their main use is doing a closest path interpolation. As in, if you have two rotations, a quaternion will smoothly allow interpolation between them using the closest axis. Converting a rotation to quaternion is straightforward. GDScript C#
    1. # Convert basis to quaternion, keep in mind scale is lostvar a = Quat(transform.basis)var b = Quat(transform2.basis)# Interpolate using spherical-linear interpolation (SLERP).var c = a.slerp(b,0.5) # find halfway point between a and b# Apply backtransform.basis = Basis(c)
    1. // Convert basis to quaternion, keep in mind scale is lostvar a = transform.basis.Quat();var b = transform2.basis.Quat();// Interpolate using spherical-linear interpolation (SLERP).var c = a.Slerp(b, 0.5f); // find halfway point between a and b// Apply backtransform.basis = new Basis(c);
    Quat type reference has more information on the datatype (it can also do transform accumulation, transform points, etc., though this is used less often). If you interpolate or apply operations to quaternions many times, keep in mind they need to be eventually normalized. Otherwise, they will also suffer from numerical precision errors. Quaternions are useful when doing camera/path/etc. interpolations, as the result will always be correct and smooth.

    Transforms are your friend

    For most beginners, getting used to working with transforms can take some time. However, once you get used to them, you will appreciate their simplicity and power. online communities and, once you become confident enough, please help others!