CAPÍTULO V

Transformaciones de vista en 3D

Como vimos en el Pipeline gráfico, en el procesamiento geométrico, una vez que hayamos hecho las transformaciones (rotación, traslación, escalamiento) deseadas para así modelar a los objetos dentro un espacio 3D sigue aplicar las transformaciones de vista, las cuales constan de una secuencia de tres transformaciones: la transformación de la cámara, de proyección y de viewport.

Transformación de la cámara

La transformación de la cámara transforma a todos los puntos definidos en el espacio global (incluyendo la cámara misma), al espacio de la cámara o vista, donde la cámara queda posicionada en el origen $(0,0,0)$ para simplificar los cálculos, y queda mirando hacia el eje $z-$ negativo por convención. Esto es

$$ \mathbf{M}_{\text{cam}} \underbrace{ \begin{bmatrix} x_{global} \\ y_{global} \\ z_{global} \\ w_{global} \end{bmatrix} }_{\text{coordenadas globales}} = \mathbf{M}_{\text{cam}} \mathbf{M}_{\text{model}} \underbrace{ \begin{bmatrix} x_{objeto} \\ y_{objeto} \\ z_{objeto} \\ w_{objeto} \end{bmatrix} }_{\text{coordenadas del objeto}} = \underbrace{ \begin{bmatrix} x_{vista} \\ y_{vista} \\ z_{vista} \\ w_{vista} \end{bmatrix} }_{\text{coordenadas de la vista}} $$ donde $\mathbf{M}_{\text{model}}$ es la matriz de transformación del objeto como mencionamos antes, llamada Model matrix.

Mientras que para las normales sería $$ ((\mathbf{M}_{\text{cam}} \mathbf{M}_{\text{model}})^{-1})^{T} \underbrace{ \begin{bmatrix} x_{objeto} \\ y_{objeto} \\ z_{objeto} \\ w_{objeto} \end{bmatrix} }_{\text{coordenadas del objeto}} = \underbrace{ \begin{bmatrix} x_{vista} \\ y_{vista} \\ z_{vista} \\ w_{vista} \end{bmatrix} }_{\text{coordenadas de la vista}} $$

Pues como vimos la transformación de la normal se calcula diferente.

La transformación de vista nos permite definir la orientación de la cámara, es decir, su posición y a dónde estará mirando.

Es importante hacer notar que desde las coordenadas globales se ha estado usando un sistema de coordenadas de mano derecha, pues por convención las transformaciones de rotación, traslación y escalamiento están especificadas de este modo. Esto es que si consideramos a la pantalla como el plano $xy$, entonces el eje $x+$ se encontraría apuntando a la derecha, el eje $y+$ hacia arriba y el eje $z+$ saldría de la pantalla.

Supongamos entonces que queremos posicionar la cámara en el vector de posición $\mathbf{e}$ (ojo) y que además esté apuntando hacia el objetivo $\mathbf{g}$, con un vector $\mathbf{t}$ el cual señale la dirección hacia arriba de la cámara. Estos vectores nos dan la información suficiente para construir un sistema de coordenadas con la base $\{\mathbf{u}, \mathbf{v},\mathbf{w}\}$ con el origen en $\mathbf{c}$, donde $$ \mathbf{w} = \frac{(\mathbf{e} - \mathbf{g})}{ \lVert \mathbf{e} - \mathbf{g} \rVert} $$ $$\mathbf{u} = \frac{\mathbf{t} \times \mathbf{w}}{\lVert \mathbf{t} \times \mathbf{w} \rVert} $$ $$ \mathbf{v} = \mathbf{w} \times \mathbf{u} $$

Vectores $\mathbf{u}, \mathbf{v}$ y $\mathbf{w}$ del objetivo a la cámara.

Ahora bien, tomando en cuenta que los objetos están en coordenadas globales, con origen en $(0,0,0)$ y los ejes $\mathbf{x}$, $\mathbf{y}$ y $\mathbf{z}$ como base (base canónica). Y ya que queremos transformarlos al espacio de la vista definido a partir de la base

