¿La ruleta rusa es realmente la respuesta?

21

He visto que en algunas implementaciones de Path Tracing, se utiliza un enfoque llamado Russian Roulette para eliminar algunas de las rutas y compartir su contribución entre las otras rutas.

Entiendo que, en lugar de seguir un camino hasta que cae por debajo de un cierto valor umbral de contribución, y luego abandonarlo, se usa un umbral diferente y los caminos cuya contribución está por debajo de ese umbral solo terminan con una pequeña probabilidad. Las otras rutas tienen su contribución aumentada en una cantidad correspondiente a compartir la energía perdida de la ruta terminada. No me queda claro si esto es para corregir un sesgo introducido por la técnica, o si toda la técnica es necesaria para evitar el sesgo.

  • ¿La ruleta rusa da un resultado imparcial?
  • ¿Es necesaria la ruleta rusa para obtener un resultado imparcial?

Es decir, ¿usar un umbral pequeño y simplemente terminar una ruta en el momento en que cae por debajo de ese umbral daría un resultado más sesgado o menos sesgado?

Dado un número arbitrariamente grande de muestras, ¿ambos enfoques convergerían en una imagen resultante imparcial?

Estoy buscando entender la razón subyacente para usar el enfoque de la ruleta rusa. ¿Hay una diferencia significativa en velocidad o calidad?


Entiendo que la energía se redistribuye entre otros rayos para preservar la energía total. Sin embargo, ¿podría esta redistribución no hacerse aún si el rayo terminara al caer por debajo de un umbral fijo, en lugar de tener una vida útil determinada al azar después de alcanzar ese umbral?

Por el contrario, 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?

trichoplax
fuente

Respuestas:

26

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. 2 rebotes de Disney's Big Hero 6 9 rebotes de Disney's Big Hero 6

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:

  1. ¿La ruleta rusa da un resultado imparcial?
  2. ¿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.
  3. ¿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.
  4. 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.

RichieSams
fuente
Sinceramente, no estoy completamente seguro 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.
v.oddou
9

La técnica de la ruleta rusa en sí misma es una forma de terminar caminos sin introducir sesgos sistémicos. El principio es bastante sencillo: si en un vértice particular tiene un 10% de posibilidades de reemplazar arbitrariamente la energía con 0, y si lo hace un número infinito de veces, verá un 10% menos de energía. El aumento de energía solo compensa eso. Si no compensó la energía perdida debido a la terminación del camino, la ruleta rusa estaría sesgada, pero toda la técnica es un método útil para evitar el sesgo.

Si fuera un adversario que buscara demostrar que la técnica de "terminar caminos cuya contribución es menor que un valor fijo pequeño" está sesgada, construiría una escena con luces tan tenues que los caminos contribuyentes siempre son menores que ese valor. Quizás estoy simulando una cámara con poca luz.

Pero, por supuesto, siempre puede exponer el valor fijo como un parámetro modificable para el usuario, por lo que puede soltarlo aún más si su escena es de poca luz. Así que ignoremos ese ejemplo por un minuto.

¿Qué sucede si considero un objeto que está iluminado por muchos caminos de muy baja energía que son recogidos por un reflector parabólico ? Los caminos de baja energía no necesariamente rebotan indiscriminadamente de una manera que puedes descuidar por completo. Del mismo modo, el razonamiento se aplica, por ejemplo, a la eliminación de rutas después de un número fijo de rebotes: puede construir una escena con una ruta que rebota en una serie de 20 espejos antes de golpear un objeto.

Otra forma de verlo: si establece la contribución de una ruta a 0 después de que cae por debajo de un épsilon fijo, ¿cómo corrige esa pérdida de energía? No estás simplemente reduciendo la energía total en alguna fracción. No sabe nada acerca de cuánta energía está descuidando, porque está cortando en algún umbral de contribución antes de conocer el otro factor: la energía incidente.

John Calsbeek
fuente
8

Solo para ampliar algunas de las otras respuestas, la prueba de que la ruleta rusa no da un resultado sesgado es muy simple.

F

F=F1++Fnorte

Reemplace cada término con:

Fyo={1pagsyoFyocon probabilidad pagsyo0 0de otra manera

Luego:

mi[Fyo]=pagsyo×1pagsyomi[Fyo]+(1-pagsyo)×0 0=mi[Fyo]

pagsyoF

Seudónimo
fuente