Saltar al contenido

Moviendo la camara y Apuntando hacia un punto con Cambios de base

septiembre 1, 2018
Motor Render desde 0 en C++ Moviendo la camara y Apuntando hacia un punto con Cambios de base

En este post aprenderemos a posicionar la cámara en el espacio y enfocarla (apuntarla) a un punto del modelo. Lo haremos mediante matrices de cambio de base.

Cambios de base en espacios 3D

En el espacio Euclideo las coordenadas se pueden representar dado un punto (origen) y una base.

Diremos que un punto $P$ tiene las coordenadas $(x, y, z)$ en la base $(O, i, j, k)$ cuando el vector $\vec{OP}$ se puede expresar de esta forma:

$$\vec{OP} = x\vec{i} + y \vec{j} + z \vec{k} = \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix}\begin{bmatrix} x \\ y \\ z \end{bmatrix}$$

Si tenemos otra base $(O’, i’, j’, k’)$ podemos transformar las coordenadas de una base a la otra. Como $(i, j, k)$ y $(i’, j’, k’)$ son bases de un espacio 3D, existe una matriz $M$ no degenerada (tiene inversa) tal que:

$$\begin{bmatrix} \vec{i’} & \vec{j’} & \vec{k’} \end{bmatrix} =\begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} \times M$$

Vamos a fijarnos en la siguiente representación de lo que estamos haciendo:

cambio de base 3d

Podemos reescribir el vector $\vec{OP}$ como la suma de los otros dos de la siguiente forma:

$$\vec{OP} = \vec{OO’} + \vec{O’P} = \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} \begin{bmatrix} O’_x \\ O’_y \\ O’_z \end{bmatrix} + \begin{bmatrix} \vec{i’} & \vec{j’} & \vec{k’} \end{bmatrix} \begin{bmatrix} x’ \\ y’ \\ z’ \end{bmatrix}$$

Ahora podemos sustituir $(i’, j’, k’)$ por una matriz de cambio de base:

$$\vec{OP} = \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} ( \begin{bmatrix} O’_x \\ O’_y \\ O’_z \end{bmatrix} + M \begin{bmatrix} x’ \\ y’ \\ z’ \end{bmatrix} )$$

Y obtenemos la fórmula para poder transformar las coordenadas de una base a otra:

$$\begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} O’_x \\ O’_y \\ O’_z \end{bmatrix} + M \begin{bmatrix} x’ \\ y’ \\ z’ \end{bmatrix} \rightarrow \begin{bmatrix} x’ \\ y’ \\ z’ \end{bmatrix} = M^{-1} ( \begin{bmatrix} x \\ y \\ z \end{bmatrix} – \begin{bmatrix} O’_x \\ O’_y \\ O’_z \end{bmatrix} )$$

Apuntando la cámara con la matriz ModelView – Implementando el gluLookAt( )

La cámara de nuestro motor de renderizado estará situada en el eje $z$. Si queremos mover la cámara lo que haremos será mover toda la escena y dejaremos la cámara inmóvil. Esto lo hacemos siguiendo las referencias de OpenGL. Lo que haremos a continuación será implementar su función gluLookAt( ).

Lo que vamos a hacer es implementar una función que permita generar una matriz de transformación para tener la cámara:

  • Situada en un punto $e$ (eye)
  • Apuntando hacia un punto $c$ (centro) de forma que un vector $u$ (up), que nos vendrá dado, sea totalmente vertical en el renderizado

En otras palabras, queremos rasterizar la imagen en la base $(c, x’, y’, z’)$ y nuestro modelo viene dado en la base $(O, x, y, z)$.

Lo que hacemos es calcular la matriz de transformación $(4 \times 4)$ que llamaremos ModelView:

  • El valor de $z’$ lo calculamos con el vector $\vec{ce}$ a partir de la posición de la cámara (eye) y el centro.
  • Normalizamos todos los valores que calculemos.
  • El valor de $x’$ lo calculamos con el producto vectorial entre el vector $\vec{u}$ (up) que deberá apuntar hacia arriba y $z’$ que acabamos de calcular.
  • Los vectores $\vec{ce}$ y $\vec{u}$ no son siempre ortogonales, pueden no serlo.
  • El valor de $y’$ será ortogonal a $x’$ y $z’$, por lo tanto, calculamos también su producto vectorial.
  • Trasladamos el origen al centro $c$.

Lo veremos mejor con una implementación:

void lookat(Vec3f eye, Vec3f center, Vec3f up) {
    Vec3f z = (eye-center).normalize();
    Vec3f x =(up ^ z).normalize();
    Vec3f y =(z ^ x).normalize();
    Matrix Minv = Matrix::identity();
    Matrix Tr   = Matrix::identity();
    for (int i=0; i<3; i++) {
        Minv[0][i] = x[i];
        Minv[1][i] = y[i];
        Minv[2][i] = z[i];
        Tr[i][3] = -center[i];
    }
    ModelView = Minv*Tr;
}
ModelView Matrix - lookat func