recién definida $\{\mathbf{u}, \mathbf{v},\mathbf{w}\}$, podemos construir la transformación de la vista con dos transformaciones: una traslación, que mueve la cámara al origen, y un cambio de base, o bien, una rotación para orientar los ejes de la cámara para que se alinee el vector $\mathbf{u}$ con el eje $\mathbf{x}$, a $\mathbf{v}$ con el eje $\mathbf{y}$ y a $\mathbf{w}$ con $\mathbf{z}$.

Entonces podemos construir la matriz de transformación de la cámara como $$ \mathbf{M}_{\text{cam}} = \underbrace{ \begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ w_x & w_y & w_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{cambio de base}} \underbrace{ \begin{bmatrix} 1 & 0 & 0 & -e_x \\ 0 & 1 & 0 & -e_y \\ 0 & 0 & 1 & -e_z \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{traslación}} = \begin{bmatrix} u_x & u_y & u_z & -e \cdot u \\ v_x & v_y & v_z & -e \cdot v \\ w_x & w_y & w_z & -e \cdot w \\ 0 & 0 & 0 & 1 \end{bmatrix} $$

Una manera de recordar cómo van posicionados cada uno de los vectores base es como sigue. Como queremos que $\mathbf{u}$ se vuelva $\mathbf{x}$, es decir, el vector $(1,0,0)$, podemos ver que al multiplicar la matriz de cambio de base con $\mathbf{u}$, obtenemos a $\mathbf{x}$, pues $\mathbf{u} \cdot \mathbf{u} = 1$. En cambio la multiplicación del segundo y tercer renglón debería ser igual a $\mathbf{0}$, ya que son ortogonales a $\mathbf{u}$. Y de manera similar con $\mathbf{v}$ y $\mathbf{w}$ podemos obtener a $\mathbf{y}$ y $\mathbf{z}$ respectivamente.

Transformación de proyección

La transformación de proyección transforma los puntos definidos en el espacio de la vista al espacio de clip o recorte, el cual está definido por un volumen de visión dado por el tipo de proyección a utilizar, de modo que los puntos que se encuentren dentro éste serán los puntos que se verán en la pantalla. Esto es $$ \mathbf{M}_{\text{proy}} \underbrace{ \begin{bmatrix} x_{vista} \\ y_{vista} \\ z_{vista} \\ w_{vista} \end{bmatrix} }_{\text{coordenadas de la vista}} = \underbrace{ \begin{bmatrix} x_{clip} \\ y_{clip} \\ z_{clip} \\ w_{clip} \end{bmatrix} }_{\text{coordenadas de clipping}} $$ Por otro lado, recordemos que estamos utilizando todo el tiempo coordenadas homogeneas, con su última componente $W=1$ para los puntos. Y en particular que el vector homogéneo $(x, y, z, W)$ representa al punto $(x/W, y/W, z/W)$. Pues esta última operación, donde se realiza la división entre la componente $W$, hace referencia a la siguiente transformación.

Si bien, esto no produce ningún cambio cuando $W=1$, que es el caso de la proyección ortográfica, es necesaria para la proyección de perspectiva, por esta razón es llamada división de perspectiva. Al ser una operación demasiado simple y depender de la transformación de proyección que se decida, decidimos dejarla aquí.

De modo que antes de pasar a la última proyección de vista, las coordenadas de recorte o clipping son transformadas a coordenadas normalizadas del dispositivo o NDC por sus siglas en inglés, con las que se asegura estar dentro del volumen canónico de vista, esto es $$ \underbrace{ \begin{bmatrix} x_{clip} \\ y_{clip} \\ z_{clip} \\ w_{clip} \end{bmatrix} }_{\text{coordenadas de clipping}} \Rightarrow \begin{bmatrix} x_{clip}/w_{clip} \\ y_{clip}/w_{clip} \\ z_{clip}/w_{clip} \end{bmatrix} = \underbrace{ \begin{bmatrix} x_{ndc} \\ y_{ndc} \\ z_{ndc} \end{bmatrix} }_{\text{coordenadas NDC}} $$

