8.2 Projeção perspectiva

Na projeção perspectiva, quanto mais distantes os objetos estiverem do centro de projeção, menor ficarão quando projetados. Isso produz o efeito de diminuição de tamanho de objetos distantes, que é o que percebemos no mundo real. A figura 8.9 mostra esse efeito em uma fotografia. Note como os elementos da cena parecem convergir em um ponto distante. Esse ponto de convergência é chamado de ponto de fuga.

Diminuição de tamanho na projeção perspectiva ([fonte](https://commons.wikimedia.org/wiki/File:One_point_perspective.jpg)).

Figura 8.9: Diminuição de tamanho na projeção perspectiva (fonte).

O número de pontos de fuga é determinado pela orientação da câmera em relação a um objeto cuboide referencial no espaço do mundo (figura 8.10).

Pontos de fuga na projeção perspectiva.

Figura 8.10: Pontos de fuga na projeção perspectiva.

Se o cubo tiver arestas paralelas aos eixos \(x\) e \(y\) da câmera, a projeção terá 1 ponto de fuga. Se o cubo tiver arestas paralelas apenas em relação a um dos eixos (\(x\) ou \(y\)), a projeção terá 2 pontos de fuga. Se o cubo não tiver arestas paralelas aos eixos \(x\) e \(y\), a projeção terá 3 pontos de fuga.

Para produzir uma matriz de projeção perspectiva, adotaremos a mesma estratégia de normalizar o volume de visão, isto é, criaremos uma transformação que converte um volume de visão no espaço da câmera para o volume de visão de tamanho \(2 \times 2 \times 2\) no espaço NDC. Entretanto, dessa vez o volume de visão terá o formato de uma pirâmide truncada (chamada de view frustum), como mostra a figura 8.11.

Volume de visão genérico para projeção perspectiva.

Figura 8.11: Volume de visão genérico para projeção perspectiva.

O volume de visão possui um formato piramidal pois todos os pontos do volume estão sobre projetores que convergem em direção à origem do espaço da câmera, que é o centro de projeção. O formato da pirâmide é definido unicamente pelos parâmetros \(l\) (left), \(r\) (right), \(b\) (bottom), \(t\) (top), \(n\) (near) e \(f\) (far).

Suponha a cena de um arranjo de 8 cubos conforme mostra a figura 8.12.

Cena dentro do volume de visão de projeção perspectiva.

Figura 8.12: Cena dentro do volume de visão de projeção perspectiva.

Após a normalização do volume de visão, todo o seu conteúdo é distorcido proporcionalmente como mostra a figura 8.13. Observe como os objetos mais distantes ficam menores em relação aos objetos mais próximos, e como as arestas laterais dos cubos não são mais paralelas como na cena original. De fato, elas agora convergem para um ponto de fuga.

Distorção da cena após a normalização do volume de visão.

Figura 8.13: Distorção da cena após a normalização do volume de visão.

Agora que a geometria da cena está distorcida, podemos seguir com o processamento do pipeline de gráfico. Após a rasterização e o mapeamento ortogonal para o espaço da janela, o resultado será uma imagem que tem a aparência de uma projeção perspectiva (figura 8.14).

Objetos em NDC e conteúdo correspondente no espaço da janela.

Figura 8.14: Objetos em NDC e conteúdo correspondente no espaço da janela.

Matriz de projeção

Para construir a matriz a projeção perspectiva, vamos observar primeiro como um ponto \((x_e,y_e,z_e)\) no espaço da câmera (o \(e\) subscrito vem de eye space) é projetado para um ponto \((x_p, y_p, z_p)\) no plano de recorte próximo (isto é, o plano com \(z_e=-n\)).

A figura 8.15 mostra a relação entre esses pontos em uma visão de cima do volume de visão.

Volume de visão visto de cima.

Figura 8.15: Volume de visão visto de cima.

Através da razão entre triângulos semelhantes, temos

\[ \frac{x_p}{-n}=\frac{x_e}{z_e}. \] Logo,

\[ x_p=\frac{-nx_e}{z_e}=\frac{nx_e}{-z_e}. \]

O mesmo raciocínio pode ser aplicado para determinar \(y_p\). A figura 8.16 mostra uma visão lateral do volume de visão.

Volume de visão visto de lado.

Figura 8.16: Volume de visão visto de lado.

Através da razão entre triângulos semelhantes,

\[ \frac{y_p}{-n}=\frac{y_e}{z_e}. \] Logo,

\[ y_p=\frac{-ny_e}{z_e}=\frac{ny_e}{-z_e}. \]

O importante a ser notado aqui é que tanto \(x_p\) quanto \(y_p\) são divididos por \(-z_e\). Então, todo ponto no espaço da câmera deverá ser dividido pela sua coordenada \(z\) negativa.

Podemos incorporar a divisão por \(-z_e\) na matriz de projeção. Lembre-se que, no vertex shader, representamos pontos e vetores em coordenadas homogêneas. A matriz de projeção converte coordenadas homogêneas do espaço da câmera (\(x_e\), \(y_e\), \(z_e\), \(w_e\)) em coordenadas homogêneas do espaço de recorte (\(x_c\), \(y_c\), \(z_c\), \(w_c\)), que são as coordenadas de gl_Position:

\[ \begin{align} \begin{bmatrix} x_c\\y_c\\z_c\\w_c \end{bmatrix} = \mathbf{M}_{\textrm{proj}} \begin{bmatrix} x_e\\y_e\\z_e\\w_e \end{bmatrix}. \end{align} \]

Após o recorte, as coordenadas do espaço de recorte são divididas por \(w_c\) para produzir coordenadas (\(x_n\), \(y_n\), \(z_n\)) no espaço NDC:

\[ \begin{align} \begin{bmatrix} x_n\\y_n\\z_n \end{bmatrix} = \begin{bmatrix} x_c/w_c\\y_c/w_c\\z_c/w_c \end{bmatrix}. \end{align} \]

Aproveitando essa divisão por \(w\), podemos obter a divisão por \(-z_e\) através da mudança da última linha da matriz de projeção, como a seguir:

\[ \begin{align} \begin{bmatrix} x_c\\y_c\\z_c\\w_c \end{bmatrix} = \begin{bmatrix} \cdot & \cdot & \cdot & \cdot \\ \cdot & \cdot & \cdot & \cdot \\ \cdot & \cdot & \cdot & \cdot \\ 0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} x_e\\y_e\\z_e\\w_e \end{bmatrix}. \end{align} \]

Observe que \(w_c=-z_e\). Portanto, as coordenadas serão divididas por \(-z_e\) como desejamos.

Da mesma forma como fizemos para normalizar o volume de visão da projeção ortográfica, sabemos que precisamos mapear os intervalos:

  • Em \(x\): \([l, r]\), no espaço da câmera, para \([-1, 1]\) em NDC;
  • Em \(y\): \([b, t]\), no espaço da câmera, para \([-1, 1]\) em NDC;
  • Em \(z\): \([-n, -f]\), no espaço da câmera, para \([-1, 1]\) em NDC.

Os fatores de translação e escala em \(x\) e em \(y\) são os mesmos da projeção ortográfica. Assim, temos a seguinte relação entre coordenadas em NDC \((x_{\textrm{ndc}}, y_{\textrm{ndc}})\) e coordenadas projetadas \((x_p, y_p)\):

\[ x_{\textrm{ndc}}=a_x x_p + b_x,\\ y_{\textrm{ndc}}=a_y y_p + b_y, \]

onde \(a\) e \(b\) são, respectivamente, os fatores de escala e translação:

\[ a_x=\frac{2}{r-l},\qquad b_x=-\frac{r+l}{r-l},\\[10pt] a_y=\frac{2}{t-b},\qquad b_y=-\frac{t+b}{t-b}. \]

Se substituirmos

\[x_p=\dfrac{n \cdot x_e}{-z_e}\]

na expressão

\[x_{\textrm{ndc}}=a_xx_p + b_x,\]

obtemos a relação final entre a coordenada \(x_e\) do espaço da câmera e a coordenada \(x_{\textrm{ndc}}\) no espaço NDC (o mesmo raciocínio pode ser aplicado para a transformação de \(y_e\) em \(y_{\textrm{ndc}}\)):

\[ \begin{align} x_{\textrm{ndc}}&=a_x x_p + b_x\\[10pt] &=\dfrac{2x_p}{r-l}-\dfrac{r+l}{r-l}\\[10pt] &=\dfrac{2 \cdot \dfrac{n \cdot x_e}{-z_e}}{r-l}-\dfrac{r+l}{r-l}\\[10pt] &=\dfrac{2n \cdot x_e}{-z_e(r-l)}-\frac{r+l}{r-l}\\[10pt] &=\dfrac{\dfrac{2n}{r-l} \cdot x_e}{-z_e}-\frac{r+l}{r-l}\\[10pt] &=\dfrac{\dfrac{2n}{r-l} \cdot x_e}{-z_e}+\dfrac{\dfrac{r+l}{r-l} \cdot z_e}{-z_e}\\[10pt] &=\dfrac{x_c}{-z_e}, \end{align} \]

onde

\[ \begin{align} x_c &= n \cdot a_x \cdot x_e - b_x \cdot z_e\\[10pt] &= \dfrac{2n}{r-l} \cdot x_e + \dfrac{r+l}{r-l} \cdot z_e. \end{align} \] De forma semelhante,

\[ y_{\textrm{ndc}} = \frac{y_c}{-z_e}, \]

onde

\[ \begin{align} y_c &= n \cdot a_y \cdot y_e - b_y \cdot z_e\\[10pt] &= \frac{2n}{t-b} \cdot y_e + \frac{t+b}{t-b} \cdot z_e. \end{align} \] Atualizando os elementos da matriz de projeção,

\[ \begin{align} \begin{bmatrix} x_c\\[0.35em]y_c\\[0.35em]z_c\\[0.35em]w_c \end{bmatrix} = \begin{bmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ \cdot & \cdot & \cdot & \cdot \\ 0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} x_e\\[0.35em]y_e\\[0.35em]z_e\\[0.35em]w_e \end{bmatrix}. \end{align} \]

Ainda precisamos determinar os elementos da terceira linha da matriz. Esses elementos correspondem à transformação de \(z_e\) em \(z_c\).

O valor de \(z_c\) não depende de \(x_e\) e \(y_e\). Assim, os valores nas duas primeiras colunas da terceira linha devem ser zero. Só precisamos determinar os elementos da terceira e quarta colunas, que chamaremos de \(\alpha\) e \(\beta\):

\[ \begin{align} \begin{bmatrix} x_c\\[0.35em]y_c\\[0.35em]z_c\\[0.35em]w_c \end{bmatrix} = \begin{bmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & \alpha & \beta \\ 0 & 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} x_e\\[0.35em]y_e\\[0.35em]z_e\\[0.35em]w_e \end{bmatrix}. \end{align} \] Logo,

\[ z_c=\alpha z_e + \beta w_e. \] Após a divisão pelo \(w\),

\[ z_n=\frac{\alpha z_e + \beta w_e}{-z_e}. \]

Sabendo que o intervalo \([-n, -f]\) deve ser mapeado para o intervalo \([-1, 1]\), podemos formar um sistema de equações lineares:

\[ \begin{array}{l} \dfrac{-\alpha n + \beta}{n}=-1\\[10pt] \dfrac{-\alpha f + \beta}{f}=1 \end{array} \quad \rightarrow \quad \begin{array}{l} -\alpha n + \beta = -n\\[10pt] -\alpha f + \beta = f \end{array} \]

Logo,

\[ \begin{align} \alpha=-\frac{f+n}{f-n},\\[10pt] \beta=-\frac{2fn}{f-n}. \end{align} \]

Com isso obtemos todos os elementos da matriz de projeção perspectiva:

\[ \begin{align} \mathbf{M}_{\textrm{persp}}= \begin{bmatrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}. \end{align} \]

Na biblioteca GLM, tal matriz pode ser criada com a função glm::frustum definida em glm/gtc/matrix_transform.hpp:

glm::mat4 glm::frustum(float left, float right, float bottom, float top, float zNear, float zFar);
glm::dmat4 glm::frustum(double left, double right, double bottom, double top, double zNear, double zFar);

onde left, right, bottom, top, zNear e zFar correspondem respectivamente aos valores \(l\), \(r\), \(b\), \(t\), \(n\) e \(f\).

Se o volume de visão for simétrico, então

\[ r = -l,\\ t = -b. \]

Assim como na projeção ortográfica com volume de visão simétrico, os termos da matriz podem ser simplificados como segue:

\[ \begin{align} r+l&=0,\\ r-l&=2r,\\ t+b&=0,\\ t-b&=2t,\\ \end{align} \]

e a matriz é simplificada para

\[ \begin{align} \mathbf{M}_{\textrm{persp}}= \begin{bmatrix} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{t} & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}. \end{align} \] Uma forma mais intuitiva de criar um volume de visão simétrico para a projeção perspectiva é através dos seguintes parâmetros:

  • Ângulo \(\theta\) de abertura vertical do campo de visão (field of view ou FOV).
  • Razão de aspecto \(w/h\) (largura pela altura) do plano de imagem.
  • Distâncias \(n\) e \(f\) dos planos de recorte próximo (near) e distante (far).

