Matemáticas y videojuegos (III)

Bt9HYEtIUAAodFjUn quaternion es una forma alternativa de representar rotaciones a través del cualquier eje. Matemáticamente, son una extensión del conjunto de números complejos (). El primero que hablo de ellos fue William Hamilton, allá por el año 1843.

 

Presentan varias ventajas comparado con las rotaciones por matrices. Concatenar quaterniones exige menos operaciones, requiere menos espacio para almacenarlos que una matriz, son más fáciles de interpolar que las matrices, etc. Podemos pensar en un quaternion como en un vector de cuatro dimensiones con la forma:

 

q = { w, x, y, z } = w + xi + yj + zk

También los podemos encontrar representados como: q = s + v, donde s es un escalar que representa la componente w de y v es el vector correspondiente a x, y y z que representa su parte imaginaria. Nosotros los podemos codificar así:

 

public struct Quaternion
{
  public double x;

  public double y;

  public double z;

  public double w;
}

Para aprender a operar con quaternions antes debemos refrescar un poco la memoria sobre los números imaginarios.

 

Planeta Imaginario.

Ecuaciones del tipo x2 + 1 = 0 no tienen solución en el conjunto de los números reales (), donde no existe ningún número que cumpla x2 = -1. En el conjunto de los números reales, ningún cuadrado de un número puede ser negativo.

Para resolver este tipo de ecuaciones tenemos que ampliar  con el conjunto de números imaginarios, donde todos los números (incluidos los negativos) tengan raíz cuadrada.

Para hacer posible que el cuadrado de un número pueda ser negativo, nos sacamos de la manga la unidad imaginaria pura i, que cumple:

i2 = -1

Farnsworth¿Cómo es posible esto? Como diría el profesor Hubert J. Farnsworth, “todo es posible si te lo imaginas“. Así, por ejemplo la ecuación x2 + 9 = 0, si tiene solución:

 

x2 + 9 = 0
x2 = -9
x = sqrt(-9) = sqrt(9) sqrt(-1) = ±3i

De la unión de  e i, obtenemos el conjunto de los números complejos (). Llamaremos número complejo al par (a, b), donde a representa la parte real y b la parte imaginaria.

[alert-announce] Dados dos números complejos, (a, b) y (c, d), se cumple que:

  • (a, b) = (c, d), si a = c  y b = d
  • (a, b) + (c, d) = (a + c, b + d)
  • (a, b) * (c, d) = (acbd, ad + bc)
[/alert-announce]  

Podríamos representar el número imaginario puro i como un número complejo de la forma (0, 1). Aplicando las anteriores reglas, podemos calcular potencias de i:

 

i2 = (0, 1) (0, 1) = (01, 0 + 0) = (-1, 0) = -1
i3 = i2i = (-1, 0) (0, 1) = i
i4 = i3i = (0, -1) (0, 1) = 1

Y también podemos sumar dos números complejos x = ai e y = bi (restarlos seria similar) :

 

x + y = ai + bi = (a + b)i

O multiplicarlos (dividir es similar):

xy = aibi = abi2 = –ab

Para dividir tened en cuenta que i/i = 1.

 

Existe una forma visual de representar a los números complejos. Si tenemos un número complejo a + bi, donde a y b son números reales, los podemos representar en como un vector (a, b) en unos ejes cartesianos, siendo Im el eje imaginario, y Re el eje real:

 

Complex_number_illustration.svg_

 

Si reemplazamos i por -i de un número complejo z, invertir la coordenada imaginaria, obtenemos el conjugado de z. Si multiplicamos un número complejo por su conjugado, eliminaremos su parte imaginaria:

(a, b) * (a, -b) = (aab(-b), –ab + ba) = (a2 + b2, 0)

El módulo de un número complejo z = (a + bi), representado como |z|, es:

|z| = sqrt(z * z) = sqrt(a2 + b2)

Como ya vimos anteriormente con los vectores, el módulo de un número complejo es la distancia del punto (a, b) al origen del sistema cartesiano.

Números hipercomplejos, quaternions y cintas de vídeo.

