24. Mais sobre ray tracing

Lembre-se de que o ray tracing é um método poderoso para sintetizar imagens altamente realistas. Ao contrário do que fizemos para usar o pipeline gráfico do WebGL, o ray tracing implementa um modelo global para a geração de imagens, baseado no rastreamento dos raios de luz, trabalhando principalmente de trás para a frente, ou seja, a partir do observador (ou câmera) para as fontes de luz. Na aula anterior apresentamos o princípio geral do processo e vimos como a reflexão e a refração são implementadas a partir de um modelo de iluminação baseado no modelo de Phong.

Na aula de hoje vamos ver mais alguns aspectos do ray tracing que completam o processo de renderização. Para isso vamos considerar como os raios são representados, gerados e como as interseções são determinadas.

24.1. Representação de Raios

Primeiramente, como um raio é representado? Um método óbvio é representá-lo por seu ponto de origem \(P\) e um vetor direcional \(\vec{u}\). Pontos pertencentes ao raio podem ser descritos parametricamente usando um escalar \(t\) tal que

\(R = { P + t \vec{u} \;\; | \;\; t > 0 }\).

Observe que esse raio \(R\) é aberto, no sentido que não inclui sua extremidade (apenas sua origem \(P\)). Isso é feito porque em muitos casos (por exemplo, reflexão) estamos disparando um raio da superfície de algum objeto. Como não queremos considerar a própria superfície como uma interseção, na prática, é bom exigir que \(t\) seja maior do que algum valor muito pequeno, por exemplo, \(t \ge 10^{-3}\). Isso é feito por causa dos erros propagados pelas operações em ponto flutuante.

Nas implementações de ray tracing, também é comum armazenar algumas informações adicionais como parte de um objeto do tipo Raio. Por exemplo, em geral é útil armazenar o valor \(t_0\) que define o ponto de intersecção com o objeto mais próximo (inicialmente, \(t0 = \infty\)) e talvez um ponteiro para o objeto intersectado.

24.2. Geração de raios

Consideremos a questão de como gerar raios, uma para cada pixel a ser pintado, como ilustrado na Fig. 24.1. Vamos supor que recebemos essencialmente as mesmas informações que usamos na função lookAt(eye, at, up) e perspective(fovy, aspect, near, far). Em particular, seja \(eye\) a posição do olho, \(at\) o ponto central para o qual a câmera está olhando, e seja \(up\) o vetor up como usado na lookAt(). Seja \(\theta_y\) o campo de visão vertical (\(y\)). Sejam \(nlins\) e \(ncols\) o número de linhas e colunas na imagem final, e \(\alpha = ncols /nlins\) a razão de aspecto da janela de visualização que contém a imagem.

Geração de raios para cada pixel.

Fig. 24.1 Geração de raios para cada pixel. Fonte: Notas do Prof. Mount.

Na função perspective() também especificamos a distância para os planos de recorte near e far. Isso foi necessário para configurar o buffer de profundidade. Como não há buffer de profundidade no ray tracing, esses valores não são necessários; assim, por simplificação, vamos supor que a janela esteja exatamente uma unidade na frente do olho. (A distância não é importante, pois a proporção e o campo de visão realmente determinam tudo até um fator de escala.)

A altura e a largura da janela de visualização em relação ao seu ponto central são

\(h = 2 \tan \cfrac{\theta_y}{2}\;\;\;\) e \(\;\;\; w = h \cdot \alpha\).

Assim, a janela se estende de \(-h/2\) a \(+h/2\) de altura e \(-w/2\) a \(+w/2\) de largura. Para obter o sistema de coordenadas da câmera, vamos proceder de forma semelhante à aula 13. A origem do sistema da câmera é o ponto \(eye\). Os vetores da base são:

\(e_z = \mbox{normalize}(eye - at),\;\;\; e_x = \mbox{normalize}(up \times e_z)\;\;\ e \;\;\ e_y = e_z \times e_x\).