Ahora podemos cargar cualquier modelo en la base $(x, y, z, 1)$  y multiplicarlo por la matriz ModelView para obtener las coordenadas en la base de la cámara.

Escalando la posición de los vértices al tamaño de la ventana con la matriz Viewport

Hasta ahora utilizábamos el siguiente cálculo para transformar las coordenadas del objeto a dibujar a la escala de la ventana:

screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);

Con esto, básicamente, lo que hacíamos era:

Coger un punto $v$ en un Vec2f, que cargamos del modelo 3D y viene expresado dentro del cuadro $[-1, 1] \times [-1, 1]$, y transformamos (escalamos) sus coordenadas a las dimensiones de la ventana con los parámetros width y height.

  • El valor de $(v.x + 1)$ varía entre 0 y 2.
  • El valor de $\frac{v.x + 1}{2}$ varía entre 0 y 1.
  • El valor de $\frac{(v.x + 1) * width}{2}$ nos cubre toda la ventana.

 

De esta forma, mapeamos el cuadrado en que viene expresado el modelo 3D de dos unidades, a todo el ancho y alto de la ventana.

Sin embargo, ahora que estamos trabajando con matrices, será mucho más fácil utilizar una matriz de transformación que nos permita multiplicarla por el resto de transformaciones y aplicarla como una más.

Matrix viewport(int x, int y, int w, int h) {
    Matrix m = Matrix::identity(4);
    m[0][3] = x+w/2.f;
    m[1][3] = y+h/2.f;
    m[2][3] = depth/2.f;

    m[0][0] = w/2.f;
    m[1][1] = h/2.f;
    m[2][2] = depth/2.f;
    return m;
}
Viewport Matrix - Escalado al tamaño de ventana

Este código simplemente genera una matriz como esta

$$\begin{bmatrix} w/2 & 0 & 0 & x + w/2 \\ 0 & h/2 & 0 & y + h/2 \\ 0 & 0 & d/2 & d/2 \\ 0 & 0 & 0 & 1 \end{bmatrix}$$

Esta matriz nos permite transformar el cubo de dos unidades $[-1, 1] \times [-1, 1] \times [-1,1]$ del modelo 3D al cubo que utilizaremos para el rasterizado final con el tamaño de la ventana $[x, x+w] \times [y, y+h] \times [0,d]$.

Como veis, ahora trabajamos con un cubo en lugar de un cuadrado, ya que también realizaremos cálculos con los valores de $z$ para el buffer en $z$. El valor de $d$ será la resolución del buffer en $z$. En este caso, utilizaremos un valor de 255 ya que nos permitirá mapear los valores a una imagen B/N para depurar nuestro código y ver los valores del buffer en $z$ que hemos calculado.

Conclusiones sobre las transformaciones que aplicaremos

  • Nuestro modelo 3D está representado en su propia base, las coordenadas objeto.
  • El modelo 3D se inserta en una escena y se transforman sus coordenadas a coordenadas de la escena o coordenadas mundiales. Esto lo hacemos con la matriz Model.
  • Transformamos las coordenadas del modelo en la escena a la base de la cámara, coordenadas de la cámara. La transformación la hacemos con la matriz View.
  • Proyectamos los puntos aplicando una deformación para la perspectiva con la matriz de Proyección. Obtenemos las llamadas coordenadas clip.
  • Transformamos las coordenadas clip en las coordenadas de la ventana. La transformación se hace con la matriz Viewport.

*En nuestro caso hemos combinado las matrices Model y View a la matriz ModelView que presentamos en este post cuando implementamos la función gluLookAt( ).

Por lo tanto, para cada punto $v$ que leemos del fichero wavefront .obj realizamos las siguientes transformaciones:

Matrix(v) * ModelView * Projection * ViewPort

Problema con la transformación de vectores normales

Dibujamos un triángulo 2D $(0, 0)$, $(0, 1)$, $(1, 0)$ y un vector $\vec{n}$ normal a su hipotenusa $h$, $(1, 1)$.

transformacion vectores normales

Ahora aplicamos una transformación para escalar solo el componente $y$ en 2, dejando la coordenada en $x$ intacta. Si transformamos el vector normal $n$ de la misma forma, se transforma en $n_e’$ $(1,2)$ y este vector ya no es ortogonal a la nueva hipotenusa del triángulo transformado.

Esto es debido a la siguiente propiedad:

Si tenemos un modelo y sus vectores normales Y aplicamos una transformación afín al modelo, ENTONCES sus vectores normales se deben transformar utilizando la transposición de la inversa de la matriz de transformación.

Por lo tanto, debemos recalcular las normales después de aplicar una transformación. Haremos hincapié en este tema más adelante cuando hablemos sobre los shaders.

En el siguiente post juntaremos el código que hemos creado para generar las matrices y actualizaremos nuestro motor de renderizado para aplicar toda la teoría que acabamos de ver.