Implementação de um Algoritmo Completo de Path Tracing

O desenvolvimento de um algoritmo completo de Path Tracing é uma etapa fundamental na computação gráfica. A tarefa central envolve a modificação de funções chave para simular o comportamento físico da luz. O ponto de partida principal para este trabalho é a função castRay(const Ray ray, int depth) em Scene.cpp, onde a lógica central do Path Tracing será implementada.

Implementação da Função castRay

Esta função é responsável por traçar um raio pela cena e calcular a cor final do ponto de impacto, levando em consideração a iluminação direta e indireta. Ela recebe um raio e sua profundidade atual, e devolve a cotnribuição de cor resultante.


// Implementação do Path Tracing
Vector3f Scene::castRay(const Ray &raio, int profundidade) const
{
   // TO DO Implementar o Algoritmo de Path Tracing aqui
   // Esta função envia um raio e sua profundidade atual para calcular a cor final do ponto de sombreamento.

   // Funções auxiliares utilizadas:
   // Intersection intersect(const Ray& raio) const;
   // Retorna a interseção entre o raio e a estrutura BVH da cena.
   // void sampleLight(Intersection &posicao, float &pdf);
   // Amostra um ponto uniformemente em todas as fontes de luz e atualiza o pdf para a densidade de probabilidade da amostragem.
   // Vector3f Material::sample(const Vector3f &wi, const Vector3f &N);
   // Amostra uma direção de saída com base nas propriedades do material, na direção incidente wi e no vetor normal N.
   // float Material::pdf(V3f wi, V3f &wo, V3f N);
   // Dado um raio incidente wi, um raio de saída wo e a normal N, calcula a densidade de probabilidade de amostragem.
   // Vector3f Material::eval(V3f wi, V3f &wo, V3f N);
   // Calcula o valor da função de distribuição de refletância bidirecional (BRDF), f_r.
   // Scene.RussianRoulette
   // Probabilidade de Continuação (PRR).

   Intersection intersecao = intersect(raio); // Obtém a interseção inicial

   if (intersecao.happened) {
       Vector3f iluminacaoDireta(0.0);
       Vector3f iluminacaoIndireta(0.0);

       Vector3f pontoImpacto = intersecao.coords;
       Vector3f direcaoSaida = raio.direction; // Direção do observador para o ponto de impacto
       Vector3f normalSuperficie = intersecao.normal; // Normal no ponto de interseção
       Material *materialSuperficie = intersecao.m;

       // 1. Cálculo da Iluminação Direta
       // Amostra uma fonte de luz
       Intersection intersecaoLuz;
       float pdfLuz = 0.0;
       sampleLight(intersecaoLuz, pdfLuz);
       Vector3f pontoLuz = intersecaoLuz.coords;

       // Cria um raio do ponto de impacto para o ponto amostrado na luz
       Vector3f direcaoLuz = (pontoLuz - pontoImpacto).normalized();
       Ray raioPX(pontoImpacto, direcaoLuz); // Adiciona um pequeno offset para evitar auto-interseção
       Intersection intersecaoPX = intersect(raioPX);

       // Se o raio atingir a fonte de luz e não houver obstrução, calcula a contribuição direta
       if (intersecaoPX.happened && intersecaoPX.m->hasEmission()) {
           Vector3f normalLuz = intersecaoPX.normal;
           // Cálculo da equação de renderização para iluminação direta
           iluminacaoDireta = intersecaoLuz.emit * materialSuperficie->eval(direcaoSaida, direcaoLuz, normalSuperficie)
                            * dotProduct(direcaoLuz, normalSuperficie) * dotProduct(-direcaoLuz, normalLuz)
                            / (intersecaoPX.distance * intersecaoPX.distance) / pdfLuz;
       }

       // 2. Cálculo da Iluminação Indireta (via Russian Roulette)
       if (get_random_float() <= RussianRoulette) {
           // Amostra uma direção de saída para a luz indireta
           Vector3f direcaoEntrada = materialSuperficie->sample(direcaoSaida, normalSuperficie).normalized();
           Ray raioPQ(pontoImpacto, direcaoEntrada);
           Intersection intersecaoQ = intersect(raioPQ);

           // Se o raio atingir um objeto não emissivo, calcula a contribuição indireta recursivamente
           if (intersecaoQ.happened && !intersecaoQ.m->hasEmission()) {
               iluminacaoIndireta = castRay(raioPQ, profundidade + 1) * materialSuperficie->eval(direcaoSaida, direcaoEntrada, normalSuperficie)
                                  * dotProduct(direcaoEntrada, normalSuperficie) / materialSuperficie->pdf(direcaoSaida, direcaoEntrada, normalSuperficie)
                                  / RussianRoulette;
           }
       }
       return materialSuperficie->getEmission() + iluminacaoDireta + iluminacaoIndireta;
   }

   // Se não houver interseção, o raio vai para o "infinito" e retorna preto
   return Vector3f(0.0);
}
   

