Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Utilizzare le trasformazioni 3D
Introduzione
Se non hai mai creato giochi 3D prima d'ora, lavorare con le rotazioni in tre dimensioni può risultare confuso all'inizio. Venendo dal 2D, il modo naturale di pensare è del tipo "Oh, è proprio come ruotare in 2D, solo che ora le rotazioni avvengono in X, Y e Z".
A prima vista, sembra facile. Per i giochi più semplici, questo modo di pensare può anche bastare. Purtroppo, spesso è incorretto.
Gli angoli in tre dimensioni sono comunemente chiamati "angoli di Eulero".
Gli angoli di Eulero furono introdotti dal matematico Leonhard Euler all'inizio del 1700.
Questo modo di rappresentare le rotazioni 3D era rivoluzionario all'epoca, ma presenta diverse imperfezioni per lo sviluppo di videogiochi (il che è prevedibile da un tizio con un cappello buffo). L'idea di questo documento è spiegare perché, oltre a delineare le migliori pratiche per gestire le trasformazioni nella programmazione di giochi 3D.
Problemi degli angoli di Eulero
Anche se può sembrare intuitivo che ogni asse abbia una rotazione, la verità è che non è proprio pratico.
Ordine degli assi
Il motivo principale è che non esiste un modo unico per costruire un orientamento a partire dagli angoli. Non esiste una funzione matematica standard che prenda tutti gli angoli insieme e produca una rotazione effettivamente 3D. L'unico modo per produrre un orientamento partendo dagli angoli è ruotare l'oggetto angolo per angolo, in un ordine arbitrario.
Questo si potrebbe fare ruotando prima in X, poi in Y e poi in Z. Oppure, potresti prima ruotare in Y, poi in Z e infine in X. Tutto funziona, ma a seconda dell'ordine, l'orientamento finale dell'oggetto non sarà necessariamente lo stesso. Infatti, ciò significa che ci sono diversi modi per costruire un orientamento da 3 angoli diversi, a seconda dell'ordine delle rotazioni.
Di seguito è riportata una visualizzazione degli assi di rotazione (in ordine X, Y, Z) in un gimbal (da Wikipedia). Come puoi vedere, l'orientamento di ciascun asse dipende dalla rotazione del precedente:
Forse ti starai chiedendo come questo ti riguarda. Vediamo un esempio pratico:
Immagina di star lavorando su un controller in prima persona (ad esempio un gioco FPS). Muovendo il mouse a sinistra e a destra si controlla l'angolo della visuale parallelo al terreno, mentre muovendolo verso l'alto e verso il basso si sposta la visuale del giocatore verso l'alto e verso il basso.
In questo caso, per ottenere l'effetto desiderato, la rotazione si deve applicare prima sull'asse Y (in questo caso "verso l'alto", poiché Godot utilizza un orientamento "Y-Su"), seguita dalla rotazione sull'asse X.
Se applicassimo prima la rotazione sull'asse X e poi su Y, l'effetto sarebbe indesiderato:
A seconda del tipo di gioco o dell'effetto desiderato, l'ordine in cui si desidera applicare le rotazioni degli assi può variare. Pertanto, applicare le rotazioni in X, Y e Z non basta: è necessario anche un ordine di rotazione.
Interpolazione
Un altro problema con l'uso degli angoli di Eulero è l'interpolazione. Immagina di voler effettuare una transizione tra due diverse posizioni della telecamera o dei nemici (rotazioni incluse). Un modo logico per affrontarlo è interpolare gli angoli da una posizione all'altra. Ci si aspetterebbe che appaia così:
Ma questo non sempre ha l'effetto previsto quando si usano gli angoli:
Infatti la telecamera ha ruotato nella direzione opposta!
Ci sono alcune ragioni per cui ciò può accadere:
Le rotazioni non corrispondono linearmente all'orientamento, quindi la loro interpolazione non sempre produce il percorso più breve (ad esempio, passare da
270a0gradi non è la stessa cosa di passare da270a360, anche se gli angoli sono equivalenti).È in gioco il blocco gimbal (il primo e l'ultimo asse ruotato si allineano, perdendo un certo grado di libertà). Consulta la pagina di Wikipedia sul blocco gimbal per una spiegazione dettagliata di questo problema.
Dici no agli angoli di Eulero
Il risultato di tutto ciò è che non dovresti usare la proprietà rotation dei nodi Node3D in Godot per i giochi. Serve principalmente nell'editor, per coerenza con il motore 2D e per rotazioni semplici (generalmente un solo asse, o anche due in casi limitati). Per quanto tu possa essere tentato, non usarla.
Esiste invece un modo migliore per risolvere questi problemi di rotazione.
Introduzione alle trasformazioni
Godot utilizza il tipo di dati Transform3D per gli orientamenti. Ogni nodo Node3D contiene una proprietà transform relativa alla trasformazione del nodo padre, se il nodo padre è un tipo derivato da Node3D.
È anche possibile accedere alla trasformazione nelle coordinate mondiali tramite la proprietà global_transform.
Una trasformazione ha una Basis (sotto-proprietà transform.basis), che consiste di tre vettori class_Vector3. Questi sono accessibili tramite la proprietà transform.basis e si possono accedere direttamente da transform.basis.x, transform.basis.y e transform.basis.z. Ogni vettore punta nella direzione in cui è stato ruotato il suo asse, quindi descrivono efficacemente la rotazione totale del nodo. La scala (purché sia uniforme) si può anche dedurre dalla lunghezza degli assi. Una base si può anche interpretare come una matrice 3x3 e utilizzata come transform.basis[x][y].
Una base predefinita (non modificata) è equivalente a:
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))
Anche questa è equivalente a una matrice di identità 3x3.
Seguendo la convenzione OpenGL, X è l'asse Destra, Y è l'asse Su e Z è l'asse Avanti.
Oltre alla base, una trasformazione ha anche un'origine. Si tratta di un Vector3 che specifica quanto lontano dall'origine effettiva (0, 0, 0) è questa trasformazione. Combinando la base con l'origine, una trasformazione rappresenta efficientemente una traslazione, una rotazione e una scala uniche nello spazio.
Un modo per visualizzare una trasformazione è osservare il gizmo 3D di un oggetto, in modalità "spazio locale".
Le frecce del gizmo mostrano gli assi X, Y e Z (rispettivamente in rosso, verde e blu) della base, mentre il centro del gizmo si trova nell'origine dell'oggetto.
Per maggiori informazioni sulla matematica dei vettori e delle trasformazioni, si prega di leggere i tutorial Matematica vettoriale.
Manipolazione delle trasformazioni
Naturalmente, le trasformazioni non sono così intuitive da manipolare come gli angoli e hanno i propri problemi.
È possibile ruotare una trasformata, moltiplicandone la base per un'altra (questo processo è chiamato accumulazione) oppure utilizzando i metodi di rotazione.
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;
Un metodo in Node3D semplifica questo:
# 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);
Ciò ruota il nodo rispetto al nodo padre.
Per ruotare rispetto allo spazio dell'oggetto (la trasformazione del nodo stesso), usa quanto segue:
# 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);
L'asse deve essere definito nel sistema di coordinate locale dell'oggetto. Ad esempio, per ruotare attorno agli assi X, Y o Z locali dell'oggetto, utilizza Vector3.RIGHT per l'asse X, Vector3.UP per l'asse Y e Vector3.FORWARD per l'asse Z.
Errori di precisione
Eseguire operazioni successive sulle trasformazioni risulterà in una perdita di precisione, dovuta a un errore di virgola mobile. Ciò significa che la scala di ciascun asse potrebbe non essere più esattamente 1.0 e che gli assi potrebbero non essere esattamente 90 gradi l'uno dall'altro.
Se una trasformazione viene ruotata a ogni frame, col tempo inizierà a deformarsi. Questo è inevitabile.
Ci sono due modi diversi per gestirlo. Il primo è quello di ortonormalizzare la trasformazione dopo un po' di tempo (magari una volta per frame se la si modifica a ogni frame):
transform = transform.orthonormalized()
transform = transform.Orthonormalized();
In questo modo tutti gli assi avranno di nuovo una lunghezza di 1.0 e saranno a 90 gradi tra l'uno all'altro. Tuttavia, qualsiasi scala applicata alla trasformazione andrà persa.
Si raccomanda di non ridimensionare i nodi che saranno manipolati; ridimensiona invece i nodi figlio (come MeshInstance3D). Se è assolutamente necessario ridimensionare il nodo, riapplica la scala alla fine:
transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);
Ottenere informazioni
A questo punto potresti pensare: "Ok, ma come faccio a ottenere gli angoli da una trasformazione?". Ancora una volta, la risposta è: non si ottiene. Devi fare del tuo meglio per smettere di pensare in angoli.
Immagina di dover sparare un proiettile nella direzione in cui è rivolto il tuo giocatore. Usa solo l'asse in avanti.
# On RigidBody3D.
# Keep in mind that -Z is forward.
bullet.transform = transform
bullet.linear_velocity = -transform.basis.z * BULLET_SPEED
// On RigidBody3D.
// Keep in mind that -Z is forward.
bullet.Transform = Transform;
bullet.LinearVelocity = -Transform.Basis.Z * BulletSpeed;
Il nemico sta guardando il giocatore? Usa il prodotto scalare per questo (vedi il tutorial Matematica vettoriale per una spiegazione del prodotto scalare):
# 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);
}
Spostati lateralmente a sinistra:
# On CharacterBody3D.
# Keep in mind that -X is left.
if Input.is_action_pressed("strafe_left"):
velocity = -transform.basis.x * MOVE_SPEED
move_and_slide()
// On CharacterBody3D.
// Keep in mind that -X is left.
if (Input.IsActionPressed("strafe_left"))
{
Velocity = -Transform.Basis.X * MoveSpeed;
}
MoveAndSlide();
Salto:
# On CharacterBody3D.
# Keep in mind that +Y is up.
if Input.is_action_just_pressed("jump"):
velocity.y = JUMP_SPEED
move_and_slide()
// On CharacterBody3D.
// Keep in mind that +Y is up.
if (Input.IsActionJustPressed("jump"))
{
Velocity = Vector3.Up * JumpSpeed;
}
MoveAndSlide();
Tutti i comportamenti e le logiche più comuni si possono effettuare avendo solo vettori.
Impostare informazioni
Ci sono, bensì, casi in cui si desidera impostare informazioni su una trasformazione. Immagina un controller in prima persona o una telecamera orbitante. Questi utilizzano sicuramente gli angoli, perché si desidera che le trasformazioni avvengano in un ordine specifico.
In questi casi, mantieni gli angoli e le rotazioni all'esterno della trasformazione e impostali a ogni frame. Non cercare di recuperarli e riutilizzarli perché la trasformazione non è pensata per questo scopo.
Esempio di guardarsi attorno, stile FPS:
# 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.screen_relative.x * LOOKAROUND_SPEED
rot_y -= event.screen_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.ScreenRelative.X * LookAroundSpeed;
_rotationY -= mouseMotion.ScreenRelative.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
}
}
Come puoi vedere, in questi casi è ancora più semplice mantenere la rotazione all'esterno, poi utilizzare la trasformazione come orientamento finale.
Interpolare con quaternioni
È possibile effettuare interpolazioni tra due trasformazioni efficientemente con i quaternioni. Ulteriori informazioni sul funzionamento dei quaternioni sono disponibili in altre fonti online. Per un uso pratico, basta comprendere che il loro uso principale è interpolare il percorso più vicino. Ad esempio, se si hanno due rotazioni, un quaternione consentirà di interpolare fluidamente tra di esse utilizzando l'asse più vicino.
Convertire una rotazione in quaternione è semplice.
# 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 = new Quaternion(transform.Basis);
var b = new Quaternion(transform2.Basis);
// 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);
Il riferimento al tipo Quaternion contiene maggiori informazioni sul tipo di dati (può anche effettuare l'accumulazione di trasformazioni, i punti di trasformazione, ecc., sebbene questa sia poco comune). Se si interpolano o si applicano operazioni ai quaternioni più volte, è necessario tenere presente che alla fine si devono normalizzare. Se no, anche loro potrebbero avere errori di precisione numerica.
I quaternioni sono utili per le interpolazioni di telecamere/percorsi/ecc., poiché il risultato sarà sempre corretto e fluido.
Le trasformazioni sono tue amiche
Per la maggior parte dei principianti, abituarsi a lavorare con le trasformazioni può richiedere un po' di tempo. Tuttavia, una volta che ti si abituerai , ne apprezzerai la semplicità e la potenza.
Non esitate a chiedere aiuto su questo argomento in una qualsiasi delle comunità online di Godot e, una volta che avrai acquisito abbastanza esperienza, aiuta anche gli altri!