En otras palabras, la transformación de proyección define el volumen de visión que la cámara estará viendo, y que será transformado al volumen canónico de vista.

El volumen canónico de vista que consideraremos es un cubo de lado 2 centrado en el origen. Por lo que las coordenadas NDC estarán dentro del cubo $[-1,1]^{3}$.

Aunado a esto, para las transformaciones de proyección se seguirá usando un sistema de coordenadas de mano derecha, y se asume que la cámara está en el origen mirando a lo largo dej eje $z-$.

Transformación de proyección ortográfica

Una característica de la proyección ortográfica es que las líneas paralelas se mantienen paralelas después de la proyección. Por lo que, cuando se usa, los objetos preservan su tamaño independientemente de su distancia con la cámara.

La proyección ortográfica define a su volumen de visión como una caja rectangular, también llamada axis-aligned box, formada por los planos: $(l)$ izquierdo, $(r)$ derecho, $(b)$ inferior, $(t)$ superior, $(n)$ cercano o frontal y $(f)$ lejano.

Notemos que como la cámara está mirando hacia $z-$ tenemos que $n > f$, ya que el plano $n$ es el más cercano a la cámara y $f$ sería un número negativo más grande, es decir, más pequeño que $n$.

Entonces, la transformación de proyección ortográfica mapea un punto dentro del paralelepípedo $[l,r] \times [b,t] \times [n,f]$ al volumen canónico de vista, es decir, mapeamos la coordenada $x$ del punto que está dentro de un rango $[l,r]$ a $[-1,1],$ de manera similar la coordenada $y$ de $[b,t]$ a $[-1,1]$ y la coordenada $z$ de $[-n,-f]$ a $[-1,1]$.

Una forma de obtener dicha transformación es simplemente trasladar al paralelepípedo de modo que su centro quede en el origen, y después escalarlo al tamaño del volumen canónico de vista, esto último con la proporción entre las dimensiones del cubo y paralelepípedo, teniendo

$$ \mathbf{M}_{\text{orth}} = \underbrace{ \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0 \\ 0 & \frac{2}{t-b} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{escalamiento}} \underbrace{ \begin{bmatrix} 1 & 0 & 0 & -\frac{r+l}{2} \\ 0 & 1 & 0 & -\frac{t+b}{2} \\ 0 & 0 & 1 & -\frac{f+n}{2} \\ 0 & 0 & 0 & 1 \end{bmatrix} }_{\text{traslación}} $$$$ \\= \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{-2}{n-f} & -\frac{f+n}{n-f} \\ 0 & 0 & 0 & 1 \end{bmatrix} $$

Transformación del volumen de visión ortográfico al volumen canónico de vista.

O bien, podemos pensarlo como una transformación de ventana. Y ya que está transformación es efectuada con una traslación y escalamiento, es invertible.

A pesar de que muchos de los APIs definen al volumen canónico con estos mismos límites, como OpenGL, hay otras propuestas que mapean la coordenada $z$ en un rango $[0,1]$.

Transformación de proyección de perspectiva

A diferencia de la proyección ortográfica, en la proyección de perspectiva las líneas paralelas no suelen permanecer paralelas después de la proyección, y es un poco menos simple. Esta proyección suele ser la más utilizada en GC ya que se ajusta más a la manera en que percibimos el mundo, es decir, los objetos más lejanos se visualizan más pequeños y viceversa.

Y para lograr esta sensación de profundidad se utiliza el mismo principio que en la cámara pinhole.

La proyección de perspectiva mapea las coordenadas $x$ y $y$ de cada punto dentro del volumen de visión, el cual tiene forma de pirámide truncada o frustrum, a un punto en el plano de proyección, guardando a su vez la posición de la coordenada $z$.

Para esta proyección seguiremos considerando que la cámara está posicionada en el origen, mirando hacia $z-$. Así como a los planos $n$ (cercano o frontal) y $f$ (lejano) que delimitan al volúmen de visión.

Proyección $P'$ de un punto $P$ sobre el plano de proyección $n.$