Que no te asuste su nombre, un número hipercomplejo no es más que la suma de un número real y varios números imaginarios. El subconjunto de los números hipercomplejos que nos interesa a nosotros es el de los quaternions, que tienen esta forma:

q0 + q1i + q2j + q3k

Donde q0, q1, q2 y q3 son números reales e i j y k son imaginarios. Si q0 = 0, diremos que tenemos un quaternion puro.

 

Para multiplicar las partes imaginarias de un número hipercomplejo, seguiremos las reglas de nuestro amigo Hamilton, que dicen lo siguiente:

[alert-announce]
  • ij = k
  • jk = i
  • kj = j
  • ji = -k
  • kj = -i
  • ik = -j
[/alert-announce]

 

Antes de saber como nos pueden ayudar los quaternions en nuestros juegos, veremos como realizar algunas operaciones básicas con ellos.

Suma y resta de quaternions.

Sumar y restar quaternions es muy parecido a lo que hemos hecho antes, teniendo 2 quaternions p = p0 + p1i + p2j + p3k y q = q0 + q1i + q2j + q3k seria:

 

p + q = (p0 + q0) + (p1 + q1)i + (p2 + q2)j + (p3 + q3)k

y la resta

 

pq = (p0q0) + (p1q1)i + (p2q2)j + (p3q3)k

La verdad es que estas operaciones apenas la usaremos, siendo mucho mas importante la multiplicación.

Multiplicar quaternions.

Será la operación que más usemos, como veremos más adelante. Para multiplicar quaternions usaremos la multiplicación de números imaginarios. La codificaremos de la siguiente forma:

 

public static Quaternion Multiply(Quaternion q1, Quaternion q2)
{
  Quaternion quaternion;

  quaternion.x = ((q1.x * q2.w) + (q2.x * q1.w)) + (q1.y * q2.z) - (q1.z * q2.y);
  quaternion.y = ((q1.y * q2.w) + (q2.y * q1.w)) + (q1.z * q2.x) - (q1.x * q2.z);
  quaternion.z = ((q1.z * q2.w) + (q2.z * q1.w)) + (q1.x * q2.y) - (q1.y * q2.x);
  quaternion.w = (q1.w * q2.w) - ((q1.x * q2.x) + (q1.y * q2.y)) + (q1.z * q2.z);

  return quaternion;
}

Conjugado.

Es similar a la forma en la que hicimos el conjugado de números complejos. Para hacer el conjugado de un quaternion simplemente cambiaremos cada parte imaginaria por su negativo. Así, si tenemos el quaternion q = q0 + q1i + q2j + q3k su conjugado, q*, será q0q1iq2jq3k.

 

public static Quaternion Conjugate(Quaternion q)
{
  Quaternion quaternion;

  quaternion.x = -q.x;
  quaternion.y = -q.y;
  quaternion.z = -q.z;
  quaternion.w = q.w;

  return quaternion;
}

El conjugado cumple (pq)* = q*p*. Osea que el conjugado del producto de dos quaternions es igual al producto de los conjugados, pero en distinto orden.

Otra propiedad interesante es que q*q es igual a q02 + q12 + q22 + q32, que es un número real.

Módulo.

El módulo de un quaternion, |q|, es sqrt(qq). Asi que |q|2 = qq.

 

public double Length()
{
  return Math.Sqrt((((x * x) + (y * y)) + (z * z)) + (w * w));
}

Inversa.

Si multiplicamos un quaternion por su inverso obtendremos el numero real 1. Para calcular el inverso de un quaternion, llamado q-1, haremos q* / |q|2.

 

public static Quaternion Inverse(Quaternion q)
{
  Quaternion quaternion;

  double num = 1.0 / ((((q.x * q.x) + (q.y * q.y)) + (q.z * q.z)) + (q.w * q.w));
  quaternion.x = -q.x * num;
  quaternion.y = -q.y * num;
  quaternion.z = -q.z * num;
  quaternion.w = q.w * num;

  return quaternion;
}

Rotando a Miss Daisy.

