Progresywne śledzenie ścieżki z wyraźnym próbkowaniem światła

14

Zrozumiałem logikę kryjącą się za ważnością próbkowania dla części BRDF. Jednak jeśli chodzi o jawne próbkowanie źródeł światła, wszystko staje się mylące. Na przykład, jeśli mam jedno punktowe źródło światła w mojej scenie i jeśli ciągle próbkuję bezpośrednio w każdej klatce, czy powinienem liczyć je jako jeszcze jedną próbkę do integracji z Monte Carlo? To znaczy, pobieram jedną próbkę z rozkładu ważonego cosinusiem, a drugą ze światła punktowego. Czy to łącznie dwie próbki, czy tylko jedna? Czy powinienem także podzielić promieniowanie pochodzące z bezpośredniej próbki na dowolny termin?

Mustafa Işık
źródło

Odpowiedzi:

19

W śledzeniu ścieżki istnieje wiele obszarów, które można próbkować pod kątem ważności. Ponadto każdy z tych obszarów może również wykorzystywać próbkowanie wielokrotnego znaczenia, zaproponowane po raz pierwszy w pracy Veacha i Guibasa z 1995 r . Aby lepiej to wyjaśnić, spójrzmy na znacznik ścieżki wstecznej:

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

Po angielsku:

  1. Wystrzel promień po scenie
  2. Sprawdź, czy coś trafiliśmy. Jeśli nie, zwracamy kolor skybox i psujemy się.
  3. Sprawdź, czy trafiliśmy w światło. Jeśli tak, dodajemy emisję światła do naszej akumulacji kolorów
  4. Wybierz nowy kierunek dla następnego promienia. Możemy to zrobić w sposób jednolity lub ważny na podstawie BRDF
  5. Ocenić BRDF i zgromadzić go. Tutaj musimy podzielić przez pdf naszego wybranego kierunku, aby postępować zgodnie z algorytmem Monte Carlo.
  6. Utwórz nowy promień na podstawie naszego wybranego kierunku i miejsca, z którego właśnie przybyliśmy
  7. [Opcjonalnie] Użyj rosyjskiej ruletki, aby wybrać, czy mamy zakończyć promień
  8. Idź 1

Dzięki temu kodowi uzyskujemy kolor tylko wtedy, gdy promień ostatecznie trafi na światło. Ponadto nie obsługuje punktowych źródeł światła, ponieważ nie mają one obszaru.

Aby to naprawić, próbkujemy światła bezpośrednio przy każdym odbiciu. Musimy zrobić kilka drobnych zmian:

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

Najpierw dodajemy „kolor + = przepustowość * SampleLights (...)”. Za chwilę zajmę się szczegółami na temat SampleLights (). Ale w zasadzie zapętla wszystkie światła i zwraca swój udział w kolorze, osłabionym przez BSDF.

To świetnie, ale musimy wprowadzić jeszcze jedną zmianę, aby była poprawna; w szczególności, co dzieje się, gdy trafimy w światło. W starym kodzie dodaliśmy emisję światła do akumulacji kolorów. Ale teraz bezpośrednio odbijamy światło przy każdym odbiciu, więc jeśli dodamy emisję światła, „podwójnie zanurzymy”. Dlatego właściwą rzeczą jest… nic; pomijamy akumulację światła.

Istnieją jednak dwa narożne przypadki:

  1. Pierwszy promień
  2. Idealnie odbijane odbicia (inaczej lustra)

Jeśli pierwszy promień trafi w światło, powinieneś bezpośrednio zobaczyć jego emisję. Więc jeśli go pomińmy, wszystkie światła pojawią się jako czarne, mimo że powierzchnie wokół nich są oświetlone.

Kiedy uderzasz w idealnie błyszczące powierzchnie, nie możesz bezpośrednio próbkować światła, ponieważ promień wejściowy ma tylko jedno wyjście. Technicznie moglibyśmy sprawdzić, czy promień wejściowy uderzy w światło, ale nie ma sensu; główna pętla śledzenia ścieżki i tak to zrobi. Dlatego jeśli uderzymy w światło tuż po tym, jak uderzymy w lustrzaną powierzchnię, musimy zgromadzić kolor. Jeśli tego nie zrobimy, światła będą czarne w lusterkach.

Teraz zagłębimy się w 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;
}

Po angielsku:

  1. Zapętlić wszystkie światła
  2. Pomiń światło, jeśli go uderzymy
    • Nie podwójnie zanurzaj się
  3. Zbierz bezpośrednie oświetlenie ze wszystkich świateł
  4. Zwróć bezpośrednie oświetlenie