En particular, usaremos al plano $n$ como el plano de proyección (image plane), teniendo entonces una distancia focal de $n$. Por lo que, al igual que con la cámara pinhole, podemos calcular las coordenadas del punto proyectado usando la razón de los triángulos semejantes formados entre el punto en el volumen $P$ y el proyectado $P'$ $$ x' = -\frac{n}{z}x \quad \text{y} \quad y' = -\frac{n}{z}y$$

Sin embargo este tipo de transformación, en donde una de las coordenadas del vector aparece como común denominador, no puede lograrse con las transformaciones afines. Por otro lado, como mencionamos antes, al multiplicar la matriz de proyección sobre las coordenadas de vista, éstas son transformadas a coordenadas de recorte, las cuales siguen siendo coordenadas homogeneas. De modo que para mapear dichas coordenadas al volumen canónico de vista o coordenadas NDC, son divididas por su componente $W$.

Por lo tanto, podemos obtener a los puntos proyectados haciendo: $W = -z$, esto es, poner el último renglón de la matriz de proyección como $(0,0,-1,0)$, y escalando cada componente por la distancia del plano $n$.

Ahora solo bastaría calcular de manera similar que con la proyección ortográfica, el mapeo de las coordenadas proyectadas $x'$ y $y'$ a los rangos del volumen canónico, donde $l,r, b$ y $t$ delimitan al plano de proyección $n$, siendo $(l,b,n)$ la esquina izquierda inferior y $(r,t,n)$ la esquina superior derecha del rectángulo. Dicho de otra forma, la coordenada $x'$ en $[l,r]$ se mapea $[-1,1],$ y la

coordenada $y$ de $[b,t]$ a $[-1,1]$. Teniendo hasta ahora a la matriz de perspectiva $$ \mathbf{M}_{\text{persp}} = \begin{bmatrix} \frac{2n}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2n}{t-b} & 0 & -\frac{t+b}{t-b} \\ \cdot & \cdot & \cdot & \cdot \\ 0 & 0 & -1 & 0 \end{bmatrix} $$

Por lo que, al igual que con la cámara pinhole, los límites $(l,r)$ definirían el ángulo de visión horizontal de la cámara, y $(t,b)$ el ángulo de visión vertical.

Por otra parte la coordenada $z_{ndc}$ se calcula diferente, ya que si tomamos la coordenada $z'$ del punto proyectado, ésta siempre sería $-n$. Por lo que, necesitamos "desproyectar" a $z'$, siendo más específicos, su posición entre los planos $n$ y $f$. Pues más adelante necesitaremos este valor para hacer comparaciones de profundidad tanto en el clipping como en el algoritmo de $z$-búfer. Además de que así podremos invertir la matriz de proyección.

Para ello, podemos modificar el tercer renglón de la matriz de proyección, de modo que obtengamos a $z$, y que además esté en un rango de $[-1,1]$. Entonces podemos expresar a la coordenada $z_{ndc}$ de la forma $$ z_{ndc} = -\frac{Az + B}{z}$$ Y para resolver los coeficientes $A$ y $B$, podemos usar la relación de $[-n,-f] \to [-1,1]$, esto es $$ \begin{cases} -(An + B)/n = -1\\ -(Af + B)/f = 1 \end{cases} \to \begin{cases} -An + B = -n \\ -Af + B = f \end{cases} $$ Entonces $$ \begin{cases} \kern{1.2em} -An + B = -n \\ - \underline{\kern{.3em} -Af + B = f} \\ A (-n + f) = -n-f \end{cases} \Rightarrow A = -\frac{f+n}{f-n} $$

Ahora, sustituimos en la primera ecuación $$ \Biggl( \frac{f+n}{f-n}\Biggr)n + B = -n$$

$$ \Rightarrow B = -n - \Biggl( \frac{f+n}{f-n}\Biggr)n $$ $$ \begin{aligned} B =& -\Biggl(1+\frac{f+n}{f-n}\Biggr)n \\ = &-\Biggl(\frac{(f-n) + (f+n)}{f-n}\Biggr)n = -\Biggl(\frac{2f}{f-n}\Biggr)n \\ = & -\frac{2fn}{f-n} \end{aligned} $$ Por lo tanto $$ z_{ndc} =\Biggl(-\frac{f+n}{f-n}z - \frac{2fn}{f-n}\Biggr)/z$$

