8.1 Projeção ortográfica
Por padrão, o pipeline do OpenGL produz uma projeção ortográfica das primitivas contidas no volume de visão, como se as coordenadas \(z\) de todos os pontos dentro do volume de visão fossem descartadas para formar figuras no plano de imagem.
Lembre-se que o volume de visão é um cubo \(2 \times 2 \times 2\) centralizado na origem no espaço normalizado do dispositivo (NDC). Logo antes da rasterização, o pipeline converte automaticamente as coordenadas do NDC para o espaço da janela, em pixels.
Na configuração padrão de glViewport(x, y, w, h)
e glDepthRange(n, f)
, o canto inferior esquerdo da janela é a origem do espaço da janela (\(x=y=0\)), e o canto superior direito é a coordenada \((w,h)\) onde \(w\) e \(h\) correspondem respectivamente à largura e altura da janela em pixels29. Assim, o seguinte mapeamento é feito internamente pelo pipeline:
- \(x_{\textrm{ndc}} \in [-1,1]\) em NDC torna-se \(x_w \in [x,w]\) no espaço da janela.
- \(y_{\textrm{ndc}} \in [-1,1]\) em NDC torna-se \(y_w \in [y,h]\) no espaço da janela.
- \(z_{\textrm{ndc}} \in [-1,1]\) em NDC torna-se \(z_w \in [n,f]\) no espaço da janela.
O mapeamento pode ser representado pela seguinte matriz de viewport:
\[
\begin{bmatrix}
x_w\\[0.5em] y_w \\[0.5em] z_w
\end{bmatrix}
=
\begin{bmatrix}
\frac{w}{2} & 0 & 0 & x+\frac{w}{2}\\
0 & \frac{h}{2} & 0 & y+\frac{h}{2}\\
0 & 0 & \frac{f-n}{2} & \frac{f+n}{2}
\end{bmatrix}
\begin{bmatrix}
x_{\textrm{ndc}}\\ y_{\textrm{ndc}}\\ z_{\textrm{ndc}}\\ 1
\end{bmatrix}.
\]
Por padrão, \(n=0\) e \(f=1\) (configuração padrão de glDepthRange
).
Após o mapeamento para o espaço da janela, as primitivas são rasterizadas. Se mais de um fragmento for mapeado para o mesmo pixel no framebuffer, o teste de profundidade pode ser utilizado para manter apenas o fragmento de menor valor \(z_w\).
A figura 8.5 mostra um exemplo no qual o espaço NDC contém 8 cubos posicionados em \((\pm0.5, \pm0.5, \pm0.5)\), alinhados aos eixos principais. Após a rasterização com o teste de profundidade habilitado na configuração padrão, o conteúdo rasterizado no espaço da janela exibirá apenas a face da frente dos 4 cubos de menor valor \(z\), como ocorreria numa projeção ortográfica sobre o plano \(z=-1\) em NDC. A figura também mostra que a origem do espaço NDC (\(O_{\textrm{ndc}}\)) é mapeada para o centro da janela.
Uma vez que o pipeline do OpenGL usa a projeção ortográfica como padrão, podemos supor inicialmente que nossa matriz de projeção é a matriz identidade, isto é, nenhuma transformação adicional precisa ser feita para produzir a projeção ortográfica. Assim, no vertex shader, as coordenadas serão modificadas apenas pelas matrizes de modelo e visão (\(\mathbf{M}_{\textrm{view}}\mathbf{M}_{\textrm{model}}\)), gerando pontos no espaço da câmera. Relembre que, no espaço da câmera, a posição da câmera é o ponto de referência do frame, e a direção de visão é a direção do eixo \(z\) negativo.
A matriz de projeção é responsável por converter coordenadas do espaço da câmera para o espaço de recorte. Entretanto, como estamos supondo que a matriz de projeção é a matriz identidade, o espaço de recorte é, neste caso, idêntico ao espaço da câmera. Então, os pontos no espaço da câmera podem ser enviados diretamente à variável embutida gl_Position
.
Internamente, o pipeline supõe que, no espaço de recorte, todas as primitivas com coordenadas menores que \(-w\) e maiores que \(+w\) devem ser recortadas. Uma vez que \(w=1\) para todos os pontos, são recortadas todas as primitivas que estiverem fora do cubo que vai de \((-1,-1,-1)\) até \((1,1,1)\). Após o recorte, as coordenadas são divididas por \(w\) para converter coordenadas do espaço de recorte para coordenadas normalizadas do dispositivo. Mas, como \(w=1\), as coordenadas continuam com o mesmo valor. Então, neste caso, o espaço NDC é idêntico ao espaço de recorte projetado, que por sua vez é idêntico ao espaço da câmera.
Há um problema em usar a matriz identidade como matriz de projeção: consideramos até agora que os modelos geométricos são representados em um sistema que segue a regra da mão direita. Entretanto, as coordenadas normalizadas do dispositivo seguem a regra da mão esquerda. Isso faz com que orientação dos triângulos fique invertida (CW vira CCW e vice-versa). Felizmente, é fácil construir uma transformação que converte as coordenadas para a regra da mão direita: basta negarmos a coordenada \(z\) de cada ponto. Essa transformação pode ser feita por uma matriz de projeção ligeiramente diferente de uma matriz identidade:
\[ \mathbf{M}_{\textrm{orth}}= \begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & -1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix}. \]
Agora, no vertex shader, gl_Position
receberá os pontos transformados por \(\mathbf{M}_{\textrm{proj}}\mathbf{M}_{\textrm{view}}\mathbf{M}_{\textrm{view}}\), onde \(\mathbf{M}_{\textrm{proj}}=\mathbf{M}_{\textrm{orth}}\).
Com a matriz \(\mathbf{M}_{\textrm{orth}}\) conseguimos consertar a inversão de orientação das primitivas. No entanto, há ainda outro problema: como a câmera está na origem em NDC, só conseguimos enxergar na tela a geometria que estiver contida no cubo de tamanho 2 em torno da câmera (isto é, a geometria após o recorte). Esse tamanho é muito limitante para a maioria das cenas. Além disso, a posição da câmera no centro do cubo não parece ser algo muito intuitivo. Na câmera LookAt, a direção de visão é a direção de \(z\) negativo. Logo, não deveríamos ser capazes de enxergar algo que está com \(z\) positivo (isto é, atrás da câmera, ainda que dentro do cubo de tamanho de 2). Para resolver isso, vamos criar uma nova matriz de projeção que supõe que o volume de visão está sempre situado em algum lugar do espaço da câmera com \(z<0\), como ilustra a figura 8.6.
Nessa figura, o lado mais perto do volume de visão está a uma distância \(n\) da câmera, medida ao longo de sua linha de visão (eixo \(z\) negativo). Esse lado mais próximo em relação à posição da câmera é chamado de plano de recorte próximo ou near clipping plane. O lado mais distante do volume de visão está a uma distância \(f\) da câmera, e é chamado de plano de recorte distante ou far clipping plane.
Na definição desse novo volume de visão, usaremos parâmetros \(l\) (left), \(r\) (right), \(b\) (bottom), \(t\) (top) para especificar a posição dos lados esquerdo e direito, de baixo e de cima do volume. Desse modo, o volume não precisará ser mais um cubo de tamanho 2 em cada direção. Podemos obter essa configuração através da modificação da matriz de projeção.
É interessante notar que a matriz de projeção ortográfica é simplesmente uma matriz que transforma o volume de visão, do espaço da câmera, para o volume de visão em NDC, como mostra a figura 8.7.
Esse mapeamento da transformação de projeção consiste em fazer com que os pontos \((l,b,-n)\) e \((r,t,-f)\) no espaço da câmera tornem-se respectivamente os pontos \((-1,-1,-1)\) e \((1,1,1)\) em NDC. Tal processo é chamado de normalização do volume de visão. Podemos fazer a normalização em três etapas:
- Translação do volume de visão de modo a centralizá-lo na origem.
- Escala do volume de visão de modo a deixá-lo com tamanho 2 em cada direção.
- Reflexão para inverter a coordenada \(z\).
Como a reflexão é uma escala com inversão de sinal, as etapas 2 e 3 podem ser feitas em conjunto, como veremos a seguir.
Translação
O centroide \(C=(c_x,c_y,c_z)\) do volume de visão no espaço da câmera é
\[ c_x = \frac{r+l}{2},\qquad c_y = \frac{t+b}{2},\qquad c_z = -\frac{f+n}{2}. \]
Logo, a matriz de translação que desloca o volume de visão para a origem é a matriz de translação por \(-C\):
\[ \mathbf{T}= \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}. \]
Escala e reflexão
Os fatores de escala \(S=(s_x, s_y, s_z)\) para redimensionar o volume de visão em um cubo com tamanho 2 em cada direção são:
\[ s_x = \frac{2}{r-l},\qquad s_y = \frac{2}{t-b},\qquad s_z = \frac{2}{f-n}. \] Entretanto, precisamos refletir o cubo na direção \(z\) para a conversão da regra da mão direita para mão esquerda. Assim, precisamos inverter o sinal de \(s_z\):
\[ s_z = -\frac{2}{f-n}. \] A matriz de escala ficará como a seguir:
\[ \mathbf{S}= \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0\\ 0 & \frac{2}{t-b} & 0 & 0\\ 0 & 0 & -\frac{2}{f-n} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix}. \]
Matriz de projeção
Concatenando as transformações de translação, escala e reflexão, obtemos a nova matriz de projeção ortográfica:
\[ \begin{align} \mathbf{M}_{\textrm{orth}}=\mathbf{S}\mathbf{T}&= \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & 0\\ 0 & \frac{2}{t-b} & 0 & 0\\ 0 & 0 & -\frac{2}{f-n} & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \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},\\ \mathbf{M}_{\textrm{orth}}&= \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}{f-n} & -\frac{f+n}{f-n}\\ 0 & 0 & 0 & 1 \end{bmatrix}. \end{align} \]
Na biblioteca GLM, tal matriz pode ser criada com a função glm::ortho
, definida em glm/gtc/matrix_transform.hpp
:
float left, float right, float bottom, float top, float zNear, float zFar);
glm::mat4 glm::ortho(double left, double right, double bottom, double top, double zNear, double zFar); glm::dmat4 glm::ortho(
onde left
, right
, bottom
, top
, zNear
e zFar
correspondem respectivamente aos valores \(l\), \(r\), \(b\), \(t\), \(n\) e \(f\).
Na maioria das aplicações, trabalhamos com volumes simétricos nas direções \(x\) e \(y\). Nesse caso, os pontos \((0,0,z)\) em NDC são projetados no centro do viewport. Em um volume simétrico,
\[ r = -l,\\ t = -b. \]
Com isso, os termos da matriz anterior podem ser simplificados como segue:
\[ \begin{align} r+l&=0,\\ r-l&=2r,\\ t+b&=0,\\ t-b&=2t,\\ \end{align} \]
e a matriz assume o fomato
\[ \mathbf{M}_{\textrm{orth}}= \begin{bmatrix} \frac{1}{r} & 0 & 0 & 0\\ 0 & \frac{1}{t} & 0 & 0\\ 0 & 0 & -\frac{2}{f-n} & -\frac{f+n}{f-n}\\ 0 & 0 & 0 & 1 \end{bmatrix}. \]
A configuração padrão é a configuração utilizada em todos os projetos da ABCg feitos até agora.↩︎