Rastreamento de caminho progressivo com amostragem de luz explícita


14

Eu entendi a lógica por trás da amostragem de importância para a parte BRDF. No entanto, quando se trata de amostrar fontes de luz explicitamente, tudo se torna confuso. Por exemplo, se eu tiver uma fonte de luz pontual em minha cena e se eu a amostrar diretamente em cada quadro constantemente, devo contar como mais uma amostra para a integração monte carlo? Ou seja, colho uma amostra da distribuição ponderada em cosseno e outra da luz pontual. São duas amostras no total ou apenas uma? Além disso, devo dividir o brilho proveniente da amostra direta para qualquer termo?

Respostas:


19

Existem várias áreas no rastreamento de caminho que podem ser amostradas por importância. Além disso, cada uma dessas áreas também pode usar Amostragem por Importância Múltipla, proposta pela primeira vez no artigo de Veach e Guibas, de 1995 . Para explicar melhor, vejamos um rastreador de caminho para trá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);
}

Em inglês:

  1. Faça um raio através da cena
  2. Verifique se atingimos alguma coisa. Caso contrário, retornamos a cor do skybox e quebramos.
  3. Verifique se acendemos uma luz. Nesse caso, adicionamos a emissão de luz ao nosso acúmulo de cores
  4. Escolha uma nova direção para o próximo raio. Podemos fazer isso de maneira uniforme, ou amostra de importância com base no BRDF
  5. Avalie o BRDF e acumule-o. Aqui temos que dividir pelo pdf de nossa direção escolhida, a fim de seguir o algoritmo de Monte Carlo.
  6. Crie um novo raio com base na direção escolhida e de onde viemos
  7. [Opcional] Use a roleta russa para escolher se devemos terminar o raio
  8. Goto 1

Com esse código, só obtemos cores se o raio eventualmente atingir uma luz. Além disso, ele não suporta fontes de luz pontuais, pois elas não têm área.

Para consertar isso, amostramos as luzes diretamente a cada salto. Temos que fazer algumas pequenas mudanças:

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

Primeiro, adicionamos "color + = throughput * SampleLights (...)". Entrarei em detalhes sobre o SampleLights () daqui a pouco. Mas, essencialmente, percorre todas as luzes e retorna sua contribuição para a cor, atenuada pelo BSDF.

Isso é ótimo, mas precisamos fazer mais uma alteração para corrigi-la; especificamente, o que acontece quando atingimos uma luz. No código antigo, adicionamos a emissão de luz ao acúmulo de cores. Mas agora amostramos diretamente a luz a cada salto, portanto, se adicionarmos a emissão da luz, "mergulharemos duas vezes". Portanto, a coisa certa a fazer é ... nada; pulamos o acúmulo de emissão de luz.

No entanto, existem dois casos de canto:

  1. O primeiro raio
  2. Saltos perfeitamente especulares (aka espelhos)

Se o primeiro raio atingir a luz, você deverá ver diretamente a emissão da luz. Portanto, se ignorarmos, todas as luzes aparecerão em preto, mesmo que as superfícies ao redor estejam acesas.

Quando você atinge superfícies perfeitamente especulares, não é possível amostrar diretamente uma luz, porque um raio de entrada possui apenas uma saída. Bem, tecnicamente, poderíamos verificar se o raio de entrada atingirá uma luz, mas não faz sentido; o loop principal do Path Tracing fará isso de qualquer maneira. Portanto, se atingirmos uma luz logo após atingirmos uma superfície especular, precisamos acumular a cor. Caso contrário, as luzes serão negras nos espelhos.

Agora, vamos nos aprofundar em 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;
}

Em inglês:

  1. Passe por todas as luzes
  2. Ignore a luz se a atingirmos
    • Não mergulhe duas vezes
  3. Acumule a iluminação direta de todas as luzes
  4. Retornar a iluminação direta

BSDF(p,ωEu,ωo)euEu(p,ωEu)

Para fontes de luz pontuais, isso é simples 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;
}

No entanto, se queremos que as luzes tenham área, primeiro precisamos provar um ponto na luz. Portanto, a definição completa é:

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

Nós podemos implementar light-> SampleLi como quisermos; nós podemos escolher o ponto uniformemente, ou amostra de importância. Em ambos os casos, dividimos a radiosidade pelo pdf da escolha do ponto. Mais uma vez, para satisfazer os requisitos de Monte Carlo.

Se o BRDF for altamente dependente da visualização, pode ser melhor escolher um ponto com base no BRDF, em vez de um ponto aleatório na luz. Mas como escolhemos? Amostra com base na luz ou com base no BRDF?

BSDF(p,ωEu,ωo)euEu(p,ωEu)

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