Usando relações trigonométricas, podemos determinar o valor de \(t\) (top em glm::frustum) (figura 8.17):

\[ \begin{align} &\frac{t}{n} = \tan \left( \frac{\theta}{2} \right),\\[10pt] &t = n\tan \left( \frac{\theta}{2} \right). \end{align} \]

Por simetria,

\[ \begin{align} b &= -t.\\ \end{align} \]

Ângulo de abertura do campo de visão vertical.

Figura 8.17: Ângulo de abertura do campo de visão vertical.

Para calcular \(r\) (right em glm::frustum), multiplicamos \(t\) pela razão de aspecto.

\[ r = t \frac{w}{h}.\\ \]

Assim, em um viewport de tamanho \(1920 \times 1080\), a razão de aspecto será \(16:9\) (widescreen). Se \(t=1080/2=540\), então \(r=540\times \frac{16}{9}=1920/2=960\).

Por simetria,

\[ \begin{align} l &= -r.\\ \end{align} \]

Na biblioteca GLM, tal matriz pode ser criada com a função glm::perspective, definida em glm/gtc/matrix_transform.hpp:

glm::mat4 perspective(float fovy, float aspect, float zNear, float zFar);
glm::dmat4 perspective(double fovy, double aspect, double zNear, double zFar);

onde fovy, aspect, zNear e zFar correspondem respectivamente aos valores \(\theta\) (em radianos), \(w/h\), \(n\) e \(f\).