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. Representação de Raios ---------------------- Primeiramente, como um raio é representado? Um método óbvio é representá-lo por seu ponto de origem :math:`P` e um vetor direcional :math:`\vec{u}`. Pontos pertencentes ao raio podem ser descritos parametricamente usando um escalar :math:`t` tal que :math:`R = { P + t \vec{u} \;\; | \;\; t > 0 }`. Observe que esse raio :math:`R` é aberto, no sentido que não inclui sua extremidade (apenas sua origem :math:`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 :math:`t` seja maior do que algum valor muito pequeno, por exemplo, :math:`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 :math:`t_0` que define o ponto de intersecção com o objeto mais próximo (inicialmente, :math:`t0 = \infty`) e talvez um ponteiro para o objeto intersectado. Geração de raios ---------------- Consideremos a questão de como gerar raios, uma para cada pixel a ser pintado, como ilustrado na :numref:`f24-geracao`. 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 :math:`eye` a posição do olho, :math:`at` o ponto central para o qual a câmera está olhando, e seja :math:`up` o vetor ``up`` como usado na ``lookAt()``. Seja :math:`\theta_y` o campo de visão vertical (:math:`y`). Sejam :math:`nlins` e :math:`ncols` o número de linhas e colunas na imagem final, e :math:`\alpha = ncols /nlins` a razão de aspecto da **janela de visualização** que contém a imagem. .. FIGURA GERACAO RAIOS .. figure:: ./figuras/a24/formacao.png :alt: Geração de raios para cada pixel. :name: f24-geracao :width: 80.0% :align: center 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 :math:`h = 2 \tan \cfrac{\theta_y}{2}\;\;\;` e :math:`\;\;\; w = h \cdot \alpha`. Assim, a janela se estende de :math:`-h/2` a :math:`+h/2` de altura e :math:`-w/2` a :math:`+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 :math:`eye`. Os vetores da base são: :math:`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 :math:`z = -1`. Agora, suponha que queremos lançar um raio para a linha :math:`lin` e a coluna :math:`col`, onde :math:`0 \le lin \le nlins` e :math:`0 \le col \le ncols`. Observe que :math:`lin/nlins` está no intervalo de 0 a 1. Ao multiplicarmos por :math:`h` o resultado permanece no intervalo :math:`[0, +h]` e então subtraindo :math:`h/2` obtemos o intervalo final desejado :math:`[-h/2, h/2]`. :math:`\vec{u}_{lin} = h \left( \cfrac{lin}{nlins} - \cfrac{1}{2} \right), \;\;\;` :math:`\vec{u}_{col} = w \left( \cfrac{col}{ncols} - \cfrac{1}{2} \right)`. A localização do ponto correspondente na janela de visualização é :math:`P ( lin , col ) = eye + \vec{u}_{col} e_x + \vec{u}_{lin} e_y - e_z`. Assim, o raio desejado :math:`R(lin, col)` (veja a :numref:`f24-geracao`) tem a origem :math:`eye` e o vetor direcional :math:`\vec{u}(lin, col) = normalize( P(lin, col) - eye )`. 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 :math:`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 :math:`t_0` que indica o ponto (e objeto) de contato mais próximo calculado até agora. Caso haja intersecção e o valor de :math:`t'` for menor que o valor atual de :math:`t_0`, então :math:`t_0` é definido como :math:`t'`. Caso contrário, o raio foi aparado antes e não cruzará esse objeto distante :math:`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. 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 :math:`R` pelo seu ponto de origem :math:`P` e um vetor direcional normalizado :math:`\vec{u}`. Suponha que a esfera seja representada pelo seu centro :math:`C` e um raio escalar :math:`r` (veja a :numref:`f24-raio-esfera`). Nosso objetivo é determinar o valor de :math:`t` para o qual o raio atinge a esfera, ou responder que não há interseção. .. figura 72 .. figure:: ./figuras/a24/raio-esfera.png :alt: Interseção de raio com uma esfera. :name: f24-raio-esfera :width: 50.0% :align: center Interseção de raio com uma esfera. Sabemos que um ponto :math:`Q` está na esfera se a sua distância ao centro da esfera for :math:`r`, isto é, se :math:`|Q - C| = r`. Devemos calcular o valor de :math:`t` que define o comprimento do raio tal que :math:`| (P + t \vec{u} - C)| = r`. Observe que a quantidade dentro do :math:`| \cdot |` acima é um vetor. Seja :math:`\vec{w} = C - P`. Assim temos que :math:`|t \vec{u} - \vec{w}| = r` Sabemos calcular :math:`\vec{u}`, :math:`\vec{w}`, e :math:`r`. Para calcular :math:`t` podemos aplicar a definição de comprimento do vetor usando produto escalar tal que :math:`(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 :math:`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 :math:`a t^2 + b t + c = 0` onde: * :math:`a = (\vec{u} \cdot \vec{u}) = 1`, pois :math:`\vec{u}` é normalizado, * :math:`b = -2(\vec{u} \cdot \vec{w} )`, * :math:`c = (\vec{w} \cdot \vec{w}) - r^2`. A solução dessa equação quadrática produz duas raízes. Como :math:`a=1` temos que :math:`\Delta = (b^2 - 4c)` e as raízes :math:`t^- \;=\; \cfrac{-b\; - \;\sqrt{ \Delta }} {2} \;\;\;` e :math:`\;\;\; t^+ \;=\; \cfrac{-b\; + \;\sqrt{ \Delta }} {2}`. .. equs Se :math:`t^- > 0` usamos :math:`t^-` para definir o ponto de interseção. Caso contrário, se :math:`t^+ > 0` nós usamos :math:`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. 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 :math:`t` é o valor do parâmetro no ponto de contato, o vetor normal é apenas :math:`\vec{n} = normalize(P + t \vec{u} - C)`. Observe que esse vetor é direcionado para fora da esfera. Se :math:`t^-` foi usado para definir a interseção, então estamos atingindo o objeto **por fora**, e então :math:`\vec{n}` é a normal desejada. No entanto, se :math:`t^+` foi usado para definir a interseção, então estamos atingindo o objeto **por dentro** e nesse caso :math:`-\vec{n}` deve ser usado. .. Mapeamento de Fótons -------------------- Nossa descrição da técnica de ray tracing até agora foi baseado do modelo de iluminação de Phong. Embora o ray tracing possa lidar com sombras, ele não é realmente um modelo de iluminação global completo porque não pode lidar com efeitos mais complexos da luz e os objetos. Tais efeitos incluem * Cáusticos (*caustics*): efeitos de iluminação quando a luz é focada através de superfícies refrativas ou reflexivas como vidro e água. Isso causa variações na intensidade da luz nas superfícies de objetos mais próximos. * Iluminação indireta: Isso ocorre quando a luz é refletida de uma superfície (por exemplo, uma parede branca) para outra. * Sangramento de cor (*color bleeding*): Quando a iluminação indireta ocorre com uma superfície colorida, a luz refletida é colorida. Por isso, uma parede branca posicionada ao lado de um objeto verde brilhante captará um pouco da cor verde. Existem vários métodos para implementar modelos de iluminação global. Um exemplo que vamos discutir nessa seção é chamado de mapeamento de fótons (*photon mapping*), que complementa bem a técnica de ray tracing descrita anteriormente. O mapeamento de fótons é particularmente poderoso porque pode lidar com superfícies reflexivas difusas e não difusas (por exemplo, especulares) e pode lidar com geometrias complexas (curvas). Alguns métodos de iluminação global mais simples, como a radiosidade, que não discutiremos, sofrem dessas limitações. A ideia básica por trás do mapeamento de fótons envolve duas etapas: * **Traçado de fótons**: Simula a propagação de fótons da fonte de luz nas superfícies. * **Renderização**: Desenha os objetos usando informações de iluminação do caminho dos fótons. Na primeira etapa, um grande número de fótons é gerado aleatoriamente a partir de cada fonte de luz e é propagado na cena 3D. À medida que cada fóton atinge uma superfície, ele é representado por três quantidades: * **Localização**: Uma posição no espaço onde o fóton pousa em uma superfície. * **Potência**: A cor e o brilho do fóton. * **Direção incidente**: A direção de onde o fóton chegou à superfície. Quando um fóton pousa, ele pode permanecer nesta superfície ou (com alguma probabilidade) pode ser refletido para outra superfície. Tal reflexão depende das propriedades da superfície incidente. Por exemplo, superfícies brilhantes geram muita reflexão, enquanto superfícies escuras não. Um fóton atingindo uma superfície colorida tem maior probabilidade de refletir a cor presente na superfície. Quando o fóton é refletido, sua direção de reflexão depende das propriedades da superfície. Por exemplo, refletores difusos espalham fótons uniformemente em todas as direções, enquanto refletores especulares refletem fótons quase ao longo da direção de reflexão perfeita. Depois que todos os fótons foram rastreados, a fase de renderização começa. Para renderizar um ponto de alguma superfície, verificamos quantos fótons pousaram perto desse ponto da superfície. Ao integrar a contribuição total desses fótons e considerar as propriedades da superfície (como cor e propriedades reflexivas), determinamos a intensidade resultante nessa pequena região. Para que isso funcione, o número de fótons disparados na cena deve ser grande o suficiente para que todos os pontos recebam um número respeitável de fótons. Por não ser um método de iluminação local, o mapeamento de fótons leva mais tempo do que o ray tracing simples usando o modelo de Phong, mas os resultados produzidos pelo mapeamento de fótons podem ser incrivelmente realistas, em comparação com o ray tracing simples, como ilustrado na figura abaixo. .. Informações Adicionais ---------------------- Os demais tópicos desta aula são opcionais e fornecidos para seu próprio interesse, caso você queira se aprofundar no tópico de ray tracing e até mesmo implementar seu próprio ray tracer. 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 :math:`P`. * **Fontes de luz**: Vamos supor que temos uma coleção de fontes de luz :math:`L_1, L_2, \ldots`. Cada fonte está associada a uma cor RGB (um vetor de intensidades, com valores não negativos). Seja :math:`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 :math:`i` estiver visível no ponto :math:`P` e 0 caso contrário. Se não houver objetos transparentes, isso pode ser calculado simplesmente disparando um raio de :math:`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 :math:`\rho_t`. * **Cor do material**: Assumimos que a cor do material de um objeto é dada por :math:`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 :math:`\rho_a`, :math:`\rho_d` e :math:`\rho_s` os coeficientes de iluminação ambiente, de difusão e especular, respectivamente. Esses coeficientes estão tipicamente no intervalo [0, 1]. Seja :math:`\alpha` o coeficiente de brilho especular. * **Vetores**: Sejam :math:`\vec{n}`, :math:`\vec{h}` e :math:`\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 :math:`a`, :math:`b` e :math:`c`, como no modelo de Phong. Seja :math:`d_i` a distância do ponto de contato :math:`P` até a :math:`i`-ésima fonte de luz. * **Reflexão** e **refração**: Sejam :math:`\rho_r` e :math:`\rho_t` os coeficientes de iluminação de reflexão e de transmissão (refratados). Se :math:`\rho_t \ne 0` então sejam :math:`\eta_i` e :math:`\eta_t` os índices de refração, e sejam :math:`\vec{r}_v` e :math:`\vec{t}` os vetores de reflexão e transmissão normalizados. Seja o par :math:`(P, \vec{u})` um raio originado no ponto :math:`P` e indo na direção :math:`\vec{u}`. A equação completa de reflexão para ray tracing é: :math:`\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 :math:`i`-ésima fonte de luz é visível de :math:`P`. Observe que se :math:`\rho_r` ou :math:`\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 :math:`r` é pequeno em relação a :math:`|\vec{w}|` (o que acontece quando a esfera está distante), então podemos perder o efeito de :math:`r` no cálculo de :math:`\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. 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. Para saber mais --------------- * Capítulo 13 do livro "Interactive Computer Graphics, 8th edition" de Angel e Shreiner. * `Aula 20 das notas de aula do Prof. Dave Mount `__. * `Ray tracing na Wikipedia `__.