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

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

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

Já que o OpenGL já produz uma projeção ortográfica, vamos supor primeiramente que uma matriz de projeção não é necessária. Assim, no vertex shader, as coordenadas de pontos são transformadas apenas pelas matrizes de modelo e visão (\(\mathbf{M}_{\textrm{view}}\mathbf{M}_{\textrm{model}}\)), gerando pontos no espaço da câmera. Como estamos supondo que não há matriz de projeção, os pontos no espaço da câmera são enviados diretamente à variável embutida gl_Position. 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.

Internamente, o pipeline supõe que gl_Position está em coordenadas de recorte. Nesse espaço, todas as primitivas com coordenadas menores que \(-w\) e maiores que \(+w\) são recortadas. Uma vez que \(w=1\) para todos os pontos, serã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 de recorte em coordenadas normalizadas do dispositivo. Mas, como \(w=1\), as coordenadas continuam com o mesmo valor. Então, neste caso, o espaço NDC é o próprio espaço da câmera. Mas há um problema: as coordenadas normalizadas do dispositivo seguem a regra da mão esquerda. Isso faz com que orientação dos triângulos seja invertida (CW vira CCW e vice-versa). Felizmente, é fácil converter as coordenadas para a regra da mão direita: basta negar a coordenada \(z\) de cada ponto. Este já é um motivo para usarmos uma matriz de projeção. Podemos primeiro considerar que nossa matriz de projeção é a matriz

\[ \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}}\).

Consertamos a inversão de orientação das primitivas. No entanto, há ainda outro problema: como a câmera está na origem em NDC, só é possível enxergar o que está em um cubo de tamanho 2 em torno da câmera (conteúdo após o recorte). Isso vai contra a nossa intuição. 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). Para resolver esses problemas, vamos supor que o volume de visão está situado em algum lugar do espaço da câmera com \(z<0\), como mostra a figura 8.6.

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

Figura 8.6: 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 perto é 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 volume de visão, também 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 matriz de projeção.

A matriz de projeção ortográfica é simplesmente a matriz que transforma o volume de visão do espaço da câmera, para o volume de visão em NDC (figura 8.7).

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

O mapeamento 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. Esse 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.

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

A matriz de projeção é a concatenação da translação com a escala:

\[ \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\), pois assim 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.↩︎