Finalmente la matriz de proyección de perspectiva sería $$ \mathbf{M}_{\text{persp}} = \begin{bmatrix} \frac{2n}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2n}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix} $$

Transformación del frustrum al volumen canónico de vista.

Notemos que la relación entre la coordenada $z_{vista}$ y $z_{ndc}$ no es lineal, de manera que mientras más cercano se encuentre un punto al plano cercano $n$, su distancia será más precisa, en caso contrario, será menos precisa cuando se acerque al plano lejano $f$. Si bien, no es posible obtener la coordenada original,

una forma de minimizar el problema es posicionar a los planos lo más cerca posible.

Es posible también construir una matriz de perspectiva que al igual que una cámara real tenga un campo de profundidad infinito, haciendo que la distancia del plano $f$ tienda a infinito $$ \mathbf{M}_{\text{persp}} \lim\limits_{f \to \infin} = \begin{bmatrix} \frac{2n}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2n}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & -1 & -2n \\ 0 & 0 & -1 & 0 \end{bmatrix} $$

Por otra parte, muchas veces simplemente queremos un sistema en el que la cámara esté mirando hacia el centro de la ventana frontal del frustrum, esto es que $l= -r$ y $t= -b$. Simplificando así la descripción de la proyección al especificar el ángulo de visión $\theta$. En este caso, el ángulo de visión vertical (FOV).

Una vez más, de forma análoga a la cámra pinhole tenemos la siguiente relación usando trigonometría básica $$ \tan\Biggl(\frac{\theta}{2}\Biggr) = \frac{t}{|n|} \Rightarrow t = tan\Biggl(\frac{\theta}{2}\Biggr)|n| $$ De modo que si consideramos que la ventana fuera cuadrada entonces tendríamos que $r = t$ y $l = b = -t$. Pero como esto no pasa siempre, tendríamos que ajustar a las coordenadas $r$ y $l$. Ahora bien, supongamos que tenemos una ventana de $n_x \times n_y$ pixeles, con $n_x \not = n_y$. Entonces la proporción entre $r$ y $t$ debería ser la misma que el número de pixeles horizonales y el número de pixeles verticales, esto es $\frac{n_x}{n_y} = \frac{r}{t}.$

Por lo que, las coordenadas $r$ y $l$ deberían ser escaladas por la proporción $\frac{n_x}{n_y}$, también llamada aspect-ratio, teniendo

$$ \mathbf{M}_{\text{persp}} = \begin{bmatrix} c/a & 0 & 0 & 0 \\ 0 & c & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix} $$ donde $a=n_x/n_y$ y $c = 1/tan(\frac{\theta}{2})$ ya que queremos que el vector termine en $[-1,1]$.

Transformación de viewport

La transformación de viewport mapea las coordenadas $x$ y $y$ de las coordenadas NDC a coordenadas de pantalla o ventana. Las coordenadas NDC son escaladas y trasladadas de modo que sean ajustadas al tamaño del área de la pantalla sobre la cual se renderizará la escena. $$ \underbrace{ \begin{bmatrix} x_{ndc} \\ y_{ndc} \end{bmatrix} }_{\text{coordenadas NDC}} = \underbrace{ \begin{bmatrix} x_{ventana} \\ y_{ventana} \end{bmatrix} }_{\text{coordenadas de ventana}} $$

Dicha área se define dentro de la ventana gráfica que es usada por la aplicación. La coordenada $z$ del volumen canónico es ignorada ya que no afectan su posición en la ventana.

De manera general, para transformar los puntos contenidos dentro de un rectángulo $[x_{min},x_{max}]\times[y_{min}, y_{max}]$ a otro rectángulo $[u_{min},u_{max}]\times[v_{min}, v_{max}]$, simplemente se traslada el punto $(x_{min},y_{min})$ al origen, luego se escala el tamaño del primer rectángulo al tamaño del segundo y finalmente se traslada a el punto $(u_{min},v_{min})$. A este tipo de transformación se le conoce como window transformation.