Vamos seguir a convenção (agora um pouco estranha) usada em arquivos .bmp e assumir que as linhas são indexadas de baixo para cima (de cima para baixo é mais comum) e as colunas são indexadas da esquerda para a direita. Cada ponto na janela de visualização tem coordenada \(z = -1\). Agora, suponha que queremos lançar um raio para a linha \(lin\) e a coluna \(col\), onde \(0 \le lin \le nlins\) e \(0 \le col \le ncols\). Observe que \(lin/nlins\) está no intervalo de 0 a 1. Ao multiplicarmos por \(h\) o resultado permanece no intervalo \([0, +h]\) e então subtraindo \(h/2\) obtemos o intervalo final desejado \([-h/2, h/2]\).

\(\vec{u}_{lin} = h \left( \cfrac{lin}{nlins} - \cfrac{1}{2} \right), \;\;\;\) \(\vec{u}_{col} = w \left( \cfrac{col}{ncols} - \cfrac{1}{2} \right)\).

A localização do ponto correspondente na janela de visualização é

\(P ( lin , col ) = eye + \vec{u}_{col} e_x + \vec{u}_{lin} e_y - e_z\).

Assim, o raio desejado \(R(lin, col)\) (veja a Fig. 24.1) tem a origem \(eye\) e o vetor direcional

\(\vec{u}(lin, col) = normalize( P(lin, col) - eye )\).

24.3. Raios e Interseções

