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 pixels30. 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 descartar fragmentos que, por exemplo, tenham um \(z_w\) maior que o \(z_w\) atualmente contido no buffer de profundidade.

A figura 8.6 mostra um exemplo no qual o espaço NDC contém 8 pequenos cubos centralizados 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 corresponderá apenas à 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.

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

Figura 8.6: Objetos em NDC e conteúdo correspondente no espaço 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 podem ser modificadas apenas pelas transformações das matrizes de modelo e visão (\(\mathbf{M}_{\textrm{view}}\mathbf{M}_{\textrm{model}}\)), gerando pontos no espaço da câmera. Lembre-se que, no espaço da câmera, a posição da câmera é o ponto de referência do quadro, 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{model}}\), 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\times 2 \times 2\) em torno da câmera, isto é, a geometria que, após o recorte, estiver dentro do volume de visão. Esse tamanho \(2\times 2 \times 2\) é muito limitante para a maioria das cenas. Além disso, a posição da câmera no centro do cubo não é algo 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\times 2 \times 2\)). Para resolver isso, podemos 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.7.

Volume de visão genérico para projeção ortográfica.

Figura 8.7: Volume de visão genérico para projeção ortográfica.

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.8.

A matriz de projeção ortográfica representa a transformação do volume de visão do espaço da câmera para o volume de visão em NDC.

Figura 8.8: A matriz de projeção ortográfica representa a transformação do volume de visão do espaço da câmera para o volume de visão em NDC.

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:

  1. Translação do volume de visão de modo a centralizá-lo na origem.
  2. Escala do volume de visão de modo a deixá-lo com tamanho 2 em cada direção.
  3. 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:

glm::mat4 glm::ortho(float left, float right, float bottom, float top, float zNear, float zFar);
glm::dmat4 glm::ortho(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\).

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}. \]


  1. A configuração padrão é a configuração utilizada em todos os projetos da ABCg feitos até agora.↩︎