Transformación de ventana.

Entonces $$ \begin{aligned} \mathbf{M}_{\text{window}} =& \begin{bmatrix} 1 & 0 & u_{min} \\ 0 & 1 & v_{min} \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \dfrac{u_{max} - u_{min}}{x_{max} - x_{min}} & 0 & 1 \\ 0 & \dfrac{v_{max} - v_{min}}{y_{max} - y_{min}} & 1 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & -x_{min} \\ 0 & 1 & -y_{min} \\ 0 & 0 & 1 \end{bmatrix} \\=& \begin{bmatrix} \dfrac{u_{max} - u_{min}}{x_{max} - x_{min}} & 0 & \dfrac{u_{min}x_{max} - u_{max}x_{min}}{x_{max} - x_{min}} \\ 0 & \dfrac{v_{max} - v_{min}}{y_{max} - y_{min}} & \dfrac{v_{min}y_{max} - v_{max}y_{min}}{y_{max} - y_{min}} \\ 0 & 0 & 1 \end{bmatrix} \end{aligned} $$

Por lo que, si queremos mapear la coordenada $x= -1$ al lado izquierdo de la ventana a renderizar, $x= 1$ al lado derecho de la ventana, $y= -1$ al inferior y $y= 1$ al superior. Es lo mismo que transformar el rectángulo $[-1,1]\times[-1,1]$ al rectángulo que conforma el área que queremos renderizar.

Sea $[x,x+w]\times[y,y+h]$ la ventana en la que se va a renderizar, entonces tenemos $$ \mathbf{M}_{\text{vp}} = \begin{bmatrix} \frac{(x+w) - x}{2} & 0 & \frac{2x + w}{2} \\ 0 & \frac{(y + h) - y}{2} & \frac{2y +h}{2} \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} \frac{w}{2} & 0 & x + \frac{w}{2} \\ 0 & \frac{h}{2} & y + \frac{h}{2} \\ 0 & 0 & 1 \end{bmatrix} $$

Nótese que este tipo de transformación es similar a la que se usa para las transformaciones de proyección con la cual se ajusta el volumen de visión al volumen canónico de vista, solo que se define en 3D, transformando una caja $[x_{min},x_{max}]\times[y_{min}, y_{max}]\times[z_{min}, z_{max}]$ a la caja $[u_{min},u_{max}]\times[v_{min}, v_{max}]\times[w_{min}, w_{max}]$, esto es $$ \mathbf{M}_{\text{window}} = \begin{bmatrix} \dfrac{u_{max} - u_{min}}{x_{max} - x_{min}} & 0 & 0 & \dfrac{u_{min}x_{max} - u_{max}x_{min}}{x_{max} - x_{min}} \\ 0 & \dfrac{v_{max} - v_{min}}{y_{max} - y_{min}} & 0 & \dfrac{v_{min}y_{max} - v_{max}y_{min}}{y_{max} - y_{min}} \\ 0 & 0 & \dfrac{w_{max} - w_{min}}{z_{max} - z_{min}} & \dfrac{w_{min}z_{max} - w_{max}z_{min}}{z_{max} - z_{min}} \\ 0 & 0 & 0 & 1 \end{bmatrix} $$

Referencias bibliográficas

Tumblin, Jack. 2016. EECS 351-1 : Introduction to Computer Graphics: Building the Virtual Camera ver. 1.5. Northwestern - Computer Science. https://canvas.northwestern.edu/files/1973650/download?download_frd=1 Ho Ahn, Song. 2008. OpenGL Transformation. Songho.ca. http://www.songho.ca/opengl/gl_transform.html Ho Ahn, Song. 2016. OpenGL Projection Matrix. Songho.ca. https://www.songho.ca/opengl/gl_projectionmatrix.html Ho Ahn, Song. 2016. OpenGL Camera. Songho.ca. https://www.songho.ca/opengl/gl_camera.html