Hay múltiples áreas en el trazado de ruta que se pueden muestrear en importancia. Además, cada una de esas áreas también puede usar Muestreo de Importancia Múltiple, propuesto por primera vez en el artículo de 1995 de Veach y Guibas . Para explicar mejor, echemos un vistazo a un trazador de ruta hacia atrás:
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// Fetch the material
Material *material = m_scene->GetMaterial(ray.GeomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.GeomID);
// If we hit a light, add the emission
if (light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
En inglés:
- Dispara un rayo a través de la escena
- Comprueba si golpeamos algo. Si no, devolvemos el color del skybox y lo rompemos.
- Comprueba si golpeamos una luz. Si es así, agregamos la emisión de luz a nuestra acumulación de color
- Elija una nueva dirección para el próximo rayo. Podemos hacer esto de manera uniforme, o una muestra de importancia basada en el BRDF
- Evaluar el BRDF y acumularlo. Aquí tenemos que dividir por el pdf de nuestra dirección elegida, para seguir el algoritmo de Monte Carlo.
- Cree un nuevo rayo basado en nuestra dirección elegida y de dónde venimos
- [Opcional] Utilice la ruleta rusa para elegir si debemos terminar el rayo
- Goto 1
Con este código, solo obtenemos color si el rayo finalmente alcanza una luz. Además, no admite fuentes de luz puntuales, ya que no tienen área.
Para solucionar esto, tomamos muestras de las luces directamente en cada rebote. Tenemos que hacer algunos pequeños cambios:
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// Fetch the material
Material *material = m_scene->GetMaterial(ray.GeomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.GeomID);
// If this is the first bounce or if we just had a specular bounce,
// we need to add the emmisive light
if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Calculate the direct lighting
color += throughput * SampleLights(sampler, interaction, material->bsdf, light);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
Primero, agregamos "color + = rendimiento * SampleLights (...)". Entraré en detalles sobre SampleLights () en un momento. Pero, esencialmente, recorre todas las luces y devuelve su contribución al color, atenuado por el BSDF.
Esto es genial, pero necesitamos hacer un cambio más para que sea correcto; específicamente, qué sucede cuando golpeamos una luz. En el antiguo código, agregamos la emisión de luz a la acumulación de color. Pero ahora tomamos muestras de la luz directamente en cada rebote, por lo que si agregamos la emisión de la luz, "sumergiremos dos veces". Por lo tanto, lo correcto es ... nada; omitimos acumular la emisión de la luz.
Sin embargo, hay dos casos de esquina:
- El primer rayo
- Rebotes perfectamente especulares (también conocidos como espejos)
Si el primer rayo golpea la luz, debería ver la emisión de la luz directamente. Entonces, si lo omitimos, todas las luces se mostrarán en negro, aunque las superficies a su alrededor estén iluminadas.
Cuando golpea una superficie perfectamente especular, no puede muestrear directamente una luz, porque un rayo de entrada solo tiene una salida. Bueno, técnicamente, podríamos verificar si el rayo de entrada golpeará una luz, pero no tiene sentido; el bucle principal de Trazado de ruta lo hará de todos modos. Por lo tanto, si golpeamos una luz justo después de golpear una superficie especular, debemos acumular el color. Si no lo hacemos, las luces serán negras en los espejos.
Ahora, profundicemos en SampleLights ():
float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
float3 L(0.0f);
for (uint i = 0; i < numLights; ++i) {
Light *light = &m_scene->Lights[i];
// Don't let a light contribute light to itself
if (light == hitLight) {
continue;
}
L = L + EstimateDirect(light, sampler, interaction, bsdf);
}
return L;
}
En inglés:
- Recorre todas las luces
- Salta la luz si la golpeamos
- Acumula la iluminación directa de todas las luces.
- Devolver la iluminación directa
B SD F( p , ωyo, ωo) Lyo( p , ωyo)
Para fuentes de luz puntuales, esto es simple como:
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
return float3(0.0f);
}
interaction.InputDirection = normalize(light->Origin - interaction.Position);
return bsdf->Eval(interaction) * light->Li;
}
Sin embargo, si queremos que las luces tengan área, primero necesitamos muestrear un punto en la luz. Por lo tanto, la definición completa es:
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float pdf;
float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (pdf != 0.0f && !all(Li)) {
directLighting += bsdf->Eval(interaction) * Li / pdf;
}
}
return directLighting;
}
Podemos implementar light-> SampleLi como queramos; Podemos elegir el punto uniformemente, o la muestra de importancia. En cualquier caso, dividimos la radiosidad por el pdf de elegir el punto. Nuevamente, para satisfacer los requisitos de Monte Carlo.
Si el BRDF depende mucho de la vista, puede ser mejor elegir un punto basado en el BRDF, en lugar de un punto aleatorio en la luz. ¿Pero cómo elegimos? ¿Muestra basada en la luz, o basada en el BRDF?
B SD F( p , ωyo, ωo) Lyo( p , ωyo)
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
float3 f;
float lightPdf, scatteringPdf;
// Sample lighting with multiple importance sampling
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (lightPdf != 0.0f && !all(Li)) {
// Calculate the brdf value
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
directLighting += f * Li * weight / lightPdf;
}
}
}
// Sample brdf with multiple importance sampling
bsdf->Sample(interaction, sampler);
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
lightPdf = light->PdfLi(m_scene, interaction);
if (lightPdf == 0.0f) {
// We didn't hit anything, so ignore the brdf sample
return directLighting;
}
float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
float3 Li = light->Le();
directLighting += f * Li * weight / scatteringPdf;
}
return directLighting;
}
En inglés:
- Primero, tomamos muestras de la luz
- Esto actualiza la interacción.
- Nos da el Li para la luz
- Y el pdf de elegir ese punto en la luz
- Verifique que el pdf sea válido y que el resplandor no sea cero
- Evalúe el BSDF usando la InputDirection muestreada
- Calcule el pdf para el BSDF dada la InputDirection muestreada
- Esencialmente, qué tan probable es esta muestra, si tuviéramos que usar el BSDF, en lugar de la luz
- Calcule el peso, utilizando el pdf ligero y el pdf BSDF
- Veach y Guibas definen un par de formas diferentes de calcular el peso. Experimentalmente, encontraron que el poder heurístico con un poder de 2 funciona mejor para la mayoría de los casos. Le remito al documento para más detalles. La implementación está debajo
- Multiplique el peso con el cálculo de iluminación directa y divida por el pdf de luz. (Para Monte Carlo) Y agregue a la acumulación de luz directa.
- Luego, probamos el BRDF
- Esto actualiza la interacción.
- Evaluar el BRDF
- Obtenga el pdf para elegir esta dirección basado en el BRDF
- Calcule el pdf ligero, dada la InputDirection muestreada
- Este es el espejo de antes. ¿Qué tan probable es esta dirección si tomáramos muestras de la luz?
- Si lightPdf == 0.0f, entonces el rayo perdió la luz, así que solo devuelva la iluminación directa de la muestra de luz.
- De lo contrario, calcule el peso y agregue la iluminación directa BSDF a la acumulación
- Finalmente, devuelva la iluminación directa acumulada.
.
inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
float f = numf * fPdf;
float g = numg * gPdf;
return (f * f) / (f * f + g * g);
}
Hay una serie de optimizaciones / mejoras que puede hacer en estas funciones, pero las he reducido para intentar que sean más fáciles de comprender. Si lo desea, puedo compartir algunas de estas mejoras.
Solo muestreo de una luz
En SampleLights () recorremos todas las luces y obtenemos su contribución. Para una pequeña cantidad de luces, esto está bien, pero para cientos o miles de luces, esto se vuelve costoso. Afortunadamente, podemos explotar el hecho de que Monte Carlo Integration es un promedio gigante. Ejemplo:
Definamos
h ( x ) = f( x ) + g( x )
h ( x )
h ( x ) = 1norte∑i = 1norteF( xyo) + g( xyo)
F( x )sol( x )
h ( x ) = 1norte∑i = 1norter ( ζ, x )p dF
ζr ( ζ, x )
r ( ζ, x ) = { f( X ) ,sol( X ) ,0.0 ≤ ζ< 0.50.5 ≤ ζ< 1.0
p dF= 12
En inglés:
- F( x )sol( x )
- 12
- Promedio
A medida que N aumenta, la estimación convergerá a la solución correcta.
Podemos aplicar este mismo principio al muestreo de luz. En lugar de tomar muestras de cada luz, elegimos una al azar y multiplicamos el resultado por la cantidad de luces (esto es lo mismo que dividir por el pdf fraccionario):
float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
// Return black if there are no lights
// And don't let a light contribute light to itself
// Aka, if we hit a light
// This is the special case where there is only 1 light
if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
return float3(0.0f);
}
// Don't let a light contribute light to itself
// Choose another one
Light *light;
do {
light = m_scene->RandomOneLight(sampler);
} while (light == hitLight);
return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}
1numLights
Múltiple importancia Muestreo de la dirección del "nuevo rayo"
La importancia del código actual solo muestra la dirección del "nuevo rayo" basada en el BSDF. ¿Qué sucede si también queremos una muestra de importancia basada en la ubicación de las luces?
Tomando de lo que aprendimos anteriormente, un método sería disparar dos rayos "nuevos" y pesar cada uno basado en sus archivos PDF. Sin embargo, esto es computacionalmente costoso y difícil de implementar sin recurrencia.
Para superar esto, podemos aplicar los mismos principios que aprendimos muestreando una sola luz. Es decir, elija aleatoriamente uno para muestrear y divida por el pdf de elegirlo.
// Get the new ray direction
// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();
Light *light = m_scene->RandomLight();
if (p < 0.5f) {
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float bsdfPdf = material->bsdf->Pdf(interaction);
float lightPdf = light->PdfLi(m_scene, interaction);
float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;
} else {
// Choose the direction based on a light
float lightPdf;
light->SampleLi(sampler, m_scene, interaction, &lightPdf);
float bsdfPdf = material->bsdf->Pdf(interaction);
float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}
Dicho todo esto, ¿realmente queremos dar una muestra importante de la dirección del "Nuevo Rayo" basada en la luz? Para la iluminación directa , la radiosidad se ve afectada tanto por el BSDF de la superficie como por la dirección de la luz. Pero para la iluminación indirecta , la radiosidad se define casi exclusivamente por el BSDF de la superficie golpeada anteriormente. Por lo tanto, agregar un muestreo de importancia ligera no nos da nada.
Por lo tanto, es común que solo muestre la "Nueva Dirección" con el BSDF, pero aplique Muestreo de Importancia Múltiple a la iluminación directa.