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.
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).
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.
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.
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.
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).
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.
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.
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
:
::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); glm
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} \]
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
:
::mat4 perspective(float fovy, float aspect, float zNear, float zFar);
glm::dmat4 perspective(double fovy, double aspect, double zNear, double zFar); glm
onde fovy
, aspect
, zNear
e zFar
correspondem respectivamente aos valores \(\theta\) (em radianos), \(w/h\), \(n\) e \(f\).