Wreszcie, EstimateDirect () właśnie oceniaBSDF(p,ωi,ωo)Li(p,ωi)

W przypadku punktowych źródeł światła jest to proste:

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

Jeśli jednak chcemy, aby światła miały powierzchnię, najpierw musimy wypróbować punkt na świetle. Dlatego pełna definicja to:

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

Możemy wdrożyć light-> SampleLi, jak chcemy; możemy wybrać punkt równomiernie lub próbkę ważności. W obu przypadkach dzielimy radość przez pdf wyboru punktu. Ponownie, aby spełnić wymagania Monte Carlo.

Jeśli BRDF jest silnie zależny od widoku, może być lepszy wybór punktu opartego na BRDF, zamiast losowego punktu na świetle. Ale jak wybieramy? Próbka oparta na świetle, czy oparta na BRDF?

Dlaczego nie oba? Wprowadź próbkowanie wielokrotnego znaczenia. W skrócie, oceniamy wiele razy, przy użyciu różnych technik próbkowania, a następnie uśrednij je razem, używając wag opartych na ich plikach pdf. W kodzie jest to: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;
}

Po angielsku:

  1. Najpierw próbkujemy światło
    • To aktualizuje interakcję.InputDirection
    • Daje nam Li za światło
    • I pdf wyboru tego punktu na światło
  2. Sprawdź, czy plik pdf jest prawidłowy, a promienność jest różna od zera
  3. Oszacuj BSDF za pomocą próbkowanego InputDirection
  4. Oblicz pdf dla BSDF, biorąc pod uwagę próbkowany InputDirection
    • Zasadniczo, jakie jest prawdopodobieństwo tej próbki, gdybyśmy próbkowali przy użyciu BSDF zamiast światła
  5. Oblicz wagę, używając lekkiego pdf i BSDF pdf
    • Veach i Guibas określają kilka różnych sposobów obliczania masy. Eksperymentalnie odkryli, że siła heurystyczna o mocy 2 działa najlepiej w większości przypadków. Więcej szczegółów odsyłam do artykułu. Implementacja jest poniżej
  6. Pomnóż wagę przez bezpośrednie obliczenie oświetlenia i podziel przez światło pdf. (Dla Monte Carlo) I dodaj do bezpośredniej akumulacji światła.
  7. Następnie próbkujemy BRDF
    • To aktualizuje interakcję.InputDirection
  8. Oceń BRDF
  9. Pobierz pdf, aby wybrać ten kierunek na podstawie BRDF
  10. Oblicz lekki pdf, biorąc pod uwagę próbkowany InputDirection
    • To jest lustro wcześniej. Jak prawdopodobny jest ten kierunek, gdybyśmy próbkowali światło
  11. Jeśli lightPdf == 0,0f, promień nie trafił w światło, więc po prostu zwróć bezpośrednie oświetlenie z próbki światła.
  12. W przeciwnym razie obliczyć masę i dodać do akumulacji bezpośrednie oświetlenie BSDF
  13. Na koniec zwróć zgromadzone bezpośrednie oświetlenie

.

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

Istnieje wiele optymalizacji / ulepszeń, które możesz zrobić w tych funkcjach, ale zmniejszyłem je, aby ułatwić ich zrozumienie. Jeśli chcesz, mogę udostępnić niektóre z tych ulepszeń.

Próbkowanie tylko jednego światła

W SampleLights () przeglądamy wszystkie światła i otrzymujemy ich wkład. W przypadku niewielkiej liczby świateł jest to w porządku, ale w przypadku setek lub tysięcy świateł staje się to drogie. Na szczęście możemy wykorzystać fakt, że integracja Monte Carlo jest gigantyczną średnią. Przykład:

Zdefiniujmy

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

Obecnie szacujemy przez:h(x)

h(x)=1Ni=1Nf(xi)+g(xi)

Ale obliczenia zarówno i jest drogie, więc zamiast tego zrobić:f(x)g(x)

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

Gdzie jest jednolitą zmienną losową, a jest zdefiniowane jako:ζr(ζ,x)

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

W tym przypadku ponieważ pdf musi być zintegrowany z 1, a do wyboru są 2 funkcje.pdf=12

Po angielsku:

  1. Losowo wybierz lub do oceny.g ( x )f(x)g(x)
  2. Podziel wynik przez (ponieważ istnieją dwa elementy)12
  3. Średni

Gdy N staje się duże, oszacowanie zbiegnie się z właściwym rozwiązaniem.

