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);
}