Up to date
This page is up to date for Godot 4.2
.
If you still find outdated information, please open an issue.
3D-Transformationen verwenden¶
Einführung¶
Wenn Sie noch nie 3D-Spiele gemacht haben, kann die Arbeit mit Rotationen in drei Dimensionen zunächst verwirrend sein. Wenn man aus dem 2D-Bereich kommt, denkt man natürlich: "Oh, das ist genau wie die Rotation in 2D, nur dass die Rotationen jetzt in X, Y und Z stattfinden ".
Auf den ersten Blick scheint dies einfach zu sein, und für einfache Spiele mag diese Denkweise sogar ausreichen. Leider ist sie oft falsch.
Winkel in drei Dimensionen werden meistens als "Eulerwinkel" bezeichnet.
Eulerwinkel wurden Anfang des 18. Jahrhunderts vom Mathematiker Leonhard Euler eingeführt.
Diese Art der Darstellung von 3D-Rotationen war damals bahnbrechend, hat aber bei der Entwicklung von Spielen einige Schwächen (was von einem Kerl 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 bei Eulerwinkeln¶
Es mag zwar intuitiv erscheinen, dass jede Achse eine Rotation 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-Rotation 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 rotieren.
Dies könnte geschehen, indem man zuerst in X, dann in Y und dann in Z rotiert. Alternativ könnte man auch zuerst in Y, dann in Z und schließlich in X rotieren. 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 Rotationen.
Es folgt eine Visualisierung der Rotationsachsen (in der Reihenfolge X, Y, Z) in einem Gimbal (aus Wikipedia). Wie Sie sehen können, hängt die Ausrichtung jeder Achse von der Rotation 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 Rotation um die Y-Achse (in diesem Fall "oben", da Godot eine "Y-Up"-Ausrichtung verwendet) erfolgen, gefolgt von einer Rotation um die X-Achse.
Wenn wir zuerst die Rotation um die X-Achse und dann um die 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 Achsenrotationen 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 Eulerwinkeln ist die Interpolation. Stellen Sie sich vor, Sie möchten zwischen zwei verschiedenen Kamera- oder Gegnerpositionen (einschließlich Rotationen) 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 ist tatsächlich in die entgegengesetzte Richtung rotiert!
Es gibt einige Gründe, warum dies passieren kann:
Rotationen 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).Es handelt sich um einen Gimbal Lock (die erste und letzte gedrehte Achse richten sich aneinander aus, so dass ein Freiheitsgrad verloren geht). Eine ausführliche Erläuterung dieses Problems finden Sie auf der Wikipedia-Seite "Gimbal Lock".
Sagen Sie "Nein" zu Euler-Winkeln¶
Aus all dem folgt, dass Sie die Property rotation
von Node3D-Nodes in Godot für Spiele nicht verwenden sollten. Sie ist hauptsächlich für den Editor gedacht, für die Kohärenz mit der 2D-Engine und für einfache Rotationen (im Allgemeinen nur eine Achse, oder sogar zwei in begrenzten Fällen). So sehr Sie auch versucht sein mögen, verwenden Sie ihn nicht.
Stattdessen gibt es einen besseren Weg, Ihre Rotationsprobleme zu lösen.
Einführung in Transforms¶
Godot verwendet den Datentyp Transform3D für Orientierungen. Jeder Node3D-Node enthält eine Transform
-Property, die relativ zum Transform des Parent ist, wenn das Parent ein von Node3D-abgeleiteter Typ ist.
Es ist auch möglich, über die Property global_transform
auf die Weltkoordinatentransformation zuzugreifen.
Eine Transform hat eine Basis (transform.basis Unter-Property), die aus drei Vector3-Vektoren besteht. Auf diese wird über die Property transform.basis
zugegriffen und kann direkt über transform.basis.x
, transform.basis.y
und transform.basis.z
zugegriffen werden. Jeder Vektor zeigt in die Richtung, in die seine Achse rotiert wurde, so dass sie effektiv die gesamte Rotation des Nodes beschreiben. Die Skalierung (solange sie einheitlich ist) kann auch aus der Länge der Achsen abgeleitet werden. Eine Basis kann auch als eine 3x3-Matrix interpretiert und als transform.basis[x][y]
verwendet werden.
Eine Default-Basis (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))
Dies entspricht auch einer 3x3-Einheitsmatrix.
Entsprechend der OpenGL-Konvention ist X
die Rechts-Achse, Y
ist die Oben-Achse und Z
ist die Vorwärts-Achse.
Zusammen mit der Basis hat ein Transform auch einen Ursprung. Dies ist ein Vector3, der angibt, wie weit vom tatsächlichen Ursprung (0, 0, 0)
dieser Transform entfernt ist. Kombiniert man die Basis mit dem Ursprung, stellt ein Transform effizient eine eindeutige Translation, Rotation und Skalierung im Raum dar.
Eine Möglichkeit, ein Transform zu visualisieren, besteht darin, das 3D-Gizmo eines Objekts im Modus "Local-Space" zu betrachten.
Die Richtungspfeile des Gizmos zeigen die Achsen X
, Y
und Z
(jeweils in rot, grün und blau) der Basis, während der Mittelpunkt des Gizmos im Ursprung des Objekts liegt.
Für weitere Informationen über die Mathematik von Vektoren und Transformationen siehe die Anleitung Vektormathematik.
Manipulation von Transforms¶
Natürlich sind Transforms nicht so einfach zu handhaben wie Winkel und haben ihre eigenen Probleme.
Es ist möglich, einen Transform zu rotieren, entweder durch Multiplikation seiner Basis mit einer anderen (dies wird Akkumulation genannt), oder durch Verwendung der Rotationsmethoden.
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)
Transform3D transform = Transform;
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);
Transform = transform;
Eine Methode in Node3D 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 rotiert.
Um relativ zum Object-Space zu rotieren (der Node-eigene Transform), verwenden Sie Folgendes:
# 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¶
Aufeinanderfolgende Operationen mit Transforms führen zu einem Präzisionsverlust aufgrund von Float-Fehlern. Dies bedeutet, dass die Skalierung jeder Achse nicht mehr genau 1.0
ist und dass sie nicht genau 90
Grad auseinander liegen.
Wenn eine Transformation bei jedem Bild rotiert wird, verformt sie sich im Laufe der Zeit. Dies ist unvermeidlich.
Es gibt zwei verschiedene Möglichkeiten, dies zu handhaben. Die erste ist die Orthonormalisierung des Transforms nach einer gewissen Zeit (vielleicht einmal pro Frame, wenn Sie es jeden Frame ändern):
transform = transform.orthonormalized()
transform = transform.Orthonormalized();
Dadurch haben alle Achsen wieder die Länge 1.0
und sind 90
Grad voneinander entfernt. Die auf den Transform angewandte Skalierung geht jedoch verloren.
Es wird empfohlen, Nodes, die manipuliert werden sollen, nicht zu skalieren; skalieren Sie stattdessen ihre Child-Nodes (z. B. MeshInstance3D). Wenn Sie den Node unbedingt skalieren müssen, dann wenden Sie dies am Ende erneut an:
transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);
Abrufen von Informationen¶
Vielleicht denken Sie jetzt: "Ok, aber wie bekomme ich den Winkel aus einem Transform zurück?". Die Antwort lautet auch hier: gar nicht. Sie müssen Ihr Bestes tun, um nicht mehr in Winkeln zu denken.
Stellen Sie sich vor, Sie müssen ein Projektil in die Richtung schießen, in die Ihr Spieler blickt. Verwenden Sie einfach die Vorwärtsachse (üblicherweise Z
oder -Z
).
bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED
bullet.Transform = transform;
bullet.LinearVelocity = transform.Basis.Z * BulletSpeed;
Schaut der Gegner den Spieler an? Verwenden Sie dazu das Skalarprodukt (eine Erklärung des Skalarprodukt finden Sie in der Anleitung Vektormathematik):
# 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 Strafen:
# 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
move_and_slide()
// Keep in mind Y is up-axis
if (Input.IsActionJustPressed("jump"))
velocity.Y = JumpSpeed;
MoveAndSlide();
Alle gebräuchlichen Verhaltensweisen und Logiken können lediglich mit Vektoren ausgeführt werden.
Festlegen von Informationen¶
Es gibt natürlich Fälle, in denen man einem Transform verschiedene Informationen festlegen möchte. Stellen Sie sich einen First-Person-Controller oder eine umkreisende Kamera vor. In diesen Fällen werden definitiv Winkel verwendet, denn Sie möchten, dass die Transforms in einer bestimmten Reihenfolge erfolgen.
Bewahren Sie in solchen Fällen die Winkel und Rotationen außerhalb des Transforms auf und legen Sie sie bei jedem Bild fest. Versuchen Sie nicht, sie abzurufen und wiederzuverwenden, denn der Transform ist nicht dafür gedacht, auf diese Weise verwendet zu werden.
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
Transform3D transform = Transform;
transform.Basis = Basis.Identity;
Transform = transform;
RotateObjectLocal(Vector3.Up, _rotationX); // first rotate about Y
RotateObjectLocal(Vector3.Right, _rotationY); // then rotate about X
}
}
Wie Sie sehen, ist es in solchen Fällen sogar einfacher, die Rotation außen vor zu lassen und dann den Transform als endgültige Ausrichtung zu verwenden.
Interpolation mit Quaternionen¶
Die Interpolation zwischen zwei Transforms kann effizient mit Quaternionen durchgeführt werden. Weitere Informationen über die Funktionsweise von Quaternionen finden Sie an anderen Stellen im Internet. Für den praktischen Gebrauch reicht es aus, wenn man weiß, dass ihr Haupteinsatzgebiet die Interpolation auf dem kürzesten Weg ist. Wenn Sie also zwei Rotationen haben, ermöglicht ein Quaternion eine reibungslose Interpolation zwischen ihnen unter Verwendung der nächstgelegenen Achse.
Die Umwandlung einer Rotation in Quaternion ist unkompliziert.
# Convert basis to quaternion, keep in mind scale is lost
var a = Quaternion(transform.basis)
var b = Quaternion(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.GetQuaternion();
var b = transform2.Basis.GetQuaternion();
// 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);
Die Quaternion-Typreferenz enthält weitere Informationen über den Datentyp (sie kann auch Akkumulation transformieren, Punkte transformieren usw., obwohl dies weniger häufig verwendet wird). Wenn Sie häufig interpolieren oder Operationen auf Quaternionen anwenden, sollten Sie daran denken, dass sie irgendwann normalisiert werden müssen. Andernfalls kommt es auch bei ihnen zu Fehlern in der numerischen Genauigkeit.
Quaternionen sind nützlich bei der Interpolation von Kamera, Pfad etc., da das Ergebnis immer korrekt und glatt ist.
Transforms sind Ihr Freund¶
Für die meisten Anfänger kann es einige Zeit dauern, sich an die Arbeit mit Transforms zu gewöhnen. Sobald Sie sich jedoch an sie gewöhnt haben, werden Sie ihre Einfachheit und Leistungsfähigkeit zu schätzen wissen.
Zögern Sie nicht, in einer von Godots Online-Communitys um Hilfe zu diesem Thema zu bitten, und wenn Sie sich sicher genug fühlen, helfen Sie bitte auch anderen!