Matemáticas vectoriales

Introducción

Este tutorial es una breve y práctica introducción al álgebra lineal aplicada al desarrollo de juegos. El álgebra lineal es el estudio de los vectores y sus usos. Los vectores tienen muchas aplicaciones tanto en el desarrollo 2D como 3D y Godot los utiliza ampliamente. Desarrollar una buena comprensión de las matemáticas vectoriales es esencial para convertirse en un desarrollador de juegos sólido.

Nota

Este tutorial no es un libro de texto formal sobre álgebra lineal. Sólo veremos cómo se aplica al desarrollo de juegos. Para una visión más amplia de las matemáticas, consulta https://es.khanacademy.org/math/linear-algebra

Sistemas de coordenadas (2D)

En el espacio 2D, las coordenadas se definen usando un eje horizontal (x) y un eje vertical (y). Una posición concreta en un espacio 2D se escribe como un par de valores como (4, 3).

../../_images/vector_axis1.png

Nota

Si eres nuevo en el mundo de los gráficos por computadora, puede parecer extraño que el eje positivo y apunte hacia abajo en lugar de hacia arriba, como probablemente aprendiste en la clase de matemáticas. Sin embargo, esto es común en la mayoría de las aplicaciones de gráficos por computadora.

Cualquier posición en el plano 2D puede ser identificada por un par de números de esta manera. Sin embargo, también podemos pensar en la posición (4, 3) como una compensación del punto (0, 0), u origen. Dibuja una flecha apuntando desde el origen hasta el punto:

../../_images/vector_xy1.png

Esto es un vector. Un vector representa mucha información útil. Además de decirnos que el punto está en (4, 3), también podemos pensar en él como un ángulo θ y una longitud (o magnitud) m. En este caso, la flecha es un vector de posición - denota una posición en el espacio, relativa al origen.

Un punto muy importante a considerar acerca de los vectores es que ellos sólo representan dirección y magnitud relativa. No hay concepto de posición en un vector. Los siguientes dos vectores son idénticos:

../../_images/vector_xy2.png

Ambos vectores representan un punto 4 unidades a la derecha y 3 unidades por debajo de algún punto de partida. No importa donde dibujes el vector en el plano, siempre representa una dirección y magnitud relativa.

Operaciones vectoriales

Se puede utilizar cualquiera de los dos métodos (coordenadas x e y o ángulo y magnitud) para referirse a un vector, pero por conveniencia los programadores típicamente usan la notación de coordenadas. Por ejemplo, en Godot el origen es la esquina superior izquierda de la pantalla, así que para colocar un nodo 2D llamado Node2D 400 píxeles a la derecha y 300 píxeles hacia abajo, usa el siguiente código:

$Node2D.position = Vector2(400, 300)
var node2D = (Node2D) GetNode("Node2D");
node2D.Position = new Vector2(400, 300);

