Matrices y transformaciones

Introducción

Antes de leer este tutorial, se recomienda leer el anterior sobre Matemáticas vectoriales ya que éste es una continuación directa.

Este tutorial será sobre transformaciones y cubrirá un poco sobre matrices (pero no en profundidad).

Las transformaciones se aplican la mayor parte del tiempo como traslación, rotación y escala, por lo que se considerarán prioritarias aquí.

Orientación de un sistema de coordenadas (OCS)

Imagina que tenemos una nave espacial en el espacio. En Godot esto es fácil, basta con mover la nave hacia algún sitio y rotarla:

../../_images/tutomat1.png

Ok, entonces en 2D esto parece simple, una posición y un ángulo para una rotación. Pero recuerda, somos adultos aquí y no usamos ángulos (además, los ángulos ni siquiera son tan útiles cuando se trabaja en 3D).

Deberíamos darnos cuenta de que en algún momento, alguien diseñó esta nave espacial. Ya sea para 2D en un dibujo como Paint.net, Gimp, Photoshop, etc. o en 3D a través de una herramienta DCC 3D como Blender, Max, Maya, etc.

Cuando se diseñó, no estaba rotado. Fue diseñado en su propio sistema de coordenadas.

../../_images/tutomat2.png

Esto significa que la punta de la nave tiene una coordenada, la aleta tiene otra, etc. Ya sea en píxeles (2D) o vértices (3D).

Así, recordemos de nuevo que la nave estaba en algún lugar en el espacio:

../../_images/tutomat3.png

¿Cómo llegó a estar allí? ¿Qué la trasladó y la giró desde el lugar en que fue diseñado a su posición actual? La respuesta es… un transform; la nave fue transformada desde su posición original a la nueva. Esto permite que la nave se muestre donde está.

Pero “transformar” es un término muy genérico para describir este proceso. Para solucionar este puzle, superpondremos la posición original de diseño de la nave en su posición actual:

../../_images/tutomat4.png

Entonces, podemos ver que el «espacio diseño» también ha sido transformado. ¿Cuál es la manera mejor de representar esta transformación? Usemos 3 vectores para esto (en 2D): un vector unitario que señala hacia el eje X positivo, un vector unitario que señala hacia el Y positivo y un desplazamiento.

../../_images/tutomat5.png

Llamemos los vectores «X», «Y» y «Origin», y los superpongamos en la nave para que tenga más sentido:

../../_images/tutomat6.png

Vale, esto es mejor, pero todavía no tiene sentido. ¿Qué tienen que ver los vectores X, Y y Origin con cómo la nave llegó allí?

Bueno, tomemos como referencia el extremo superior de la nave:

../../_images/tutomat7.png

Y vamos a aplicarle la siguiente operación (y también a todos los puntos de la nave, pero rastrearemos la parte superior como nuestro punto de referencia):

var new_pos = pos - origin
var newPosition = pos - origin;

Haciendo esto al punto seleccionado lo moverá de nuevo al centro:

../../_images/tutomat8.png

Esto era de esperar, pero entonces hagamos algo más interesante. Utiliza el producto de X y el punto, y añádelo al producto de Y y el punto:

var final_pos = Vector2(x.dot(new_pos), y.dot(new_pos))
var finalPosition = new Vector2(x.Dot(newPosition), y.Dot(newPosition));

Entonces lo que tenemos es… espera un minuto, ¡es la nave en su posición de diseño!

../../_images/tutomat9.png

¿Cómo sucedió esta magia negra? ¡La nave se perdió en el espacio y ahora ha vuelto a casa!

Puede parecer extraño, pero tiene mucha lógica. Recuerda, como hemos visto en el Matemáticas vectoriales, lo que pasó es que la distancia al eje X, y la distancia al eje Y fueron calculadas. El cálculo de la distancia en una dirección o plano fue uno de los usos del producto punto. Esto fue suficiente para recuperar las coordenadas de diseño de cada punto de la nave.

Por lo tanto, lo que hemos estado trabajando hasta ahora (con X, Y y Origin) es un Sistema de Coordenadas Orientado. X e Y es la Base, y Origin es el desplazamiento.

