Trazado de ruta progresivo con muestreo de luz explícito

14

Entendí la lógica detrás del muestreo de importancia para la parte BRDF. Sin embargo, cuando se trata de tomar muestras de fuentes de luz explícitamente, todo se vuelve confuso. Por ejemplo, si tengo una fuente de luz puntual en mi escena y si la muestreo directamente en cada cuadro constantemente, ¿debería contarla como una muestra más para la integración de Monte Carlo? Es decir, tomo una muestra de la distribución ponderada por coseno y otra de la luz puntual. ¿Son dos muestras en total o solo una? Además, ¿debo dividir el resplandor proveniente de la muestra directa en algún término?

Mustafa Işık
fuente

Respuestas:

19

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:

  1. Dispara un rayo a través de la escena
  2. Comprueba si golpeamos algo. Si no, devolvemos el color del skybox y lo rompemos.
  3. Comprueba si golpeamos una luz. Si es así, agregamos la emisión de luz a nuestra acumulación de color
  4. 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
  5. Evaluar el BRDF y acumularlo. Aquí tenemos que dividir por el pdf de nuestra dirección elegida, para seguir el algoritmo de Monte Carlo.
  6. Cree un nuevo rayo basado en nuestra dirección elegida y de dónde venimos
  7. [Opcional] Utilice la ruleta rusa para elegir si debemos terminar el rayo
  8. 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:

  1. El primer rayo
  2. 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:

  1. Recorre todas las luces
  2. Salta la luz si la golpeamos
    • No doble inmersión
  3. Acumula la iluminación directa de todas las luces.
  4. Devolver la iluminación directa

BSDF(p,ωi,ωo)Li(p,ωi)

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?

BSDF(p,ωi,ωo)Li(p,ωi)

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:

  1. 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
  2. Verifique que el pdf sea válido y que el resplandor no sea cero
  3. Evalúe el BSDF usando la InputDirection muestreada
  4. 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
  5. 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
  6. 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.
  7. Luego, probamos el BRDF
    • Esto actualiza la interacción.
  8. Evaluar el BRDF
  9. Obtenga el pdf para elegir esta dirección basado en el BRDF
  10. 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?
  11. Si lightPdf == 0.0f, entonces el rayo perdió la luz, así que solo devuelva la iluminación directa de la muestra de luz.
  12. De lo contrario, calcule el peso y agregue la iluminación directa BSDF a la acumulación
  13. 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)=1Ni=1Nf(xi)+g(xi)

f(x)g(x)

h(x)=1Ni=1Nr(ζ,x)pdf

ζr(ζ,x)

r(ζ,x)={f(x),0.0ζ<0.5g(x),0.5ζ<1.0

pdf=12

En inglés:

  1. f(x)g(x)
  2. 12
  3. 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.

RichieSams
fuente
Gracias por la respuesta aclaratoria! Entiendo que si tuviéramos que usar un trazado de ruta sin un muestreo de luz explícito, nunca llegaríamos a una fuente de luz puntual. Entonces, básicamente podemos agregar su contribución. Por otro lado, si tomamos muestras de una fuente de luz de área, debemos asegurarnos de no volver a golpearla con la luz indirecta para evitar una doble inmersión
Mustafa Işık
¡Exactamente! ¿Hay alguna parte sobre la que necesite aclaración? ¿O no hay suficientes detalles?
RichieSams
Además, ¿se utiliza el muestreo de importancia múltiple solo para el cálculo de iluminación directa? Tal vez me perdí pero no vi otro ejemplo de ello. Si disparo solo un rayo por rebote en mi trazado de ruta, parece que no puedo hacerlo para el cálculo de iluminación indirecta.
Mustafa Işık
2
Muestreo de importancia múltiple se puede aplicar en cualquier lugar donde utilice el muestreo de importancia. El poder del muestreo de importancia múltiple es que podemos combinar los beneficios de las técnicas de muestreo múltiple. Por ejemplo, en algunos casos, el muestreo de importancia ligera será mejor que el muestreo BSDF. En otros casos, viceversa. MIS combinará lo mejor de ambos mundos. Sin embargo, si el muestreo BSDF será mejor el 100% del tiempo, no hay razón para agregar la complejidad de MIS.
Agregué
1
Parece que separamos las fuentes de radiación entrantes en dos partes, directa e indirecta. Tomamos muestras de luces explícitamente para la parte directa y mientras tomamos muestras de esta parte, es razonable tomar muestras de las luces así como también los BSDF. Sin embargo, para la parte indirecta, no tenemos idea de qué dirección podría darnos valores de radiancia más altos, ya que es el problema en sí lo que queremos resolver. Sin embargo, podemos decir qué dirección puede contribuir más de acuerdo con el término coseno y BSDF. Esto es lo que entiendo. Corrígeme si me equivoco y gracias por tu increíble respuesta.
Mustafa Işık