Godot soporta tanto Vector2 como ref:`Vector3 <class_Vector3> para uso 2D y 3D respectivamente. Las mismas reglas matemáticas discutidas en este artículo se aplican a ambos tipos.

Acceso para miembros

Se puede acceder directamente a los componentes individuales del vector por nombre.

# create a vector with coordinates (2, 5)
var a = Vector2(2, 5)
# create a vector and assign x and y manually
var b = Vector2()
b.x = 3
b.y = 1
// create a vector with coordinates (2, 5)
var a = new Vector2(2, 5);
// create a vector and assign x and y manually
var b = new Vector2();
b.x = 3;
b.y = 1;

Añadir vectores

Al sumar o restar dos vectores, se suman los componentes correspondientes:

var c = a + b  # (2, 5) + (3, 1) = (5, 6)
var c = a + b;  // (2, 5) + (3, 1) = (5, 6)

También podemos ver esto visualmente añadiendo el segundo vector al final del primero:

../../_images/vector_add1.png

Nota que sumar a + b da el mismo resultado que b + a.

Multiplicación escalar

Nota

Los vectores representan tanto la dirección como la magnitud. Un valor que representa sólo la magnitud se llama escalar.

Un vector puede ser multiplicado por un escalar:

var c = a * 2  # (2, 5) * 2 = (4, 10)
var d = b / 3  # (3, 6) / 3 = (1, 2)
var c = a * 2;  // (2, 5) * 2 = (4, 10)
var d = b / 3;  // (3, 6) / 3 = (1, 2)
../../_images/vector_mult1.png

Nota

Multiplicar un vector por un escalar no cambia su dirección, sólo su magnitud. Así es como escala un vector.

Aplicaciones prácticas

Veamos dos usos comunes para sumar y restar vectores.

Movimiento

Un vector puede representar cualquier cantidad con una magnitud y dirección. Los ejemplos típicos son: posición, velocidad, aceleración y fuerza. En esta imagen, la nave espacial en el paso 1 tiene un vector de posición de (1,3) y un vector de velocidad de (2,1). El vector de velocidad representa hasta dónde se mueve la nave en cada paso. Podemos encontrar la posición para el paso 2 sumando la velocidad a la posición actual.

../../_images/vector_movement1.png

Truco

La velocidad mide el cambio de posición por unidad de tiempo. La nueva posición se encuentra añadiendo velocidad a la posición anterior.

Apuntar hacia un objetivo

En este escenario, hay un tanque que desea apuntar con su torreta a un robot. Si se resta la posición del tanque de la posición del robot, el vector apunta desde el tanque al robot.

../../_images/vector_subtract2.png

Truco

Para encontrar un vector que apunte de A a B se utiliza B - A.

Vectores unitarios

Un vector con magnitud de 1 se llama vector unitario. También se les conoce como vectores de dirección o normales. Los vectores unitarios son útiles cuando se necesita llevar un registro de una dirección.

Normalización

Normalizar un vector significa reducir su longitud a 1 preservando su dirección. Esto se hace dividiendo cada uno de sus componentes por su magnitud:

var a = Vector2(2, 4)
var m = sqrt(a.x*a.x + a.y*a.y)  # get magnitude "m" using the Pythagorean theorem
a.x /= m
a.y /= m
var a = new Vector2(2, 4);
var m = Mathf.Sqrt(a.x*a.x + a.y*a.y);  // get magnitude "m" using the Pythagorean theorem
a.x /= m;
a.y /= m;

Debido a que esta es una operación muy común, Vector2 y Vector3 proporcionan un método para normalizar:

a = a.normalized()
a = a.Normalized();

Advertencia

Debido a que la normalización implica dividir por la longitud del vector, no se puede normalizar un vector de longitud 0. Si lo intentas, se producirá un error.

Reflexión

Un uso común de los vectores unitarios es indicar normales. Los vectores normales son vectores unitarios alineados perpendicularmente a una superficie, definiendo su dirección. Se utilizan comúnmente para iluminación, colisiones y otras operaciones que involucran superficies.

Por ejemplo, imagina que tenemos una pelota en movimiento que queremos rebotar en una pared u otro objeto:

../../_images/vector_reflect1.png

La superficie normal tiene un valor de (0, -1) porque es una superficie horizontal. Cuando la pelota choca, tomamos el movimiento que le queda (la cantidad que queda cuando golpea la superficie) y lo reflejamos usando el normal. En Godot, la clase Vector2 tiene un método bounce() para manejar esto. Aquí hay un ejemplo de GDScript del diagrama anterior usando un KinematicBody2D:

# object "collision" contains information about the collision
var collision = move_and_collide(velocity * delta)
if collision:
    var reflect = collision.remainder.bounce(collision.normal)
    velocity = velocity.bounce(collision.normal)
    move_and_collide(reflect)
// KinematicCollision2D contains information about the collision
KinematicCollision2D collision = MoveAndCollide(_velocity * delta);
if (collision != null)
{
    var reflect = collision.Remainder.Bounce(collision.Normal);
    _velocity = _velocity.Bounce(collision.Normal);
    MoveAndCollide(reflect);
}

Producto punto

El producto punto es uno de los conceptos más importantes en la matemática vectorial, pero a menudo es malentendido. El producto punto es una operación en dos vectores que devuelve un escalar. A diferencia de un vector, que contiene tanto la magnitud como la dirección, un valor escalar sólo tiene magnitud.

La fórmula para el producto punto toma dos formas comunes:

A \cdot B = \left \| A \right \|\left \| B \right \|\cos \Theta

y

A \cdot B = A_{x}B_{x} + A_{y}B_{y}

Sin embargo, en la mayoría de los casos es más fácil utilizar el método incorporado. Nótese que el orden de los dos vectores no importa:

var c = a.dot(b)
var d = b.dot(a)  # these are equivalent
float c = a.Dot(b);
float d = b.Dot(a);  // these are equivalent

El producto punto es más útil cuando se usa con vectores unitarios, haciendo que la primera fórmula se reduzca a cosθ. Esto significa que podemos usar el producto punto para indicarnos algo sobre el ángulo entre dos vectores:

../../_images/vector_dot3.png

Cuando se utilizan vectores unitarios, el resultado estará siempre entre -1 (180°) y 1 (0°).

Orientación

Podemos usar este hecho para detectar si un objeto está orientado hacia otro objeto. En el diagrama de abajo, el jugador P está tratando de evitar a los zombis A y B. Asumiendo que el campo de visión de un zombi es 180°, ¿pueden ver al jugador?

../../_images/vector_facing2.png

Las flechas verdes fA y fB son vectores unitarios que representan las direcciones de los zombis y el semicírculo azul representa su campo de visión. Para zombie A, encontramos el vector de dirección AP apuntando al jugador usando P - A y lo normalizamos. Si el ángulo entre este vector y el vector opuesto es inferior a 90°, el zombi podrá ver al jugador.

En código se vería así:

var AP = (P - A).normalized()
if AP.dot(fA) > 0:
    print("A sees P!")
var AP = (P - A).Normalized();
if (AP.Dot(fA) > 0)
{
    GD.Print("A sees P!");
}

Producto vectorial

Al igual que el producto punto, el producto vectorial (o producto cruz) es una operación sobre dos vectores. Sin embargo, el resultado del producto vectorial es un vector con una dirección que es perpendicular a ambos. Su magnitud depende de su ángulo relativo. Si dos vectores son paralelos, el resultado de su producto cruz será nulo.

\left \|a \times b  \right \| = \left \| a \right \|\left \| b \right \|\ |\sin(a,b)|

../../_images/tutovec16.png

El producto vectorial se calcula así:

var c = Vector3()
c.x = (a.y * b.z) - (a.z * b.y)
c.y = (a.z * b.x) - (a.x * b.z)
c.z = (a.x * b.y) - (a.y * b.x)
var c = new Vector3();
c.x = (a.y * b.z) - (a.z * b.y);
c.y = (a.z * b.x) - (a.x * b.z);
c.z = (a.x * b.y) - (a.y * b.x);

En Godot, puedes usar el método integrado:

var c = a.cross(b)
var c = a.Cross(b);

Nota

En el producto vectorial, el orden importa. a.cross(b) no da el mismo resultado que b.cross(a). Los vectores resultantes apuntan en direcciones opuestas.

Calculando normales

Un uso común de los productos vectorial es encontrar una superficie normal de un plano o superficie en el espacio 3D. Si tenemos el triángulo ABC podemos usar sustracción vectorial para encontrar dos bordes AB y AC. Utilizando el producto vectorial, AB x AC produce un vector perpendicular a ambos: la superficie normal.

A continuación se muestra una función para calcular el normal en un triángulo:

func get_triangle_normal(a, b, c):
    # find the surface normal given 3 vertices
    var side1 = b - a
    var side2 = c - a
    var normal = side1.cross(side2)
    return normal
Vector3 GetTriangleNormal(Vector3 a, Vector3 b, Vector3 c)
{
    // find the surface normal given 3 vertices
    var side1 = b - a;
    var side2 = c - a;
    var normal = side1.Cross(side2);
    return normal;
}

Apuntando a un objetivo

En la sección de producto punto anterior, vimos cómo se podía usar para encontrar el ángulo entre dos vectores. Sin embargo, en 3D esto no es suficiente información. También necesitamos saber qué eje girar. A esto podemos encontrarlo calculando el producto vectorial de la dirección de orientación actual y la dirección objetivo. El vector perpendicular resultante es el eje de rotación.

Más información

Para más información sobre el uso de la matemática vectorial en Godot, consulta los siguientes artículos: