3D Transformationen verwenden¶
Einführung¶
Wenn Sie noch nie 3D-Spiele gemacht haben, kann die Arbeit mit Drehungen in drei Dimensionen zunächst verwirrend sein. Wenn man aus dem 2D-Bereich kommt, denkt man natürlich: "Oh, das ist genau wie das Drehen in 2D, nur dass die Drehungen jetzt in X, Y und Z stattfinden ".
At first, this seems easy. For simple games, this way of thinking may even be enough. Unfortunately, it's often incorrect.
Winkel in drei Dimensionen werden meistens als "Eulerwinkel" bezeichnet.

Euler-Winkel wurden Anfang des 18. Jahrhunderts vom Mathematiker Leonhard Euler eingeführt.

Diese Art der Darstellung von 3D-Drehungen war damals bahnbrechend, hat aber bei der Entwicklung von Spielen einige Schwächen (was von einem Mann mit einem lustigen Hut zu erwarten ist). Die Idee dieses Dokuments ist es, zu erklären, warum das so ist, und die empfohlenen Vorgehensweisen für den Umgang mit Transformationen bei der Programmierung von 3D-Spielen zu beschreiben.
Probleme der Eulerwinkel¶
Es mag zwar intuitiv erscheinen, dass jede Achse eine Drehung hat, aber in Wahrheit ist das einfach nicht praktikabel.
Achsenreihenfolge¶
Der Hauptgrund dafür ist, dass es keinen eindeutigen Weg gibt, um eine Orientierung aus den Winkeln zu konstruieren. Es gibt keine mathematische Standardfunktion, die alle Winkel zusammennimmt und eine tatsächliche 3D-Drehung erzeugt. Die einzige Möglichkeit, eine Orientierung aus den Winkeln zu erzeugen, besteht darin, das Objekt Winkel für Winkel in einer beliebigen Reihenfolge zu drehen.
Dies könnte geschehen, indem man zuerst in X, dann in Y und dann in Z dreht. Alternativ könnte man auch zuerst in Y, dann in Z und schließlich in X drehen. Alles ist möglich, aber je nach Reihenfolge ist die endgültige Ausrichtung des Objekts nicht unbedingt dieselbe. Das bedeutet, dass es mehrere Möglichkeiten gibt, eine Orientierung aus 3 verschiedenen Winkeln zu konstruieren, abhängig von der Reihenfolge der Drehungen.
Es folgt eine Visualisierung der Drehachsen (in der Reihenfolge X, Y, Z) in einem Gimbal (aus Wikipedia). Wie Sie sehen können, hängt die Ausrichtung jeder Achse von der Drehung der vorhergehenden Achse ab:

Sie fragen sich vielleicht, was das für Sie bedeutet. Sehen wir uns ein praktisches Beispiel an:
Stellen Sie sich vor, Sie arbeiten an einem First-Person-Controller (z. B. in einem FPS-Spiel). Wenn Sie die Maus nach links und rechts bewegen, steuern Sie Ihren Blickwinkel parallel zum Boden, während das Bewegen der Maus nach oben und unten den Blick des Spielers nach oben und unten bewegt.
Um den gewünschten Effekt zu erzielen, muss in diesem Fall zuerst eine Drehung in der Y-Achse ("nach oben" in diesem Fall, da Godot eine "Y-Up"-Ausrichtung verwendet) erfolgen, gefolgt von einer Drehung in der X-Achse.

Wenn wir zuerst die Drehung in der X-Achse und dann in der Y-Achse anwenden würden, wäre der Effekt unerwünscht:

Je nach Art des Spiels oder des gewünschten Effekts kann die Reihenfolge, in der die Achsendrehungen angewendet werden sollen, unterschiedlich sein. Daher reicht es nicht aus, Rotationen in X, Y und Z anzuwenden: Sie benötigen auch eine Rotationsreihenfolge.
Interpolation¶
Ein weiteres Problem bei der Verwendung von Euler-Winkeln ist die Interpolation. Stellen Sie sich vor, Sie möchten zwischen zwei verschiedenen Kamera- oder Feindpositionen (einschließlich Drehungen) wechseln. Ein logischer Weg, dies zu tun, ist die Interpolation der Winkel von einer Position zur nächsten. Man würde erwarten, dass dies wie folgt aussieht:

Bei der Verwendung von Winkeln hat dies jedoch nicht immer die erwartete Wirkung:

Die Kamera drehte sich tatsächlich in die entgegengesetzte Richtung!
Es gibt einige Gründe, warum dies passieren kann:
Drehungen lassen sich nicht linear auf die Orientierung abbilden, so dass ihre Interpolation nicht immer den kürzesten Weg ergibt (z. B. ist der Weg von
270
nach0
Grad nicht derselbe wie der von270
nach360
, auch wenn die Winkel gleich sind).Gimbal lock is at play (first and last rotated axis align, so a degree of freedom is lost). See Wikipedia's page on Gimbal Lock for a detailed explanation of this problem.
Sagen Sie "Nein" zu Euler-Winkeln¶
The result of all this is that you should 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.
Stattdessen gibt es einen besseren Weg, Ihre Rotationsprobleme zu lösen.
Einführung in Transformationen¶
Godot uses the 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.
Es ist auch möglich, über die Eigenschaft global_transform
auf die Weltkoordinatentransformation zuzugreifen.
A transform has a 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]
.
Eine Standardbasis (unverändert) ist vergleichbar mit:
var basis = Basis()
# Contains the following default values:
basis.x = Vector3(1, 0, 0) # Vector pointing along the X axis
basis.y = Vector3(0, 1, 0) # Vector pointing along the Y axis
basis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis
// 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))
Auch dies ist analog zu einer 3x3-Identitätsmatrix.
Entsprechend der OpenGL-Konvention ist X
die Rechts-Achse, Y
ist die Aufwärts-Achse und Z
ist die Vorwärts-Achse.
Together with the 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.