Después de ver las operaciones básicas que podemos hacer con quaternions, estamos en condiciones para afrontar lo que nos interesa de los quarternions, rotar puntos con respecto a cualquier eje. Usaremos los quaternions casi siempre de la siguiente forma:

  1. Haremos que represente la rotación que queremos. Para esto partiremos de un vector, o una matriz, o unos ángulos de Euler, que representen esa rotación.
  2. Teniendo ya la rotación “dentro” del quatarnion, operaremos sobre él. Normalmente haremos interpolaciones (ir de un valor a otro), que son mucho mas fáciles de hacer que con matrices y es la verdadera causa de usar quaternions.
  3. Transformaremos nuestro quaternion en su forma de matriz, para poder seguir con los cálculos.

 

Primero veremos como representar esa rotación en un quaternion dado un vector que define el eje de rotación.

 

Tenemos el quaternion q = q0 + q1i + q2j + q3k, podemos pensar en i, j y k como en vectores imaginarios i, j y k. Así, q1, q2 y q3 serian las componentes de dichos vectores. Nuestro quaternion podría representarse ahora como:

q = q0 + q

Donde q0 es un escalar y q es un vector que representa la dirección en la cual el eje de la rotación señala.

 

Dada una rotacion ß con respecto al eje u (vector normalizado), el quaternion que representa esa rotación es:

q = cos(ß/2) + sin(ß/2)u

Sabiendo esto, si queremos representar una rotación sobre un eje determinado en un quaternion, la función que usaremos seria:

 

public static Quaternion FromAxisAngle(Vector3 axis, double angle)
{
  Quaternion quaternion;

  double halfAngle = angle * 0.5;
  double sin = Math.Sin(halfAngle);

  quaternion.x = axis.x * sin;
  quaternion.y = axis.y * sin;
  quaternion.z = axis.z * sin;
  quaternion.w = Math.Cos(halfAngle);

  return quaternion;
}

Otro método interesante es partiendo de los llamados ángulos de Euler. Mediante este sistema se define una rotación a través de tres simples rotaciones de los ejes:

 

426px-Eulerangles.svg_

 

En este gráfico vemos como primero se rotaría sobre el eje z (ángulo α), luego sobre el nuevo eje x, o N, (ángulo β) y finalmente sobre el nuevo eje z (ángulo γ). Este es el orden más utilizado, aunque puede haber otras convenciones distintas.

 

public static Quaternion FromYawPitchRoll(double yaw, double pitch, double roll)
{
  Quaternion quaternion;

  double halfRoll = roll * 0.5;
  double halfPitch = pitch * 0.5;
  double halfYaw = yaw * 0.5;

  double sinRoll = Math.Sin(halfRoll);
  double cosRoll = Math.Cos(halfRoll);
  double sinPitch = Math.Sin(halfPitch);
  double cosPitch = Math.Cos(halfPitch);
  double sinYaw = Math.Sin(halfYaw);
  double cosYaw = Math.Cos(halfYaw);

  quaternion.x = ((cosYaw * sinPitch) * cosRoll) + ((sinYaw * cosPitch) * sinRoll);
  quaternion.y = ((sinYaw * cosPitch) * cosRoll) - ((cosYaw * sinPitch) * sinRoll);
  quaternion.z = ((cosYaw * cosPitch) * sinRoll) - ((sinYaw * sinPitch) * cosRoll);
  quaternion.w = ((cosYaw * cosPitch) * cosRoll) + ((sinYaw * sinPitch) * sinRoll);

  return quaternion;
}

Ya sabemos como representar una rotacion en un quaternion. Ahora veremos como rotar un vector, teniendo un quaternion con una rotación ya definida. Definimos el quaternion puro p:

p = 0 + r

Donde r es el vector a rotar. Pues bien, la parte imaginaria de la operación qpq*, es nuestro ansiado vector r rotado.

 

Podemos aplicar muchas rotaciones en un solo quaternion. Imaginemos que queremos rotar un vector representado por el quaternion puro v por q y luego por p.

 

Como vimos anteriormente, para calcular lo primero, hacemos qvq*. Para hacer la segunda rotacion, p(qvq*)p*.

 