Amostragem de Luz: Scene::sampleLight

A amostragem de luz na cena é crucial para a iluminação direta. Esta função seleciona um ponto aleatório em uma das fontes de luz da cena, ponderando pela área de cada uma.


void Scene::sampleLight(Intersection &posicaoAmostrada, float &densidadeProbabilidade) const
{
   float somaAreaEmissiva = 0;
   for (uint32_t indice = 0; indice < objetosCena.size(); ++indice) {
       if (objetosCena[indice]->hasEmit()){
           somaAreaEmissiva += objetosCena[indice]->getArea();
       }
   }
   float valorAleatorio = get_random_float() * somaAreaEmissiva; // Escolhe um valor aleatório dentro da soma total das áreas
   somaAreaEmissiva = 0;
   for (uint32_t indice = 0; indice < objetosCena.size(); ++indice) {
       if (objetosCena[indice]->hasEmit()){
           somaAreaEmissiva += objetosCena[indice]->getArea();
           // Se o valor aleatório for menor ou igual à soma acumulada, amostra este objeto emissivo
           if (valorAleatorio <= somaAreaEmissiva){
               objetosCena[indice]->Sample(posicaoAmostrada, densidadeProbabilidade);
               break;
           }
       }
   }
}
   

Teste de Interseção de AABB: Bounds3::IntersectP

Para otimização da travessia de estruturas de dados espaciais como BVHs, é fundamental ter uma maneira rápida de testar a interseção antre um raio e uma caixa delimitadora alinhada aos eixos (AABB). Esta função implementa o método do "slab" para isso.


inline bool Bounds3::IntersectP(const Ray& raio, const Vector3f& direcaoInvertida,
                               const std::array<int, 3>& componentesNegativas) const
{
   // direcaoInvertida: Recíproco da direção do raio (1.0/x, 1.0/y, 1.0/z) para otimização (multiplicação é mais rápida que divisão).
   // componentesNegativas: Indica se cada componente da direção do raio é negativa (int(x>0), int(y>0), int(z>0)), para simplificar a lógica.

   // Calcula os parâmetros 't' para os planos de entrada e saída em cada eixo.
   Vector3f t_entrada = (pMin - raio.origin) * direcaoInvertida;
   Vector3f t_saida   = (pMax - raio.origin) * direcaoInvertida;

   // Se uma componente da direção do raio for negativa, os planos de entrada e saída são invertidos.
   if(componentesNegativas[0]) std::swap(t_entrada.x, t_saida.x);
   if(componentesNegativas[1]) std::swap(t_entrada.y, t_saida.y);
   if(componentesNegativas[2]) std::swap(t_entrada.z, t_saida.z);

   // Encontra o 't' de entrada máximo e o 't' de saída mínimo entre os eixos.
   float t_max_entrada = std::max(std::max(t_entrada.x, t_entrada.y), t_entrada.z);
   float t_min_saida   = std::min(std::min(t_saida.x, t_saida.y), t_saida.z);

   // Retorna true se houver interseção válida (entrada antes da saída e caixa não atrás do raio).
   return t_max_entrada <= t_min_saida && t_min_saida >= 0;
}
   