The gizmo's arrows show the 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.

Für weitere Informationen über die Mathematik von Vektoren und Transformationen siehe die Anleitung Vektor-Mathematik.
Manipulation von Transformationen¶
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.
var axis = Vector3(1, 0, 0) # Or Vector3.RIGHT
var rotation_amount = 0.1
# Rotate the transform around the X axis by 0.1 radians.
transform.basis = Basis(axis, rotation_amount) * transform.basis
# shortened
transform.basis = transform.basis.rotated(axis, rotation_amount)
Vector3 axis = new Vector3(1, 0, 0); // Or Vector3.Right
float rotationAmount = 0.1f;
// Rotate the transform around the X axis by 0.1 radians.
transform.basis = new Basis(axis, rotationAmount) * transform.basis;
// shortened
transform.basis = transform.basis.Rotated(axis, rotationAmount);
Eine Methode in Spatial vereinfacht dies:
# Rotate the transform around the X axis by 0.1 radians.
rotate(Vector3(1, 0, 0), 0.1)
# shortened
rotate_x(0.1)
// Rotate the transform around the X axis by 0.1 radians.
Rotate(new Vector3(1, 0, 0), 0.1f);
// shortened
RotateX(0.1f);
Dadurch wird der Node relativ zum übergeordneten Node gedreht.
Verwenden Sie Folgendes, um sich relativ zum Objektraum zu drehen (die Node-eigene Transformation):
# Rotate around the object's local X axis by 0.1 radians.
rotate_object_local(Vector3(1, 0, 0), 0.1)
// Rotate around the object's local X axis by 0.1 radians.
RotateObjectLocal(new Vector3(1, 0, 0), 0.1f);
Präzisionsfehler¶
Doing successive operations on transforms will result in a loss of precision due to floating-point error. This means the scale of each axis may no longer be exactly 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.
There are two different ways to handle this. The first is to orthonormalize the transform after some time (maybe once per frame if you modify it every frame):
transform = transform.orthonormalized()
transform = transform.Orthonormalized();
This will make all axes have 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:
transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);
Abrufen von Informationen¶
You might be thinking at this point: "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.
Imagine you need to shoot a bullet in the direction your player is facing. Just use the forward axis (commonly Z
or -Z
).
bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED
bullet.Transform = transform;
bullet.LinearVelocity = transform.basis.z * BulletSpeed;
Schaut der Feind den Spieler an? Verwenden Sie dazu das Skalarprodukt (eine Erklärung des Skalarprodukt finden Sie in der Anleitung Vektor-Mathematik):
# Get the direction vector from player to enemy
var direction = enemy.transform.origin - player.transform.origin
if direction.dot(enemy.transform.basis.z) > 0:
enemy.im_watching_you(player)
// Get the direction vector from player to enemy
Vector3 direction = enemy.Transform.origin - player.Transform.origin;
if (direction.Dot(enemy.Transform.basis.z) > 0)
{
enemy.ImWatchingYou(player);
}
nach links schießen:
# Remember that +X is right
if Input.is_action_pressed("strafe_left"):
translate_object_local(-transform.basis.x)
// Remember that +X is right
if (Input.IsActionPressed("strafe_left"))
{
TranslateObjectLocal(-Transform.basis.x);
}
Springen:
# Keep in mind Y is up-axis
if Input.is_action_just_pressed("jump"):
velocity.y = JUMP_SPEED
velocity = move_and_slide(velocity)
// Keep in mind Y is up-axis
if (Input.IsActionJustPressed("jump"))
velocity.y = JumpSpeed;
velocity = MoveAndSlide(velocity);
Alle gebräuchlichen Verhaltensweisen und Logiken können nur mit Vektoren ausgeführt werden.
Festlegen von Informationen¶
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 re-use them because the transform is not meant to be used this way.
Beispiel für das Umsehen im FPS-Stil:
# accumulators
var rot_x = 0
var rot_y = 0
func _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
// accumulators
private 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
}
}
As you can see, in such cases it's even simpler to keep the rotation outside, then use the transform as the final orientation.
Interpolation mit Quaternionen¶
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.
Die Umwandlung einer Rotation in Quaternion ist unkompliziert.
# Convert basis to quaternion, keep in mind scale is lost
var 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 back
transform.basis = Basis(c)
// Convert basis to quaternion, keep in mind scale is lost
var 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 back
transform.basis = new Basis(c);
The 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.
Transformationen sind Ihr Freund¶
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.
Don't hesitate to ask for help on this topic in any of Godot's online communities and, once you become confident enough, please help others!