Como las multiplicaciones son asociativas, lo anterior puede escribirse tambien como (pq)v(q*p*).

 

También sabemos que (q*p*) = (pq)*, así que nos queda (pq)v(pq)*. Para simplificar aun más, llamaremos r al producto pq, con lo que nuestro vector final es simplemente rvr*.

Interpola esto!

Ya que los quaternions pueden ser representados como vectores, son perfectos para interpolarlos. La forma más simple (aunque no muy útil) seria una interpolación lineal:

q(t) = (1t)q1 + tq2

Donde q1 y q2 son los dos quaternions que queremos interpolar y t va de 0 a 1. La funcion q(t) recorre la línea que conecta ambos quaternions (pensemos que son vectores). Si el ángulo es grande, esta interpolación no daría unos resultados muy buenos.

 

Otro tipo de interpolación más interesante es la esferica, o SLERP (Spherical Linear intERPolation), que recorrería ambos quaterniones siguiendo el arco (y no la linea) que une ambos quaterniones (si los dos quaternions están muy cerca, podemos usar una interpolación lineal, nadie lo notara…).

 

Seria así:

q(t) = (q1sin((1t)ß) + q2sin(tß) ) / sin(ß)

En este gráfico podéis ver claramente la diferencia entre una interpolación linear (izquierda) y otra esférica (derecha):

 

interpolation_angle

 

 

[tabs] [tab title=”Lineal”]
public static Quaternion Lerp(Quaternion q1, Quaternion q2, float t)
{
  double oneMinusT = 1 - t;

  Quaternion quaternion = new Quaternion();
  if ((((q1.x * q2.x) + (q1.y * q2.y)) + (q1.z * q2.z)) + (q1.w * q2.w) >= 0)
  {
    quaternion.x = (oneMinusT * q1.x) + (t * q2.x);
    quaternion.y = (oneMinusT * q1.y) + (t * q2.y);
    quaternion.z = (oneMinusT * q1.z) + (t * q2.z);
    quaternion.w = (oneMinusT * q1.w) + (t * q2.w);
  }
  else
  {
    quaternion.x = (oneMinusT * q1.x) - (t * q2.x);
    quaternion.y = (oneMinusT * q1.y) - (t * q2.y);
    quaternion.z = (oneMinusT * q1.z) - (t * q2.z);
    quaternion.w = (oneMinusT * q1.w) - (t * q2.w);
  }

  double invLength = 1 / quaternion.Length();
  quaternion.x *= invLength;
  quaternion.y *= invLength;
  quaternion.z *= invLength;
  quaternion.z *= invLength;

  return quaternion;
 }
[/tab] [tab title=”Esférica”]
public static Quaternion Slerp(Quaternion q1, Quaternion q2, float t)
{
  double k;
  double m;
  Quaternion quaternion;

  double diff = (((q1.x * q2.x) + (q1.y * q2.y)) + (q1.z * q2.z)) + (q1.w * q2.w);
  bool flag = false;
  if (diff < 0)
  {
    flag = true;
    diff = -diff;
  }
  else if (diff > 0.999999)
  {
    m = 1 - t;
    k = flag ? -t : t;
  }
  else
  {
    double cosDiff = Math.Acos(diff);
    double invCosDiff = 1 / Math.Sin(cosDiff);

    m = Math.Sin((1 - t) * cosDiff) * invCosDiff;
    k = flag ? -Math.Sin(t * cosDiff) * invCosDiff : Math.Sin(t * cosDiff) * invCosDiff;
  }

  quaternion.x = (m * q1.x) + (k * q2.x);
  quaternion.y = (m * q1.y) + (k * q2.y);
  quaternion.z = (m * q1.z) + (k * q2.z);
  quaternion.w = (m * q1.w) + (k * q2.w);

  return quaternion;
 }
[/tab] [/tabs]

 

Con esto acabamos el tema de quaternions. Como siempre, podéis descargaros el código de esta serie de artículos de nuestro Github.

githubLink

¡Hasta el próximo capítulo!

You Might Also Like

No Comments

Leave a Reply