Travessia da BVH: BVHAccel::getIntersection

A estrutura BVH (Bounding Volume Hierarchy) acelera significtaivamente o processo de busca por interseções. Esta função recursiva atravessa a BVH para encontrar a interseção mais próxima de um raio com os objetos da cena.


Intersection BVHAccel::getIntersection(BVHBuildNode* noAtual, const Ray& raio) const
{
   Intersection resultado;
   // TODO Percorrer a BVH para encontrar a interseção

   // Testa se o raio intersecta a AABB do nó atual. Se não, retorna uma interseção inválida.
   if (!noAtual->bounds.IntersectP(raio, raio.direction_inv, std::array<int, 3>
   {raio.direction.x > 0, raio.direction.y > 0, raio.direction.z > 0})) {
       return resultado;
   }

   // Se o nó atual é um nó folha (contém um objeto), tenta intersectar diretamente com o objeto.
   if (noAtual->object) {
       return noAtual->object->getIntersection(raio);
   }

   // Se o nó não é folha e o raio intersecta sua AABB, recursivamente verifica os nós filhos.
   Intersection intersecaoEsquerda = getIntersection(noAtual->left, raio);
   Intersection intersecaoDireita = getIntersection(noAtual->right, raio);

   // Retorna a interseção mais próxima (menor distância).
   return intersecaoEsquerda.distance <= intersecaoDireita.distance ? intersecaoEsquerda : intersecaoDireita;
}
   

Propriedade de Emissão de Material: Material::hasEmission

Esta função simples verifica se um material é emissivo, ou seja, se ele próprio emite luz. É essencial para identificar fontes de luz na cena.


bool Material::hasEmission() {
   // Um material é considerado emissivo se o vetor de emissão (m_emission)
   // tiver uma magnitude maior que um pequeno valor (EPSILON).
   if (m_emission.norm() > EPSILON) return true;
   else return false;
}
   

Interseção Raio-Triângulo: Triangle::getIntersection

Esta função implementa o algoritmo de Moller-Trumbore para encontrar a interseção entre um raio e um triângulo. É otimizado para ser rápido e robusto.


inline Intersection Triangle::getIntersection(Ray raio)
{
   Intersection intersecaoTriangular;

   // Back-face culling: se o raio aponta na mesma direção da normal, significa que está vindo de "trás" do triângulo.
   if (dotProduct(raio.direction, normal) > 0)
       return intersecaoTriangular; // Não há interseção válida.

   double coordU, coordV, distanciaT = 0;
   Vector3f vetorP = crossProduct(raio.direction, e2); // e2 é a segunda aresta do triângulo (v2 - v0)
   double determinante = dotProduct(e1, vetorP);       // e1 é a primeira aresta (v1 - v0)
   
   // Se o determinante é próximo de zero, o raio é paralelo ao plano do triângulo ou degenerado.
   if (fabs(determinante) < EPSILON)
       return intersecaoTriangular;

   double invDeterminante = 1. / determinante;
   Vector3f vetorT = raio.origin - v0; // Vetor do vértice v0 do triângulo até a origem do raio.

   coordU = dotProduct(vetorT, vetorP) * invDeterminante;
   // Testa se a coordenada 'u' está dentro dos limites do triângulo.
   if (coordU < 0 || coordU > 1)
       return intersecaoTriangular;

   Vector3f vetorQ = crossProduct(vetorT, e1);
   coordV = dotProduct(raio.direction, vetorQ) * invDeterminante;
   // Testa se a coordenada 'v' está dentro dos limites do triângulo.
   if (coordV < 0 || coordU + coordV > 1)
       return intersecaoTriangular;

   distanciaT = dotProduct(e2, vetorQ) * invDeterminante;

   // TODO encontrar interseção raio-triângulo
   // Se a interseção for válida (distância positiva), preenche os dados da interseção.
   if (distanciaT < 0) return intersecaoTriangular;

   intersecaoTriangular.happened = true;
   intersecaoTriangular.coords = raio(distanciaT);  // Calcula as coordenadas do ponto de interseção
   intersecaoTriangular.distance = distanciaT;      // Distância linear
   intersecaoTriangular.m = m;                      // Material do triângulo
   intersecaoTriangular.obj = this;                 // Ponteiro para o objeto triângulo
   intersecaoTriangular.normal = normal;            // Normal do triângulo no ponto de interseção

   return intersecaoTriangular;
}
   

