Para entender la ruleta rusa, echemos un vistazo a un trazador de ruta hacia atrás muy básico:
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);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// 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 emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
ES DECIR. saltamos alrededor de la escena, acumulando color y atenuación de luz a medida que avanzamos. Para ser completamente imparcial matemáticamente, los rebotes deben ir al infinito. Pero esto no es realista y, como notó, no es visualmente necesario; Para la mayoría de las escenas, después de un cierto número de rebotes, digamos 10, la cantidad de contribución al color final es muy, muy mínima.
Entonces, para ahorrar recursos informáticos, muchos trazadores de ruta tienen un límite estricto para la cantidad de rebotes. Esto agrega sesgo.
Dicho esto, es difícil elegir cuál debería ser ese límite difícil. Algunas escenas se ven geniales después de 2 rebotes; otros (digamos con transmisión o SSS) pueden tomar hasta 10 o 20.
Si elegimos demasiado bajo, la imagen estará visiblemente sesgada. Pero si elegimos demasiado alto, estamos desperdiciando energía y tiempo de cálculo.
Como notó, una forma de resolver esto es terminar la ruta después de que alcancemos un umbral de atenuación. Esto también agrega sesgo.
Sujetar después de un umbral funcionará , pero de nuevo, ¿cómo elegimos el umbral? Si elegimos demasiado grande, la imagen estará visiblemente sesgada, demasiado pequeña, y estamos desperdiciando recursos.
La ruleta rusa intenta resolver estos problemas de manera imparcial. Primero, aquí está el código:
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);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// 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 emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Russian Roulette
// Randomly terminate a path with a probability inversely equal to the throughput
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
// Add the energy we 'lose' by randomly terminating paths
throughput *= 1 / p;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
La ruleta rusa termina aleatoriamente un camino con una probabilidad inversamente igual al rendimiento. Por lo tanto, es más probable que se terminen las rutas con bajo rendimiento que no contribuirán mucho a la escena.
Si nos detenemos allí, todavía estamos sesgados. 'Perdemos' la energía del camino que terminamos al azar. Para hacerlo imparcial, aumentamos la energía de los caminos no terminados por su probabilidad de terminar. Esto, junto con ser aleatorio, hace que la ruleta rusa sea imparcial.
Para responder a sus últimas preguntas:
- ¿La ruleta rusa da un resultado imparcial?
- ¿Es necesaria la ruleta rusa para obtener un resultado imparcial?
- Depende de lo que quieras decir con imparcial. Si quieres decir matemáticamente, entonces sí. Sin embargo, si te refieres a lo visual, entonces no. Solo tiene que elegir la profundidad máxima de la ruta y el umbral de corte con mucho cuidado. Esto puede ser muy tedioso ya que puede cambiar de una escena a otra.
- ¿Puede usar una probabilidad fija (corte) y luego redistribuir la energía 'perdida'? ¿Es esto imparcial?
- Si usa una probabilidad fija, está agregando sesgo. Al redistribuir la energía 'perdida', se reduce el sesgo, pero todavía está sesgado matemáticamente. Para ser completamente imparcial, debe ser aleatorio.
- Si la energía que se perdería al terminar un rayo sin redistribuir su energía finalmente se pierde de todos modos (ya que los rayos a los que se redistribuye también terminan finalmente), ¿cómo mejora esto la situación?
- La ruleta rusa solo detiene el rebote. No elimina la muestra por completo. Además, la energía "perdida" se tiene en cuenta en los rebotes hasta la terminación. Entonces, la única forma de que la energía se 'pierda eventualmente de todos modos' sería tener una habitación completamente negra.
Al final, la ruleta rusa es un algoritmo muy simple que utiliza una cantidad muy pequeña de recursos computacionales adicionales. A cambio, puede ahorrar una gran cantidad de recursos computacionales. Por lo tanto, realmente no puedo ver una razón para no usarlo.
to be completely unbiased it must be random
. Creo que aún puede obtener resultados matemáticos aceptables utilizando el peso fraccional de las muestras, en lugar del pase / caída binario que impone la ruleta rusa, es solo que la ruleta convergerá más rápido porque está operando un muestreo de importancia perfecta.