Matrices y transformaciones

Introducción

Antes de leer este tutorial, recomendamos leer y entender el tutorial anterior Matemáticas vectoriales, ya que éste requiere conocimiento sobre vectores.

Este tutorial trata sobre las transformaciones y cómo las representamos en Godot usando matrices. No es una guía completa y detallada de las matrices. Las transformaciones se aplican la mayoría de las veces como traslación, rotación y escala, por lo que nos centraremos en cómo representarlas con matrices.

La mayor parte de esta guía se centra en el 2D, usando Transform2D y Vector2, pero la forma en que funcionan las cosas en el 3D es muy similar.

Nota

Como se mencionó en el tutorial anterior, es importante recordar que en Godot, el eje Y apunta abajo en 2D. Esto es lo opuesto a como la mayoría de las escuelas enseñan álgebra lineal, con el eje Y apuntando hacia arriba.

Nota

La convención es que el eje X es rojo, el eje Y es verde y el eje Z es azul. Este tutorial está codificado por colores para coincidir con estas convenciones, pero también representaremos el vector de origen con un color azul.

Componentes de la matriz y la matriz de Identidad

La matriz de identidad representa una transformación sin traslación, sin rotación y sin escala. Empecemos por mirar la matriz de identidad y cómo sus componentes se relacionan con la forma en que aparece visualmente.

../../_images/identity.png

Las matrices tienen filas y columnas, y una matriz de transformación tiene convenciones específicas sobre lo que hace cada una.

En la imagen de arriba, podemos ver que el vector X rojo está representado por la primera columna de la matriz, y el vector Y verde está igualmente representado por la segunda columna. Un cambio en las columnas cambiará estos vectores. Veremos cómo pueden ser manipulados en los próximos ejemplos.

No debes preocuparte por manipular las filas directamente, ya que normalmente trabajamos con columnas. Sin embargo, se puede pensar en las filas de la matriz como mostrando qué vectores contribuyen a moverse en una dirección determinada.

Cuando nos referimos a un valor como t.x.y, ese es el componente Y del vector de la columna X. En otras palabras, la parte inferior izquierda de la matriz. De manera similar, t.x.x está arriba a la izquierda, t.y.x está arriba a la derecha, y t.y.y está abajo a la derecha, donde t es la Transform2D.

Escalando la matriz de transformación

La aplicación de una escala es una de las operaciones más fáciles de entender. Empecemos colocando el logo de Godot debajo de nuestros vectores para que podamos ver los efectos en un objeto:

../../_images/identity-godot.png

Ahora, para escalar la matriz, todo lo que tenemos que hacer es multiplicar cada componente por la escala que queremos. Escalémosla por 2. 1 por 2 se convierte en 2, y 0 por 2 se convierte en 0, así que terminamos con esto:

../../_images/scale.png

Para hacerlo en código, podemos simplemente multiplicar cada uno de los vectores:

var t = Transform2D()
# Scale
t.x *= 2
t.y *= 2
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Scale
t.x *= 2;
t.y *= 2;
Transform = t; // Change the node's transform to what we just calculated.

Si quisiéramos devolverlo a su escala original, podemos multiplicar cada componente por 0.5. Eso es más o menos todo lo que hay que hacer para escalar una matriz de transformación.