Renderização Multi-threaded: Renderer::Render

Para acelerar o processo de renderização, a computação de múltiplos raios pode ser paralelizada usando OpenMP. A diretiva #pragma omp parallel for permite que as iterações de um loop sejam distribuídas entre múltiplos threads.


// A função principal de renderização. Aqui iteramos sobre todos os pixels da imagem,
// geramos raios primários e os lançamos na cena. O conteúdo do framebuffer é salvo em um arquivo.
void Renderer::Render(const Scene& cena)
{
   std::vector<Vector3f> bufferDeQuadros(cena.width * cena.height);

   float escala = tan(deg2rad(cena.fov * 0.5));
   float razaoAspecto = cena.width / (float)cena.height;
   Vector3f posicaoCamera(278, 273, -800);
   int indicePixel = 0;

   // Altere o valor de spp para mudar a quantidade de amostras por pixel
   int amostrasPorPixel = 4;
   std::cout << "SPP: " << amostrasPorPixel << "\n";

   // Loop principal para cada pixel da imagem
   for (uint32_t linha = 0; linha < cena.height; ++linha) {
       for (uint32_t coluna = 0; coluna < cena.width; ++coluna) {
           // Gera a direção do raio primário para o pixel atual
           float coordX = (2 * (coluna + 0.5) / (float)cena.width - 1) *
                          razaoAspecto * escala;
           float coordY = (1 - 2 * (linha + 0.5) / (float)cena.height) * escala;

           Vector3f direcaoRaioPrimario = normalize(Vector3f(-coordX, coordY, 1));

           // Paraleliza o loop de amostras por pixel usando OpenMP
           // Cada pixel tem suas 'amostrasPorPixel' calculadas em paralelo.
           #pragma omp parallel for
           for (int amostra = 0; amostra < amostrasPorPixel; amostra++){
               bufferDeQuadros[indicePixel] += cena.castRay(Ray(posicaoCamera, direcaoRaioPrimario), 0) / amostrasPorPixel;
           }
           indicePixel++;
       }
       UpdateProgress(linha / (float)cena.height);
   }
   UpdateProgress(1.f);

   // Salva o framebuffer para um arquivo .ppm
   FILE* arquivoPPM = fopen("output.ppm", "wb");
   (void)fprintf(arquivoPPM, "P6\n%d %d\n255\n", cena.width, cena.height);
   for (auto i = 0; i < cena.height * cena.width; ++i) {
       static unsigned char cor[3];
       cor[0] = (unsigned char)(255 * std::pow(clamp(0, 1, bufferDeQuadros[i].x), 0.6f));
       cor[1] = (unsigned char)(255 * std::pow(clamp(0, 1, bufferDeQuadros[i].y), 0.6f));
       cor[2] = (unsigned char)(255 * std::pow(clamp(0, 1, bufferDeQuadros[i].z), 0.6f));
       fwrite(cor, 1, 3, arquivoPPM);
   }
   fclose(arquivoPPM);
}
   

Tags: PathTracing RayTracing BVH OpenMP ComputerGraphics

Publicado em 7-19 06:41