Możemy zastosować tę samą zasadę do próbkowania światła. Zamiast próbkować każde światło, losowo wybieramy jedno i mnożymy wynik przez liczbę świateł (jest to to samo, co dzielenie przez ułamek pdf):

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

W tym kodzie wszystkie światła mają jednakową szansę na wybranie. Jeśli jednak chcemy, możemy zrobić próbkę. Na przykład, możemy dać większym światłom większą szansę na wybranie lub światła bliżej powierzchni uderzenia. Musisz tylko podzielić wynik przez pdf, który nie byłby już .1numLights

Wielokrotne znaczenie Próbkowanie w kierunku „New Ray”

Obecny kod ma znaczenie tylko próbki kierunku „New Ray” na podstawie BSDF. Co zrobić, jeśli chcemy również podkreślić próbkę na podstawie położenia świateł?

Biorąc z tego, czego się nauczyliśmy powyżej, jedną z metod byłoby strzelenie dwoma „nowymi” promieniami i wagą na podstawie ich plików pdf. Jest to jednak zarówno kosztowne obliczeniowo, jak i trudne do wdrożenia bez rekurencji.

Aby temu zaradzić, możemy zastosować te same zasady, których się nauczyliśmy, próbkując tylko jedno światło. Oznacza to, że losowo wybierz próbkę i podziel przez pdf wyboru.

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

To powiedziawszy, czy naprawdę chcemy naświetlić próbkę kierunku „New Ray” w oparciu o światło? W przypadku bezpośredniego oświetlenia na radość wpływa zarówno BSDF powierzchni, jak i kierunek światła. Ale w przypadku oświetlenia pośredniego radość jest prawie wyłącznie określona przez BSDF wcześniej uderzanej powierzchni. Tak więc dodanie lekkości próbkowania nie daje nam niczego.

Dlatego powszechne jest, aby próbkować tylko „Nowy kierunek” za pomocą BSDF, ale stosować próbkowanie wielokrotnego znaczenia do bezpośredniego oświetlenia.

RichieSams
źródło
Dziękuję za wyjaśniającą odpowiedź! Rozumiem, że gdybyśmy użyli znacznika ścieżki bez wyraźnego próbkowania światła, nigdy nie trafilibyśmy w punktowe źródło światła. Możemy więc w zasadzie dodać jego wkład. Z drugiej strony, jeśli próbkujemy obszarowe źródło światła, musimy upewnić się, że nie powinniśmy ponownie uderzyć go pośrednim oświetleniem, aby uniknąć podwójnego zanurzenia
Mustafa Işık
Dokładnie! Czy jest jakaś część, która wymaga wyjaśnienia? Czy nie ma wystarczającej ilości szczegółów?
RichieSams
Ponadto, czy próbkowanie o różnym znaczeniu jest używane tylko do bezpośredniego obliczenia oświetlenia? Może tęskniłem, ale nie widziałem innego przykładu. Jeśli wystrzelę tylko jeden promień na odbicie w moim znaczniku ścieżki, wydaje się, że nie mogę tego zrobić dla obliczeń oświetlenia pośredniego.
Mustafa Işık
2
Próbkowanie wielokrotnego znaczenia można zastosować wszędzie tam, gdzie używa się próbkowania ważności. Potęgą próbkowania o wielorakim znaczeniu jest to, że możemy łączyć zalety wielu technik próbkowania. Na przykład w niektórych przypadkach próbkowanie o niewielkim znaczeniu będzie lepsze niż próbkowanie BSDF. W innych przypadkach odwrotnie. MIS połączy to, co najlepsze z obu światów. Jeśli jednak próbkowanie BSDF będzie lepsze w 100% przypadków, nie ma powodu, aby dodawać złożoności MIS. Dodałem kilka sekcji do odpowiedzi, aby rozwinąć ten punkt
RichieSams,
1
Wydaje się, że rozdzieliliśmy przychodzące źródła promieniowania na dwie części jako bezpośrednie i pośrednie. Próbkujemy światła bezpośrednio dla części bezpośredniej i podczas próbkowania tej części, zasadne jest, aby ważność próbkować światła, a także BSDF. Jednak w części pośredniej nie mamy pojęcia, który kierunek może potencjalnie dać nam wyższe wartości promieniowania, ponieważ to sam problem chcemy rozwiązać. Możemy jednak powiedzieć, który kierunek może wnieść większy wkład zgodnie z terminem cosinus i BSDF. Właśnie to rozumiem. Popraw mnie, jeśli się mylę, i dziękuję za wspaniałą odpowiedź.
Mustafa Işık