Para calcular la escala del objeto a partir de una matriz de transformación existente, puedes usar length()` en cada uno de los vectores de la columna.

Nota

En los proyectos reales, puedes usar el método scaled() para realizar el escalado.

Rotando la matriz de transformación

Empezaremos de la misma manera que antes, con el logo de Godot debajo de la matriz de identidad:

../../_images/identity-godot.png

Como ejemplo, digamos que queremos girar nuestro logo de Godot en el sentido de las agujas del reloj en 90 grados. Ahora mismo el eje X apunta a la derecha y el eje Y apunta hacia abajo. Si lo rotamos en nuestra cabeza, lógicamente veríamos que el nuevo eje X apunta hacia abajo y el nuevo eje Y hacia la izquierda.

Puedes imaginar que tomas el logo de Godot y sus vectores, y luego lo giras alrededor del centro. Dondequiera que termines de girar, la orientación de los vectores determina lo que es la matriz.

Necesitamos representar "abajo" e "izquierda" en coordenadas normales, lo que significa que pondremos X en (0, 1) e Y en (-1, 0). Estos son también los valores de Vector 2.DOWN y Vector 2.LEFT. Cuando hacemos esto, obtenemos el resultado deseado de rotar el objeto:

../../_images/rotate1.png

Si tiene problemas para entender lo anterior, intente este ejercicio: Corte un cuadrado de papel, dibuju los vectores X e Y encima de él, colócalo en un papel cuadriculado, luego gíralo y anota los puntos finales.

Para realizar la rotación en el código, necesitamos ser capaces de calcular los valores de forma programada. Esta imagen muestra las fórmulas necesarias para calcular la matriz de transformación desde un ángulo de rotación. No te preocupes si esta parte parece complicada, te prometo que es lo más difícil que necesitas saber.

../../_images/rotate2.png

Nota

Godot representa todas las rotaciones con radianes, no grados. Un giro completo es TAU o PI*2 radianes, y un cuarto de giro de 90 grados es TAU/4 o PI/2 radianes. Trabajar con TAU usualmente resulta en un código más legible.

Nota

Dato curioso: Además de que la Y está abajo en Godot, la rotación se representa en el sentido de las agujas del reloj. Esto significa que todas las funciones matemáticas y de trigonometría se comportan igual que en un sistema de sentido de las horas de un reloj con la Y hacia arriba, ya que estas diferencias "se cancelan". Se puede pensar que las rotaciones en ambos sistemas son "de X a Y".

Para realizar una rotación de 0.5 radianes (unos 28,65 grados), simplemente conectamos un valor de 0.5 a la fórmula anterior y evaluamos para encontrar cuáles deberían ser los valores reales:

../../_images/rotate3.png

Así es como se haría en código (colocar el script en un Nodo2D):

var rot = 0.5 # The rotation to apply.
var t = Transform2D()
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
transform = t # Change the node's transform to what we just calculated.
float rot = 0.5f; // The rotation to apply.
Transform2D t = Transform2D.Identity;
t.x.x = t.y.y = Mathf.Cos(rot);
t.x.y = t.y.x = Mathf.Sin(rot);
t.y.x *= -1;
Transform = t; // Change the node's transform to what we just calculated.

Para calcular la rotación del objeto a partir de una matriz de transformación existente, puedes usar atan2(t.x.y, t.x.x), donde t es la Transform2D.

Nota

En los proyectos reales, puedes usar el método rotated() para realizar rotaciones.

La base de la matriz de transformación

Hasta ahora sólo hemos trabajado con los vectores x y y, que se encargan de representar la rotación, la escala y/o el esquilado (avanzado, cubierto al final). Los vectores X e Y juntos son llamados la base de la matriz de transformación. Es importante conocer los términos "base" y "vectores base".

Habrás notado que Transform2D tiene en realidad tres valores Vector2: x, y y "origen". El valor de "origen" no es parte de la base, pero es parte de la transformación, y lo necesitamos para representar la posición. A partir de ahora seguiremos el vector de origen en todos los ejemplos. Puedes pensar en el origen como en otra columna, pero a menudo es mejor pensar en él como algo completamente separado.

Nótese que en 3D, Godot tiene una estructura Basis separada para mantener los tres valores Vector3 de la base, ya que el código puede volverse complejo y tiene sentido separarlo de Transform (que está compuesto por un Basis y un extra Vector3 para el origen).

Trasladando la matriz de transformación

Cambiar el vector de origen se llama "transladar" la matriz de transformación. Transladar es básicamente un término técnico para "mover" el objeto, pero explícitamente no implica ninguna rotación.

Trabajemos con un ejemplo para ayudar a entender esto. Empezaremos con la transformación de identidad como la última vez, excepto que esta vez seguiremos el vector de origen.

../../_images/identity-origin.png

Si queremos que el objeto se mueva a una posición de (1, 2), sólo tenemos que poner su vector de origen en (1, 2):

../../_images/translate.png

También hay un método translated(), que realiza una operación diferente a la de añadir o cambiar el origen directamente. El método translated() traducirá el objeto relativo a su propia rotación. Por ejemplo, un objeto rotado 90 grados en el sentido de las agujas del reloj se moverá a la derecha cuando se translated() con Vector 2.UP.

Nota

El 2D de Godot usa coordenadas basadas en píxeles, por lo que en los proyectos reales querrás transladarlos por cientos de unidades.

Poniendo todo junto

Vamos a aplicar todo lo que hemos mencionado hasta ahora en una transformación. Para seguir, crear un proyecto simple con un nodo Sprite y usar el logo de Godot para el recurso de la textura.

Pongamos la traslación a (350, 150), rotar por -0,5 rad, y escalar por 3. He publicado una captura de pantalla, y el código para reproducirla, ¡pero os animo a intentar reproducir la captura de pantalla sin mirar el código!

../../_images/putting-all-together.png
var t = Transform2D()
# Translation
t.origin = Vector2(350, 150)
# Rotation
var rot = -0.5 # The rotation to apply.
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
# Scale
t.x *= 3
t.y *= 3
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Translation
t.origin = new Vector2(350, 150);
// Rotation
float rot = -0.5f; // The rotation to apply.
t.x.x = t.y.y = Mathf.Cos(rot);
t.x.y = t.y.x = Mathf.Sin(rot);
t.y.x *= -1;
// Scale
t.x *= 3;
t.y *= 3;
Transform = t; // Change the node's transform to what we just calculated.

Inclinando la matrix transformada (Avanzado)

Nota

Si sólo buscas cómo usar las matrices de transformación, no dudes en saltarte esta sección del tutorial. Esta sección explora un aspecto poco común de las matrices de transformación con el propósito de construir una comprensión de las mismas.

Habrá notado que una transformación tiene más grados de libertad que la combinación de las acciones anteriores. La base de una matriz de transformación 2D tiene cuatro números totales en dos clase_Vector2 valores, mientras que un valor de rotación y un Vector2 para la escala sólo tiene 3 números. El concepto de alto nivel para el grado de libertad que falta se llama shearing.

Normalmente, siempre tendrá los vectores base perpendiculares entre sí. Sin embargo, la inclinación puede ser útil en algunas situaciones, y comprender la inclinación le ayuda a entender cómo funcionan las transformaciones.

Para mostrar visualmente cómo se verá, superpongamos un grid sobre el logo de Godot:

../../_images/identity-grid.png

Cada punto de este grid se obtiene sumando los vectores base. La esquina inferior derecha es X + Y, mientras que la esquina superior derecha es X - Y. Si cambiamos los vectores base, toda el grid se mueve con el, ya que la grid está compuesta de los vectores base. Todas las líneas del grid que son actualmente paralelas permanecerán paralelas sin importar los cambios que hagamos en los vectores base.

Como ejemplo, pongamos Y a (1, 1):

../../_images/shear.png
var t = Transform2D()
# Shear by setting Y to (1, 1)
t.y = Vector2.ONE
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Shear by setting Y to (1, 1)
t.y = Vector2.One;
Transform = t; // Change the node's transform to what we just calculated.

Nota

No puedes establecer los valores crudos de un Transform2D en el editor, así que debes usar código si quieres inclinar el objeto.

Debido a que los vectores ya no son perpendiculares, el objeto ha sido inclinado. El centro inferior del grid, que es (0, 1) con respecto a sí mismo, se encuentra ahora en una posición mundial de (1, 1).

Las coordenadas intra-objeto se llaman coordenadas UV en las texturas, así que tomemos prestada esa terminología para aquí. Para encontrar la posición del mundo desde una posición relativa, la fórmula es U * X + V * Y, donde U y V son números y X e Y son los vectores base.

La esquina inferior derecha del grid, que siempre está en la posición UV de (1, 1), está en la posición mundial de (2, 1), que se calcula a partir de X*1 + Y*1, que es (1, 0) + (1, 1), o (1 + 1, 0 + 1), o (2, 1). Esto coincide con nuestra observación de dónde está la esquina inferior derecha de la imagen.

De manera similar, la esquina superior derecha del grid, que siempre está en la posición UV de (1, -1), está en la posición mundial de (0, -1), que se calcula a partir de X*1 + Y*-1, que es (1, 0) - (1, 1), o (1 - 1, 0 - 1), o (0, -1). Esto concuerda con nuestra observación de dónde está la esquina superior derecha de la imagen.

Esperemos que ahora entiendas completamente cómo una matriz de transformación afecta al objeto, y la relación entre los vectores base y cómo los "UV" o "intra-coordinados" del objeto han cambiado su posición en el mundo.

Nota

En Godot, toda la matemática de transformación se hace en relación con el nodo padre. Cuando nos referimos a la "posición del mundo", eso sería relativo al padre del nodo, si el nodo tuviera un padre.

Si desea una explicación adicional, debería ver el excelente video de 3Blue1Brown sobre las transformaciones lineales: https://www.youtube.com/watch?v=kYB8IZa5AuE

Aplicaciones prácticas de las transformaciones

En los proyectos reales, normalmente trabajarás con transformaciones dentro de transformaciones teniendo múltiples nodos Node2D o Spatial separados unos de otros.

Sin embargo, a veces es muy útil calcular manualmente los valores que necesitamos. Repasaremos cómo podrías usar Transform2D o Transform para calcular manualmente las transformaciones de los nodos.

Convertir las posiciones entre las transformaciones

Hay muchos casos en los que se querría convertir una posición dentro y fuera de una transformación. Por ejemplo, si tienes una posición relativa al jugador y te gustaría encontrar la posición mundial (parentesco con los padres), o si tienes una posición mundial y quieres saber dónde está relativa al jugador.

Podemos encontrar lo que un vector relativo al jugador sería definido en el espacio mundial como usando el método "xform":

# World space vector 100 units below the player.
print(transform.xform(Vector2(0, 100)))
// World space vector 100 units below the player.
GD.Print(Transform.Xform(new Vector2(0, 100)));

Y podemos usar el método "xform_inv" para encontrar la posición del espacio mundial que sería si en su lugar se definiera en relación con el jugador:

# Where is (0, 100) relative to the player?
print(transform.xform_inv(Vector2(0, 100)))
// Where is (0, 100) relative to the player?
GD.Print(Transform.XformInv(new Vector2(0, 100)));

Nota

Si se sabe de antemano que la transformación está posicionada en (0, 0), se pueden utilizar los métodos "base_xform" o "base_xform_inv" en su lugar, que se saltan el tratar con la translación.

Mover un objeto en relación a sí mismo

Una operación común, sobre todo en los juegos 3D, es mover un objeto relativo a sí mismo. Por ejemplo, en los juegos de disparos en primera persona, querrías que el personaje se moviera hacia adelante (eje -Z) cuando presionas W.

Dado que los vectores base son la orientación relativa al padre, y el vector de origen es la posición relativa al padre, podemos simplemente añadir múltiplos de los vectores base para mover un objeto con respecto a sí mismo.

Este código mueve un objeto 100 unidades a su propio derecho:

transform.origin += transform.x * 100
Transform2D t = Transform;
t.origin += t.x * 100;
Transform = t;

Para moverse en 3D, necesitarías reemplazar "x" por "base.x".

Nota

En los proyectos reales, puedes usar "translate_object_local" en 3D o "move_local_x" y "move_local_y" en 2D para hacer esto.

Aplicando transformaciones sobre transformaciones

Una de las cosas más importantes que hay que saber sobre las transformaciones es cómo se pueden usar varias de ellas juntas. La transformación de un nodo padre afecta a todos sus hijos. Diseccionemos un ejemplo.

En esta imagen, el nodo hijo tiene un "2" después de los nombres de los componentes para distinguirlos del nodo padre. Puede parecer un poco abrumador con tantos números, pero recuerde que cada número se muestra dos veces (junto a las flechas y también en las matrices), y que casi la mitad de los números son cero.

../../_images/apply.png

Las únicas transformaciones que ocurren aquí son que al nodo padre se le ha dado una escala de (2, 1), al hijo se le ha dado una escala de (0.5, 0.5), y a ambos nodos se les ha dado posiciones.

Todas las transformaciones de los hijos se ven afectadas por las transformaciones de los padres. El hijo tiene una escala de (0.5, 0.5), por lo que se esperaría que fuera un cuadrado de proporción 1:1, y lo es, pero sólo en relación con el padre. El vector X del hijo termina siendo (1, 0) en el espacio mundial, porque está escalado por los vectores base del progenitor. De manera similar, el vector nodo hijo origen se establece en (1, 1), pero en realidad lo mueve (2, 1) en el espacio mundial, debido a los vectores base al nodo padre.

Para calcular manualmente la transformación espacial del mundo de un hijo, este es el código que usaríamos:

# Set up transforms just like in the image, except make positions be 100 times bigger.
var parent = Transform2D(2, 0, 0, 1, 100, 200)
var child = Transform2D(0.5, 0, 0, 0.5, 100, 100)

# Calculate the child's world space transform
# origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
var origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin
# basis_x = (2, 0) * 0.5 + (0, 1) * 0
var basis_x = parent.x * child.x.x + parent.y * child.x.y
# basis_y = (2, 0) * 0 + (0, 1) * 0.5
var basis_y = parent.x * child.y.x + parent.y * child.y.y

# Change the node's transform to what we just calculated.
transform = Transform2D(basis_x, basis_y, origin)
// Set up transforms just like in the image, except make positions be 100 times bigger.
Transform2D parent = new Transform2D(2, 0, 0, 1, 100, 200);
Transform2D child = new Transform2D(0.5f, 0, 0, 0.5f, 100, 100);

// Calculate the child's world space transform
// origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
Vector2 origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin;
// basisX = (2, 0) * 0.5 + (0, 1) * 0 = (0.5, 0)
Vector2 basisX = parent.x * child.x.x + parent.y * child.x.y;
// basisY = (2, 0) * 0 + (0, 1) * 0.5 = (0.5, 0)
Vector2 basisY = parent.x * child.y.x + parent.y * child.y.y;

// Change the node's transform to what we just calculated.
Transform = new Transform2D(basisX, basisY, origin);

En los proyectos reales, podemos encontrar la transformación del mundo del hijo aplicando una transformada sobre otra usando el operador *:

# Set up transforms just like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Change the node's transform to what would be the child's world transform.
transform = parent * child
// Set up transforms just like in the image, except make positions be 100 times bigger.
Transform2D parent = new Transform2D(2, 0, 0, 1, 100, 200);
Transform2D child = new Transform2D(0.5f, 0, 0, 0.5f, 100, 100);

// Change the node's transform to what would be the child's world transform.
Transform = parent * child;

Nota

¡Al multiplicar las matrices, el orden importa! No las mezcles.

Por último, la aplicación de la transformación de la identidad siempre hará nada.

Si desea una explicación adicional, debería ver el excelente vídeo de 3Blue1Brown sobre la composición de la matriz: https://www.youtube.com/watch?v=XkY2DOUCWMU

Invirtiendo la matriz de transformación

La función "affine_inverse" devuelve una transformación que "deshace" la transformación anterior. Esto puede ser útil en algunas situaciones, pero es más fácil dar sólo algunos ejemplos.

Multiplicar una transformación inversa por la transformación normal deshace todas las transformaciones:

var ti = transform.affine_inverse()
var t = ti * transform
# The transform is the identity transform.
Transform2D ti = Transform.AffineInverse();
Transform2D t = ti * Transform;
// The transform is the identity transform.

La transformación de una posición por una transformación y su inversa resulta en la misma posición (lo mismo para "xform_inv"):

var ti = transform.affine_inverse()
position = transform.xform(position)
position = ti.xform(position)
# The position is the same as before.
Transform2D ti = Transform.AffineInverse();
Position = Transform.Xform(Position);
Position = ti.Xform(Position);
// The position is the same as before.

¿Cómo funciona todo en 3D?

Una de las grandes cosas de las matrices de transformación es que funcionan de manera muy similar entre las transformaciones 2D y 3D. Todo el código y las fórmulas utilizadas anteriormente para 2D funcionan igual en 3D, con 3 excepciones: la adición de un tercer eje, que cada eje es del tipo clase_Vector3, y también que Godot almacena la class_Base por separado de la Transform, ya que las matemáticas pueden volverse complejas y tiene sentido separarlas.

Todos los conceptos de cómo funciona la traslación, rotación, escala e inclinación en 3D son todos iguales comparados con los de 2D. Para escalar, tomamos cada componente y lo multiplicamos; para rotar, cambiamos donde cada vector base está apuntando; para traducir, manipulamos el origen; y para inclinar, cambiamos los vectores base para que no sean perpendiculares.

../../_images/3d-identity.png

Si quieres, es una buena idea jugar con las transformaciones para entender cómo funcionan. Godot te permite editar matrices de transformación 3D directamente desde el inspector. Puedes descargar este proyecto que tiene líneas y cubos de colores para ayudar a visualizar los vectores Basis y el origen tanto en 2D como en 3D: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

Nota

La sección "Matrix" del Espacio en el inspector de Godot 3.2 muestra la matriz como se ha transpuesto, con las columnas horizontales y las filas verticales. Esto puede cambiarse para que sea menos confuso en una futura versión de Godot.

Nota

No puedes editar la matriz de transformación de Node2D directamente en el inspector de Godot 3.2. Esto puede ser cambiado en una futura versión de Godot.

Si desea una explicación adicional, debería ver el excelente vídeo de 3Blue1Brown sobre las transformaciones lineales en 3D: https://www.youtube.com/watch?v=rHLEWRxRGiM

Representando una rotación en 3D (avanzado)

La mayor diferencia entre las matrices de transformación 2D y 3D es cómo se representa la rotación por sí misma sin los vectores base.

Con el 2D, tenemos una forma fácil (atan2) de cambiar entre una matriz de transformación y un ángulo. En 3D, no podemos simplemente representar la rotación como un número. Hay algo llamado ángulos de Euler, que pueden representar las rotaciones como un conjunto de 3 números, sin embargo, son limitados y no muy útiles, excepto para casos triviales.

En la 3D no solemos utilizar ángulos, o bien utilizamos una base de transformación (utilizada prácticamente en todas partes en Godot), o bien utilizamos cuaternarios. Godot puede representar cuaternarios usando la estructura Quat. Mi sugerencia es ignorar completamente cómo funcionan bajo el capó, porque son muy complicados y poco intuitivos.

Sin embargo, si realmente debes saber cómo funciona, aquí hay algunos grandes recursos, que puedes seguir en orden:

https://www.youtube.com/watch?v=mvmuCPvRoWQ

https://www.youtube.com/watch?v=d4EgbgTm0Bg

https://eater.net/quaternions