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.8 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.8: 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.9).

Pontos de fuga na projeção perspectiva.

Figura 8.9: 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×2×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.10.

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

Figura 8.10: 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.11.

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

Figura 8.11: 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.12. 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.12: 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.13).

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

Figura 8.13: 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 (xe,ye,ze) no espaço da câmera (o e subscrito vem de eye space) é projetado para um ponto (xp,yp,zp) no plano de recorte próximo (isto é, o plano com ze=n).

A figura 8.14 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.14: Volume de visão visto de cima.

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

xpn=xeze. Logo,

xp=nxeze=nxeze.

O mesmo raciocínio pode ser aplicado para determinar yp. A figura 8.15 mostra uma visão lateral do volume de visão.

Volume de visão visto de lado.

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

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

ypn=yeze. Logo,

yp=nyeze=nyeze.

O importante a ser notado aqui é que tanto xp quanto yp são divididos por ze. Então, todo ponto no espaço da câmera deverá ser dividido pela sua coordenada z negativa.

Podemos incorporar a divisão por ze 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 (xe, ye, ze, we) em coordenadas homogêneas do espaço de recorte (xc, yc, zc, wc), que são as coordenadas de gl_Position:

[xcyczcwc]=Mproj[xeyezewe].

Após o recorte, as coordenadas do espaço de recorte são divididas por wc para produzir coordenadas (xn, yn, zn) no espaço NDC:

[xnynzn]=[xc/wcyc/wczc/wc].

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

[xcyczcwc]=[0010][xeyezewe].

Observe que wc=ze. Portanto, as coordenadas serão divididas por ze 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 (xndc,yndc) e coordenadas projetadas (xp,yp):

xndc=axxp+bx,yndc=ayyp+by,

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

ax=2rl,bx=r+lrl,ay=2tb,by=t+btb.

Se substituirmos

xp=nxeze

na expressão

xndc=axxp+bx,

obtemos a relação final entre a coordenada xe do espaço da câmera e a coordenada xndc no espaço NDC (o mesmo raciocínio pode ser aplicado para a transformação de ye em yndc):

xndc=axxp+bx=2xprlr+lrl=2nxezerlr+lrl=2nxeze(rl)r+lrl=2nrlxezer+lrl=2nrlxeze+r+lrlzeze=xcze,

onde

xc=naxxebxze=2nrlxe+r+lrlze. De forma semelhante,

yndc=ycze,

onde

yc=nayyebyze=2ntbye+t+btbze. Atualizando os elementos da matriz de projeção,

[xcyczcwc]=[2nrl0r+lrl002ntbt+btb00010][xeyezewe].

Ainda precisamos determinar os elementos da terceira linha da matriz. Esses elementos correspondem à transformação de ze em zc.

O valor de zc não depende de xe e ye. 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 α e β:

[xcyczcwc]=[2nrl0r+lrl002ntbt+btb000αβ0010][xeyezewe]. Logo,

zc=αze+βwe. Após a divisão pelo w,

zn=αze+βweze.

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

αn+βn=1αf+βf=1αn+β=nαf+β=f

Logo,

α=f+nfn,β=2fnfn.

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

Mpersp=[2nrl0r+lrl002ntbt+btb000f+nfn2fnfn0010].

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:

r+l=0,rl=2r,t+b=0,tb=2t,

e a matriz é simplificada para

Mpersp=[nr0000nt0000f+nfn2fnfn0010]. 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 θ 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.16):

tn=tan(θ2),t=ntan(θ2).

Por simetria,

b=t.

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

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

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

r=twh.

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

Por simetria,

l=r.

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 θ (em radianos), w/h, n e f.