En la programación gráfica hay algunos conceptos que, siendo muy simples, pueden llevar a confusiones y tirones de pelo si no se dejan claros desde un principio. Uno de ellos es el handedness (la mayoría de conceptos de programación gráfica están en inglés y en muchas ocasiones, como esta, no conozco una buena traducción), y otro es la representación de vectores como vectores filas y columnas, o como se suele encontrar en la literatura, la notación row major y column major. En esta entrada hablaré de este último concepto, mientras que dejaremos el handedness para la próxima entrada.
Creo que la confusión surge porque el término row major (y column major) está sobrecargado y se refiere a dos cosas diferentes que no tienen relación. Por un lado, se refiere a un aspecto técnico sobre la disposición de elementos en memoria; por otro, se refiere a una notación matemática que representa a los vectores como si fueran filas o columnas, y que influye en el orden de multiplicación por matrices.
En el aspecto técnico, row o column major se utiliza en el contexto de la programación para referirse al orden en el que se disponen en memoria los elementos de una matriz. Por ejemplo, supongamos que tenemos la siguiente matriz:
$$ \begin{pmatrix}
1 & 2 \\
3 & 4 \\
\end{pmatrix} $$
En el convenio row major, los elementos de la matriz se dispondrían en memoria en el orden siguiente: {1, 2, 3, 4}; en column major, los elementos se almacenarían en este orden: {1, 3, 2, 4}.
Como vemos, el concepto es puramente técnico y de bajo nivel; sin embargo, y aquí es donde viene mayormente el lío, también se utiliza en el contexto del álgebra para explicar si un vector lo consideramos como una fila o una columna. Es decir, en este caso estamos hablando de un concepto de notación matemática, como vamos a ver a continuación.
Supongamos una matriz de traslación \(T\), es decir, una matriz que representa una transformación por la que trasladamos un punto de unas coordenadas a otras. En mucha de la literatura, esta matriz se representa de la siguiente manera:
$$ T_1 = \begin{pmatrix}
1 & 0 & 0 & t_x \\
0 & 1 & 0 & t_y \\
0 & 0 & 1 & t_z \\
0 & 0 & 0 & 1
\end{pmatrix} $$
Sin embargo, también es posible encontrarse la siguiente matriz para representar lo mismo:
$$ T_2 = \begin{pmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
t_x & t_y & t_z & 1
\end{pmatrix} $$
Como puedes observar, \(T_1\) es la traspuesta de \(T_2\) (o viceversa), es decir, las dos matrices son iguales cambiando filas por columnas: \(T_1 = T_2^T\).
Entonces, ¿cuál de las dos usamos? Aquí es donde entra en juego la notación que mencionaba anteriormente en la que puedes considerar un vector como si fuera una fila o como si fuera una columna. Al final todo se reduce a una convención de notación, pero una convención que influye en el orden en el que multiplicamos las matrices por los vectores.
Para entender por qué influye, vamos a tomar dos vectores, que representan puntos(1)Un vector que representa un punto tiene un 1 en su cuarta componente, también llamada componente homogénea, mientras que un vector que representa una dirección tiene un 0 en su cuarta componente. Esto es como un “truquillo” para representar que un punto puede trasladarse, pero no una dirección, ya que no tiene sentido trasladar una dirección., sólo que el primero lo representamos como una fila y el segundo como una columna:
$$V_{fila} = (v_x, v_y, v_z, 1)$$
$$ V_{col} = V_{fila}^T=\begin{pmatrix}
v_x \\
v_y \\
v_z \\
1
\end{pmatrix} $$
Fíjate que hay combinaciones de multiplicaciones que, matemáticamente, no son posibles. Por ejemplo, no se pueden multiplicar las matrices por la izquierda del vector fila, porque las dimensiones de las primeras son 4×4 y las del vector es 1×4.
Y ahora vamos a multiplicar cada vector por cada una de las dos matrices de traslación definidas arribas, por delante y por detrás.:
$$V_{fila}T_1 = (v_x, v_y, v_z, v_{x}t_{x} + v_{y}t_{y} + v_{z}t_{z} + 1)$$
$$V_{fila}T_2 = (v_x + t_x, v_y + t_y, v_z + t_z, 1)$$
$$T_{1}V_{col} =(v_x + t_x, v_y + t_y, v_z + t_z, 1)$$
$$T_{2}V_{col} = (v_x, v_y, v_z, v_{x}t_{x} + v_{y}t_{y} + v_{z}t_{z} + 1)$$
Como vemos, sólo la segunda y la tercera opciones tienen sentido para representar una traslación. Por tanto, si el vector se representa como una columna, se multiplica por la derecha de la matriz \(T_1\). Además, las transformaciones se leen de derecha a izquierda:
$$ABCV_{col}$$
Arriba primero aplicamos la transformación C, luego la B y finalmente la A.
Si el vector se representa como una fila, se multiplica por la izquierda de la matriz \(T_2\) y las transformaciones se leen en el orden natural:
$$V_{fila}ABC$$
Arriba primero aplicamos la transformación A, luego la B, y finalmente la C.
Como decía arriba, todo es más lioso porque el concepto de row major y column major está sobrecargado y originalmente se refiere al orden en el que almacenamos los elementos de una matriz (un array bidimensional) en memoria.
Por ejemplo, la especificación OpenGL asume vectores columnas en su documentación a nivel de notación matemática, pero al final para OpenGL una matriz no es más que un array de 16 elementos.
Según la especificación, los elementos que forman la base de la matriz (el sistema de coordenadas) deben almacenarse de forma contigua, y los elementos del vector que determina el origen de coordenadas (el vector de traslación) deben ocupar las posiciones 13, 14 y 15. Pero claro, esto es posible tanto si usamos la notación matemática row major como la column major. Por ejemplo, supongamos que tenemos la siguiente matriz:
$$ M = \begin{pmatrix}
a_x & b_x & c_x & t_x \\
a_y & b_y & c_y & t_y \\
a_z & b_z & c_z & t_z \\
0 & 0 & 0 & 1
\end{pmatrix} $$
Esta matriz sigue una notación column major (fíjate que los vectores de traslación están en la última columna por lo que para reflejar una traslación tendríamos que hacer una multiplicación por la derecha con un vector columna), sin embargo, los elementos de la matriz se pueden disponer en memoria tanto siguiendo la convención row major como column major.
Por ejemplo, en la convención row major los primeros 4 elementos serían: \(a_x b_x c_x t_x\); mientras que con la convención column major éstos serían: \(a_x a_y a_z 0\). OpenGL, como he mencionado antes, espera que los vectores que forman la base de la matriz (en este caso \(\vec{a} \vec{b} \vec{c}\)) estén consecutivos en memoria, y que el vector que determina el origen del sistema de coordenadas (en este caso \(\vec{t}\)) ocupe las últimas posiciones en memoria. Por tanto, en este caso, para respetar la especificación OpenGL tendríamos que almacenar la matriz usando la convención column major.
Sin embargo, si consideráramos la matriz anterior transpuesta:
$$ M = \begin{pmatrix}
a_x & a_y & a_z & 0 \\
b_x & b_y & b_z & 0 \\
c_x & c_y & c_z & 0 \\
t_x & t_y & t_z & 1
\end{pmatrix} $$
, lo que significa que estamos usando la notación row major (tendríamos que multiplicar un vector considerado como fila por la izquierda), OpenGL esperaría que dispusiéramos los elementos en memoria utilizando la convención row major.
La propia API de OpenGL permite transponer una matriz cuando la pasamos para que se la entreguemos como espera. Este es el caso de la función glUniformMatrix4vfx, cuyo tercer parámetro indica si queremos transponer la matriz que pasamos como argumento.
Puesto que es más común encontrar la convención column major en la literatura, yo usaré dicha convención en este blog, pero ten en cuenta que en algunos libros y recursos web puedes encontrar las matrices de transformación transpuestas respecto a las que encontrarás aquí.
Recuerda lo que explicaba en esta entrada: las matemáticas forman una de las grandes bases de los gráficos por ordenador, ¡así que conviene conocerlas lo mejor posible!
Notas
↑1 | Un vector que representa un punto tiene un 1 en su cuarta componente, también llamada componente homogénea, mientras que un vector que representa una dirección tiene un 0 en su cuarta componente. Esto es como un “truquillo” para representar que un punto puede trasladarse, pero no una dirección, ya que no tiene sentido trasladar una dirección. |
---|
Comentarios