Basis

Sabemos lo que es Origin. Es donde termina el 0,0 (origin) del sistema de coordenadas de diseño después de ser transformado a una nueva posición. Esta es la razón por la que se llama Origin («Origen»), pero en la práctica, es sólo una compensación a la nueva posición.

La Base es más interesante. La base es la dirección de X e Y en el OCS desde la nueva ubicación transformada. Indica lo que ha cambiado, tanto en 2D como en 3D. Origin (desplazamiento) y Base (dirección) comunicarán «Hey, los ejes X e Y originales de tu diseño están aquí, apuntando hacia estas direcciones».

Entonces, cambiemos la representación de la base. En lugar de 2 vectores, usemos una matriz.

../../_images/tutomat10.png

Los vectores están arriba en la matriz, horizontalmente. El siguiente problema ahora es que… ¿qué es eso de la matriz? Bueno, asumiremos que nunca has oído hablar de una matriz.

Transformaciones en Godot

Este tutorial no explicará las matemáticas de la matriz (y sus operaciones) en profundidad, sólo su uso práctico. Hay mucho material para eso, que debería ser mucho más fácil de entender después de completar este tutorial. Sólo explicaremos cómo usar las transformaciones.

Transform2D

class_Transform2D es una matriz de 3x2. Tiene 3 elementos Vector2 y se usa para 2D. El eje «X» es el elemento 0, el eje «Y» es el elemento 1 y el «Origin» es el elemento 2. No está dividido en base/origen por conveniencia, debido a su simplicidad.

var m = Transform2D()
var x = m[0] # 'X'
var y = m[1] # 'Y'
var o = m[2] # 'Origin'
var m = new Transform2D();
Vector2 x = m[0]; // 'X'
Vector2 y = m[1]; // 'Y'
Vector2 o = m[2]; // 'Origin'

La mayoría de las operaciones se explicarán con este tipo de datos (Transform2D), pero la misma lógica se aplica al 3D.

Identidad

Una transformación importante es la matriz de «identidad». Esto significa:

  • “X” Apunta a la derecha: Vector2(1,0)
  • “Y” Apunta hacia arriba (o hacia abajo en píxeles): Vector2(0,1)
  • “Origin” es el origen Vector2(0,0)
../../_images/tutomat11.png

Es fácil adivinar que una matriz de identidad es sólo una matriz que alinea la transformación con su sistema de coordenadas padre. Es un OCS que no ha sido traducido, rotado o escalado.

# The Transform2D constructor will default to Identity
var m = Transform2D()
print(m)
# prints: ((1, 0), (0, 1), (0, 0))
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultTransform = new Transform2D();
GD.Print(defaultTransform);
// prints: ((0, 0), (0, 0), (0, 0))

// Instead we can use the Identity property.
var identityTransform = Transform2D.Identity;
GD.Print(identityTransform);
// prints: ((1, 0), (0, 1), (0, 0))

Operaciones

Rotación

La rotación de Transform2D se realiza mediante la función «rotated»:

var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
../../_images/tutomat12.png

Traslación

Hay dos maneras de trasladar un Transform2D, la primera es moviendo el origen:

# Move 2 units to the right
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m[2] += Vector2(2,0)
// Move 2 units to the right
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m[2] += new Vector2(2, 0);
../../_images/tutomat13.png

Esto siempre funcionará en coordenadas globales.

En cambio, si se desea realizar la traslación en coordenadas locales de la matriz (hacia donde se orienta la base), está el método Transform2D.translated():

# Move 2 units towards where the basis is oriented
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m = m.translated( Vector2(2,0) )
// Move 2 units towards where the basis is oriented
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m = m.Translated(new Vector2(2, 0));
../../_images/tutomat14.png

También se pueden transformar manualmente las coordenadas globales en coordenadas locales:

var local_pos = m.xform_inv(point)
var localPosition = m.XformInv(point);

Pero aún mejor, hay funciones helper para esto como se puede leer en las siguientes secciones.

Coordenadas locales a globales y viceversa