Em inglês:

  1. Primeiro, nós provamos a luz
    • Isso atualiza a interação.InputDirection
    • Nos dá o Li para a luz
    • E o pdf de escolher esse ponto na luz
  2. Verifique se o pdf é válido e o brilho é diferente de zero
  3. Avalie o BSDF usando o InputDirection de amostra
  4. Calcular o pdf para o BSDF, considerando o InputDirection da amostra
    • Essencialmente, qual é a probabilidade dessa amostra, se formos amostrar usando o BSDF, em vez da luz
  5. Calcular o peso, usando o pdf leve e o BSDF pdf
    • Veach e Guibas definem algumas maneiras diferentes de calcular o peso. Experimentalmente, eles encontraram a heurística de poder com um poder de 2 para funcionar melhor na maioria dos casos. Refiro-lhe o artigo para mais detalhes. A implementação está abaixo
  6. Multiplique o peso com o cálculo da iluminação direta e divida pela luz pdf. (Para Monte Carlo) E adicione à acumulação direta de luz.
  7. Em seguida, experimentamos o BRDF
    • Isso atualiza a interação.InputDirection
  8. Avalie o BRDF
  9. Obtenha o pdf para escolher essa direção com base no BRDF
  10. Calcular o pdf leve, de acordo com o InputDirection da amostra
    • Este é o espelho de antes. Qual a probabilidade dessa direção, se amostrássemos a luz
  11. Se lightPdf == 0.0f, o raio perdeu a luz, então retorne a iluminação direta da amostra de luz.
  12. Caso contrário, calcule o peso e adicione a iluminação direta do BSDF à acumulação
  13. Por fim, retorne a iluminação direta 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);
}

Há várias otimizações / aprimoramentos que você pode fazer nessas funções, mas eu as reduzi para tentar facilitar a compreensão. Se você quiser, posso compartilhar algumas dessas melhorias.

Apenas amostragem de uma luz

Em SampleLights (), percorremos todas as luzes e obtemos sua contribuição. Para um pequeno número de luzes, isso é bom, mas para centenas ou milhares de luzes, isso fica caro. Felizmente, podemos explorar o fato de que a Integração Monte Carlo é uma média gigante. Exemplo:

Vamos definir

h(x)=f(x)+g(x)

h(x)

h(x)=1NEu=1Nf(xEu)+g(xEu)

f(x)g(x)

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

ζr(ζ,x)

r(ζ,x)={f(x),0,0ζ<0,5g(x),0,5ζ<1.0

pdf=12

Em inglês:

  1. f(x)g(x)
  2. 12
  3. Média

À medida que N aumenta, a estimativa converge para a solução correta.

Podemos aplicar esse mesmo princípio à amostragem de luz. Em vez de amostrar todas as luzes, escolhemos aleatoriamente uma e multiplicamos o resultado pelo número de luzes (é o mesmo que dividir pelo pdf fracionário):

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

Amostragem de Importância Múltipla na Direção "New Ray"

A importância atual do código apenas mostra a direção "New Ray" com base no BSDF. E se quisermos também amostra de importância com base na localização das luzes?

Partindo do que aprendemos acima, um método seria disparar dois "novos" raios e pesar cada um com base em seus PDFs. No entanto, isso é computacionalmente caro e difícil de implementar sem recursão.

Para superar isso, podemos aplicar os mesmos princípios que aprendemos amostrando apenas uma luz. Ou seja, escolha aleatoriamente um para provar e divida pelo pdf de sua escolha.

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

Isso tudo dito, nós realmente queremos para amostra importância a direção "New Ray" com base na luz? Para iluminação direta , a radiosidade é afetada pelo BSDF da superfície e pela direção da luz. Mas para iluminação indireta , a radiosidade é quase exclusivamente definida pelo BSDF da superfície atingida anteriormente. Portanto, adicionar amostras de importância leve não nos dá nada.

Portanto, é comum apenas amostrar por importância a "Nova Direção" com o BSDF, mas aplicar Amostragem de Importância Múltipla à iluminação direta.


Obrigado pela resposta esclarecedora! Entendo que se usássemos um rastreador de caminho sem amostragem explícita de luz, nunca atingiríamos uma fonte de luz pontual. Então, podemos basicamente adicionar sua contribuição. Por outro lado, se provar uma fonte de luz de área, nós temos que ter certeza de que não deve bater-lo novamente com a iluminação indireta, a fim de evitar a dupla dip
Mustafa Işık

Exatamente! Há alguma parte sobre a qual você precise de esclarecimentos? Ou não há detalhes suficientes?
RichieSams

Além disso, a amostragem de importância múltipla é usada apenas para o cálculo da iluminação direta? Talvez eu tenha perdido, mas não vi outro exemplo disso. Se eu fotografar apenas um raio por salto no traçador, parece que não posso fazê-lo para o cálculo da iluminação indireta.
Mustafa Işık

2
A Amostragem de Importância Múltipla pode ser aplicada em qualquer lugar em que você use a amostragem de importância. O poder da amostragem de importância múltipla é que podemos combinar os benefícios de várias técnicas de amostragem. Por exemplo, em alguns casos, a amostragem de importância leve será melhor que a amostragem por BSDF. Em outros casos, vice-versa. O MIS combinará o melhor dos dois mundos. No entanto, se a amostragem BSDF for melhor 100% do tempo, não há razão para adicionar a complexidade do MIS. Eu adicionei algumas seções para a resposta para expandir sobre este ponto
RichieSams

1
Parece que separamos as fontes de radiação recebidas em duas partes como direta e indireta. Amostramos luzes explicitamente para a parte direta e, enquanto amostramos essa parte, é razoável a importância de amostrar as luzes e os BSDFs. Para a parte indireta, no entanto, não temos idéia sobre qual direção pode potencialmente nos fornecer valores de radiância mais altos, já que é o próprio problema que queremos resolver. No entanto, podemos dizer qual direção pode contribuir mais de acordo com o termo cosseno e o BSDF. Isto é o que eu entendo. Corrija-me se eu estiver errado e obrigado por sua resposta incrível.
Mustafa Işık
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.