Dado um objeto na cena, um procedimento de interseção de raios determina se o raio intercepta o objeto e, nesse caso, retorna o valor \(t' > 0\) que define onde ocorre a interseção. Este é um uso natural da programação orientada a objetos, uma vez que o procedimento de interseção pode se tornar um método do objeto.

Considerando que a cena é formada por vários objetos, um raio pode intersectar mais de um objeto. Considere que o raio mantenha um valor \(t_0\) que indica o ponto (e objeto) de contato mais próximo calculado até agora. Caso haja intersecção e o valor de \(t'\) for menor que o valor atual de \(t_0\), então \(t_0\) é definido como \(t'\). Caso contrário, o raio foi aparado antes e não cruzará esse objeto distante \(t\). Na prática, é útil que o procedimento de interseção determine duas outras quantidades. Primeiro, deve retornar o vetor normal no ponto de interseção e segundo, deve indicar se a interseção ocorre dentro ou fora do objeto. Essa última informação é útil para calcular a refração em superfícies transparentes.

24.4. Interseção Raio-Esfera

Vamos considerar um dos testes de interseção não triviais mais populares para raios, o teste de interseção com uma esfera no espaço 3D. Representamos um raio \(R\) pelo seu ponto de origem \(P\) e um vetor direcional normalizado \(\vec{u}\). Suponha que a esfera seja representada pelo seu centro \(C\) e um raio escalar \(r\) (veja a Fig. 24.2). Nosso objetivo é determinar o valor de \(t\) para o qual o raio atinge a esfera, ou responder que não há interseção.

Interseção de raio com uma esfera.

Fig. 24.2 Interseção de raio com uma esfera.

Sabemos que um ponto \(Q\) está na esfera se a sua distância ao centro da esfera for \(r\), isto é, se \(|Q - C| = r\). Devemos calcular o valor de \(t\) que define o comprimento do raio tal que

\(| (P + t \vec{u} - C)| = r\).

Observe que a quantidade dentro do \(| \cdot |\) acima é um vetor. Seja \(\vec{w} = C - P\). Assim temos que

\(|t \vec{u} - \vec{w}| = r\)

Sabemos calcular \(\vec{u}\), \(\vec{w}\), e \(r\). Para calcular \(t\) podemos aplicar a definição de comprimento do vetor usando produto escalar tal que

\((t \vec{u} - \vec{w}) \cdot (t \vec{u} - \vec{w}) = r^2\).

Observe que essa equação tem valor escalar (não é um vetor). Como o produto escalar é um operador linear podemos manipulá-lo algebricamente tal que

\(t^2 (\vec{u} \cdot \vec{u}) - 2 t (\vec{u} \cdot \vec{w}) + (\vec{w} \cdot \vec{w}) - r^2 = 0\).

Essa é uma equação quadrática da forma \(a t^2 + b t + c = 0\) onde:

  • \(a = (\vec{u} \cdot \vec{u}) = 1\), pois \(\vec{u}\) é normalizado,
  • \(b = -2(\vec{u} \cdot \vec{w} )\),
  • \(c = (\vec{w} \cdot \vec{w}) - r^2\).

A solução dessa equação quadrática produz duas raízes. Como \(a=1\) temos que \(\Delta = (b^2 - 4c)\) e as raízes

\(t^- \;=\; \cfrac{-b\; - \;\sqrt{ \Delta }} {2} \;\;\;\) e \(\;\;\; t^+ \;=\; \cfrac{-b\; + \;\sqrt{ \Delta }} {2}\).

Se \(t^- > 0\) usamos \(t^-\) para definir o ponto de interseção. Caso contrário, se \(t^+ > 0\) nós usamos \(t^+\). Se nenhum dos dois for positivos, então não há interseção.

Observe que não é uma boa ideia comparar números reais (em ponto flutuante) com zero, pois erros de ponto flutuante sempre são possíveis. Uma regra geral é considerar um valor pequeno, por exemplo, t > EPSILON = 1E-3. A escolha adequada de EPSILON é um pouco “mágica”. Geralmente é ajustado até que a imagem final pareça boa.

24.5. Vetor Normal

Além de calcular a interseção do raio com o objeto, também é necessário calcular o vetor normal no ponto de interseção. No caso da esfera, observe que o vetor normal é direcionado do centro da esfera ao ponto de contato. Assim, se \(t\) é o valor do parâmetro no ponto de contato, o vetor normal é apenas

\(\vec{n} = normalize(P + t \vec{u} - C)\).

Observe que esse vetor é direcionado para fora da esfera. Se \(t^-\) foi usado para definir a interseção, então estamos atingindo o objeto por fora, e então \(\vec{n}\) é a normal desejada. No entanto, se \(t^+\) foi usado para definir a interseção, então estamos atingindo o objeto por dentro e nesse caso \(-\vec{n}\) deve ser usado.

24.6. Equação de Iluminação para Ray Tracing

Para implementar um programa baseado nessa técnica simples de ray tracing, vamos combinar o familiar modelo de iluminação de Phong com a reflexão e a refração calculadas anteriormente. Considere que disparamos um raio e que ele atingiu um objeto em algum ponto \(P\).

  • Fontes de luz: Vamos supor que temos uma coleção de fontes de luz \(L_1, L_2, \ldots\). Cada fonte está associada a uma cor RGB (um vetor de intensidades, com valores não negativos). Seja \(L_a\) a cor RGB da luz ambiente, que é aplicada globalmente.

  • Visibilidade das fontes de luz: A função Vis(P, i) retorna 1 se a fonte de luz \(i\) estiver visível no ponto \(P\) e 0 caso contrário. Se não houver objetos transparentes, isso pode ser calculado simplesmente disparando um raio de \(P\) para a fonte de luz e vendo se ele atinge algum objeto.

    Quando objetos transparentes estão presentes, isso é consideravelmente mais difícil, pois precisamos considerar todos os feixes de luz refratados que atingem a fonte de luz. A área de cáustica trata da simulação de iluminação indireta através de objetos transparentes. Uma suposição simplificadora (mas irrealista) é que objetos transparentes nunca bloqueiam a luz da iluminação. Um pouco mais realista é assumir que o objeto transparente atenua a luz de acordo com um valor \(\rho_t\).

  • Cor do material: Assumimos que a cor do material de um objeto é dada por \(C\). Este também é um vetor RGB, no qual cada componente está no intervalo [0, 1]. Assumimos que a cor especular é a mesma da fonte de luz e que o objeto não emite luz. Seja \(\rho_a\), \(\rho_d\) e \(\rho_s\) os coeficientes de iluminação ambiente, de difusão e especular, respectivamente. Esses coeficientes estão tipicamente no intervalo [0, 1]. Seja \(\alpha\) o coeficiente de brilho especular.

  • Vetores: Sejam \(\vec{n}\), \(\vec{h}\) e \(\vec{l}\) os vetores normal, halfway e de luz, todos normalizados. Consulte a aula sobre o modelo de iluminação de Phong para saber como eles são calculados.

  • Atenuação: Assumimos que a atenuação da luz é quadrática, dada pelos coeficientes \(a\), \(b\) e \(c\), como no modelo de Phong. Seja \(d_i\) a distância do ponto de contato \(P\) até a \(i\)-ésima fonte de luz.

  • Reflexão e refração: Sejam \(\rho_r\) e \(\rho_t\) os coeficientes de iluminação de reflexão e de transmissão (refratados). Se \(\rho_t \ne 0\) então sejam \(\eta_i\) e \(\eta_t\) os índices de refração, e sejam \(\vec{r}_v\) e \(\vec{t}\) os vetores de reflexão e transmissão normalizados.

    Seja o par \((P, \vec{u})\) um raio originado no ponto \(P\) e indo na direção \(\vec{u}\). A equação completa de reflexão para ray tracing é:

    \(\begin{flalign}I = \rho_a L_a C + \sum_i Vis(P, i) \cfrac{L_i}{a + b d_i + c d_i^2} [\rho_d C max(0, \vec{n}\cdot \vec{l}) + \rho_s max(0, (\vec{n} \cdot \vec{h})^\alpha) ]\\ + \rho_r trace(P, \vec{r}_v) + \rho_t trace(P, \vec{t})\end{flalign}.\)

    Lembre-se de que Vis(P, i) indica se a \(i\)-ésima fonte de luz é visível de \(P\). Observe que se \(\rho_r\) ou \(\rho_t\) forem iguais a 0 (como é frequentemente o caso), então a chamada de trace() correspondente não precisa ser feita. Observe que a atenuação e a iluminação não são aplicadas aos resultados de reflexão e refração. Esse modelo parece se comportar razoavelmente na maioria das situações de iluminação, onde luzes e objetos estão relativamente próximos do olho.

  • Questões Numéricas: Existem algumas instabilidades numéricas que devem ser observadas ao lidar com a interseção raio-esfera. Se \(r\) é pequeno em relação a \(|\vec{w}|\) (o que acontece quando a esfera está distante), então podemos perder o efeito de \(r\) no cálculo de \(\Delta\). Na maioria das aplicações, essa precisão adicional não é garantida. Se você precisar, consulte um livro sobre análise numérica para cálculos mais precisos.

24.7. Onde estamos e para onde vamos?

Nessa aula completamos a descrição da técnica básica de ray tracing. Nossa descrição foi baseada no modelo mais simples de Phong e permite tratar fenômenos mais complexos como reflexão, refração e sombras. Apesar de permitir a geração de imagens mais realistas, essa técnica ainda apresenta limitações, principalmente por pintar cada pixel usando apenas um raio (ou um número bem pequeno deles). Efeitos de interações mais complexas da luz com os objetos, como cáusticos (caustics), inter reflexão difusa e sangramento de cor podem ser simulados por técnicas ainda mais complexas como radiosidade e mapeamento de fótons.

Essa aula termina nosso curso de introdução à computação gráfica usando WebGL. Nessa introdução, buscamos reforçar suas habilidades de raciocínio geométrico e sua relação com a álgebra linear. Essas ferramentas tornam possível o modelamento de objetos e a implementação de algoritmos bastante eficientes para realizar as transformações geométricas necessárias para criar animações de cenas realísticas e simulações complexas em tempo real.

Os próximos tópicos dessas notas de aula são opcionais e foram colocamos a sua disposição caso você continue com o interesse de aprender ainda mais sobre Computação Gráfica.

24.8. Para saber mais