Hay métodos helper para convertir entre coordenadas locales y globales.

Existen Node2D.to_local() y Node2D.to_global() tanto para 2D como para Spatial.to_local() y :ref:`Spatial.to_global() <class_Spatial_method_to_global>`para 3D.

Escala

Una matriz también se puede escalar. La escala multiplicará los vectores base por un vector (vector X por componente x de la escala, vector Y por componente y de la escala). Dejará el origen en paz:

# Make the basis twice its size.
var m = Transform2D()
m = m.scaled( Vector2(2,2) )
// Make the basis twice its size.
var m = Transform2D.Identity;
m = m.Scaled(new Vector2(2, 2));
../../_images/tutomat15.png

Este tipo de operaciones en matrices son acumulativas. Significa que cada uno comienza en relación con el anterior. Para aquellos que han estado viviendo en este planeta el tiempo suficiente, una buena referencia de cómo funciona la transformación es ésta:

../../_images/tutomat16.png

Una matriz se usa de manera similar a una tortuga. Lo más probable es que la tortuga tuviera una matriz dentro (y es probable que estés aprendiendo esto muchos años después de descubrir a Santa Claus no es real).

Transformar

La transformación es el acto de cambiar entre sistemas de coordenadas. Para convertir una posición (2D o 3D) del «diseñador» del sistema de coordenadas al OCS, se utiliza el método «xform».

var new_pos = m.xform(pos)
var newPosition = m.Xform(position);

Y sólo para la base (sin traducción):

var new_pos = m.basis_xform(pos)
var newPosition = m.BasisXform(position);

Transformación inversa

Para hacer la operación opuesta (lo que hicimos con el cohete), se utiliza el método «xform_inv»:

var new_pos = m.xform_inv(pos)
var newPosition = m.XformInv(position);

Sólo para la Base:

var new_pos = m.basis_xform_inv(pos)
var newPosition = m.BasisXformInv(position);

Matrices ortonormales

Sin embargo, si la matriz ha sido escalada (los vectores no son de longitud unitaria), o los vectores de base no son ortogonales (90°), la transformación inversa no funcionará.

En otras palabras, la transformación inversa sólo es válida en matrices ortonormales. Para esto, estos casos se debe computar un inverso afín.

La transformación, o transformación inversa, de una matriz de identidad devolverá la posición sin cambios:

# Does nothing, pos is unchanged
pos = Transform2D().xform(pos)
// Does nothing, position is unchanged
position = Transform2D.Identity.Xform(position);

Afín inverso

El inverso afín es una matriz que hace la operación inversa de otra matriz, no importa si la matriz tiene escala o los vectores del eje no son ortogonales. El inverso afín se calcula con el método affine_inverse():

var mi = m.affine_inverse()
pos = m.xform(pos)
pos = mi.xform(pos)
# pos is unchanged
var mi = m.AffineInverse();
position = m.Xform(position);
position = mi.Xform(position);
// position is unchanged

Si la matriz es ortonormal, entonces:

# if m is orthonormal, then
pos = mi.xform(pos)
# is the same is
pos = m.xform_inv(pos)
// if m is orthonormal, then
position = mi.Xform(position);
// is the same is
position = m.XformInv(position);

Multiplicación de matrices

Las matrices se pueden multiplicar. Multiplicación de dos matrices «encadenan» (concatenan) sus transformaciones.

Sin embargo, según la convención, la multiplicación tiene lugar en orden inverso.

Ejemplo:

var m = more_transforms * some_transforms
var m = moreTransforms * someTransforms;

Para que quede un poco más claro, esto:

pos = transform1.xform(pos)
pos = transform2.xform(pos)
position = transform1.Xform(position);
position = transform2.Xform(position);

Es lo mismo que:

# note the inverse order
pos = (transform2 * transform1).xform(pos)
// note the inverse order
position = (transform2 * transform1).Xform(position);

Sin embargo, esto no es lo mismo:

# yields a different results
pos = (transform1 * transform2).xform(pos)
// yields a different results
position = (transform1 * transform2).Xform(position);

Porque en matemáticas matriciales, A * B no es lo mismo que B * A.

Multiplicación por el inverso

Multiplicar una matriz por su inverso, resulta en una identidad:

# No matter what A is, B will be identity
var B = A.affine_inverse() * A
// No matter what A is, B will be identity
var B = A.AffineInverse() * A;

Multiplicación por la identidad

Multiplicando una matriz por la identidad, resultará en la matriz sin cambios:

# B will be equal to A
B = A * Transform2D()
// B will be equal to A
var B = A * Transform2D.Identity;

Consejos sobre matrices

Al utilizar una jerarquía de transformación, recuerda que la multiplicación de matrices se invierte. Para obtener la transformación global para una jerarquía, haz:

var global_xform = parent_matrix * child_matrix
var globalTransform = parentMatrix * childMatrix;

Para 3 niveles:

var global_xform = gradparent_matrix * parent_matrix * child_matrix
var globalTransform = grandparentMatrix * parentMatrix * childMatrix;

Para hacer una matriz relativa al padre, hay que utilizar el inverso afín (o el inverso regular para matrices ortonormales).

# transform B from a global matrix to one local to A
var B_local_to_A = A.affine_inverse() * B
// transform B from a global matrix to one local to A
var bLocalToA = A.AffineInverse() * B;

Revertirlo como en el ejemplo anterior:

# transform back local B to global B
B = A * B_local_to_A
// transform back local B to global B
B = A * bLocalToA;

¡Bien, espero que esto sea suficiente! Vamos a completar el tutorial moviéndonos a matrices 3D.

Matrices y transformaciones en 3D

Como se ha mencionado anteriormente, para 3D, tratamos con 3 Vector3 vectores para la matriz de rotación, y uno extra para el origen.

Basis

Godot tiene un tipo especial para una matriz 3x3, llamado Basis. Se puede utilizar para representar una rotación y escala 3D. Los subvectores se acceden así:

var m = Basis()
var x = m[0] # Vector3
var y = m[1] # Vector3
var z = m[2] # Vector3
var m = new Basis();
Vector3 x = m[0];
Vector3 y = m[1];
Vector3 z = m[2];

O, de forma alternativa, como:

var m = Basis()
var x = m.x # Vector3
var y = m.y # Vector3
var z = m.z # Vector3
var m = new Basis();
Vector3 x = m.x;
Vector3 y = m.y;
Vector3 z = m.z;

La Identidad de Basis tiene los siguientes valores:

../../_images/tutomat17.png

Y se puede acceder así:

# The Basis constructor will default to Identity
var m = Basis()
print(m)
# prints: ((1, 0, 0), (0, 1, 0), (0, 0, 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);;
// prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))

Rotación en 3D

La rotación en 3D es más compleja que en 2D (traslation y scale son iguales), porque la rotación es una operación 2D implícita. Para rotar en 3D, se debe seleccionar un eje. La rotación, entonces, ocurre alrededor de este eje.

El eje para la rotación debe ser un vector normal. Es decir, un vector que puede apuntar a cualquier dirección, pero la longitud debe ser uno (1.0).

#rotate in Y axis
var m3 = Basis()
m3 = m3.rotated( Vector3(0,1,0), PI/2 )
// rotate in Y axis
var m3 = Basis.Identity;
m3 = m3.Rotated(new Vector3(0, 1, 0), Mathf.Pi / 2);

Transformar

Para añadir el componente final a la mezcla, Godot proporciona el tipo Transform. Transform tiene dos miembros:

  • basis (de tipo Basis)
  • origin (de tipo Vector3)

Cualquier transformación 3D puede ser representada con Transform, y la separación de la base y el origen hace más fácil trabajar el traslado y la rotación por separado.

Un ejemplo:

var t = Transform()
pos = t.xform(pos) # transform 3D position
pos = t.basis.xform(pos) # (only rotate)
pos = t.origin + pos # (only translate)
var t = new Transform(Basis.Identity, Vector3.Zero);
position = t.Xform(position); // transform 3D position
position = t.basis.Xform(position); // (only rotate)
position = t.origin + position; // (only translate)