Progressive Pfadverfolgung mit expliziter Lichtabtastung

14

Ich verstand die Logik hinter der Wichtigkeitsprobe für den BRDF-Teil. Wenn es jedoch darum geht, Lichtquellen explizit abzutasten, wird alles verwirrend. Wenn ich zum Beispiel eine Punktlichtquelle in meiner Szene habe und sie ständig direkt an jedem Bild abtaste, sollte ich sie als weiteres Beispiel für die Integration von Monte Carlo zählen? Das heißt, ich nehme eine Probe aus der cosinusgewichteten Verteilung und eine andere aus dem Punktlicht. Sind es insgesamt zwei Proben oder nur eine? Sollte ich auch die Strahlung, die von der direkten Stichprobe kommt, auf einen Begriff aufteilen?

Mustafa Işık
quelle

Antworten:

19

In der Pfadverfolgung gibt es mehrere Bereiche, für die eine Stichprobe erstellt werden kann. Darüber hinaus kann in jedem dieser Bereiche auch Multiple Importance Sampling verwendet werden, das erstmals in der Veröffentlichung von Veach und Guibas aus dem Jahr 1995 vorgeschlagen wurde . Schauen wir uns zur besseren Erklärung einen Rückwärtspfad-Tracer an:

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

Auf Englisch:

  1. Schießen Sie einen Strahl durch die Szene
  2. Überprüfen Sie, ob wir etwas getroffen haben. Wenn nicht, geben wir die Farbe der Skybox zurück und brechen.
  3. Überprüfen Sie, ob wir ein Licht schlagen. Wenn ja, addieren wir die Lichtemission zu unserer Farbakkumulation
  4. Wähle eine neue Richtung für den nächsten Strahl. Wir können dies einheitlich oder anhand des BRDF-Beispiels durchführen
  5. Bewerten Sie das BRDF und akkumulieren Sie es. Hier müssen wir durch das pdf unserer gewählten Richtung dividieren, um dem Monte-Carlo-Algorithmus zu folgen.
  6. Erstellen Sie einen neuen Strahl, der auf der gewählten Richtung und der Herkunft basiert
  7. [Optional] Verwenden Sie russisches Roulette, um zu entscheiden, ob wir den Strahl beenden sollen
  8. Gehe zu 1

Mit diesem Code erhalten wir nur dann Farbe, wenn der Strahl irgendwann auf ein Licht trifft. Darüber hinaus werden pünktliche Lichtquellen nicht unterstützt, da sie keine Fläche haben.

Um dies zu beheben, testen wir die Lichter direkt bei jedem Sprung. Wir müssen ein paar kleine Änderungen vornehmen:

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

Zuerst fügen wir "color + = throughput * SampleLights (...)" hinzu. Ich werde gleich auf SampleLights () eingehen. Im Wesentlichen durchläuft es jedoch alle Lichter und gibt ihren Beitrag zur Farbe zurück, die vom BSDF gedämpft wird.

Das ist großartig, aber wir müssen noch eine Änderung vornehmen, um es richtig zu machen. speziell, was passiert, wenn wir ein Licht schlagen. Im alten Code haben wir die Lichtemission zur Farbakkumulation addiert. Aber jetzt tasten wir das Licht bei jedem Sprung direkt ab. Wenn wir also die Emission des Lichts hinzufügen, würden wir "doppelt eintauchen". Daher ist das Richtige, was zu tun ist ... nichts; Wir überspringen die Ansammlung der Lichtemission.

Es gibt jedoch zwei Eckfälle:

  1. Der erste Strahl
  2. Perfekt spiegelnde Bounces (auch Spiegel genannt)

Wenn der erste Strahl auf das Licht trifft, sollten Sie die Emission des Lichts direkt sehen. Wenn wir es also überspringen, werden alle Lichter schwarz angezeigt, obwohl die Oberflächen um sie herum beleuchtet sind.

Wenn Sie auf eine perfekt spiegelnde Oberfläche treffen, können Sie ein Licht nicht direkt abtasten, da ein Eingangsstrahl nur einen Ausgang hat. Nun, technisch könnten wir prüfen, ob der Eingangsstrahl auf ein Licht trifft, aber es macht keinen Sinn; Die Hauptschleife für die Pfadverfolgung wird dies sowieso tun. Wenn wir also direkt nach dem Auftreffen auf eine spiegelnde Oberfläche auf ein Licht treffen, müssen wir die Farbe akkumulieren. Andernfalls leuchten die Spiegel schwarz.

Nun wollen wir uns mit SampleLights () befassen:

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

Auf Englisch:

  1. Schlaufe durch alle Lichter
  2. Überspringe das Licht, wenn wir es treffen
    • Tauchen Sie nicht doppelt ein
  3. Sammeln Sie die direkte Beleuchtung von allen Lichtern
  4. Bringen Sie die direkte Beleuchtung zurück

Schließlich wertet Estimatedirect () nur aus BSDF(p,ωich,ωÖ)Lich(p,ωich)

Für pünktliche Lichtquellen ist dies einfach:

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

Wenn wir jedoch möchten, dass Lichter eine Fläche haben, müssen wir zuerst einen Punkt auf dem Licht abtasten. Daher lautet die vollständige Definition:

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

Wir können light-> SampleLi implementieren, wie wir wollen; wir können den Punkt einheitlich wählen oder die Stichprobe der Wichtigkeit. In beiden Fällen teilen wir die Radiosität durch das PDF, indem wir den Punkt auswählen. Auch hier gilt, die Anforderungen von Monte Carlo zu erfüllen.

Wenn die BRDF stark von der Ansicht abhängig ist, ist es möglicherweise besser, einen Punkt basierend auf der BRDF zu wählen, als einen zufälligen Punkt auf dem Licht. Aber wie wählen wir aus? Probe basierend auf dem Licht oder basierend auf dem BRDF?

BSDF(p,ωich,ωÖ)Lich(p,ωich)

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

Auf Englisch:

  1. Zuerst probieren wir das Licht
    • Dadurch wird die Interaktion.InputDirection aktualisiert
    • Gibt uns das Li für das Licht
    • Und das PDF zur Auswahl dieses Punktes im Licht
  2. Vergewissern Sie sich, dass das PDF gültig ist und der Strahlungsgrad ungleich Null ist
  3. Bewerten Sie die BSDF mit der abgetasteten InputDirection
  4. Berechnen Sie das PDF für das BSDF anhand der abgetasteten InputDirection
    • Wie wahrscheinlich ist diese Stichprobe, wenn wir die BSDF anstelle des Lichts verwenden
  5. Berechnen Sie das Gewicht mit dem Light-PDF und dem BSDF-PDF
    • Veach und Guibas definieren verschiedene Methoden zur Gewichtsberechnung. Experimentell fanden sie die Potenzheuristik mit einer Potenz von 2 für die meisten Fälle am besten. Ich verweise Sie auf das Papier für weitere Details. Die Implementierung ist unten
  6. Multiplizieren Sie das Gewicht mit der direkten Beleuchtungsberechnung und dividieren Sie es durch das PDF-Licht. (Für Monte Carlo) Und zur direkten Lichtakkumulation hinzufügen.
  7. Dann probieren wir die BRDF
    • Dadurch wird die Interaktion.InputDirection aktualisiert
  8. Bewerten Sie die BRDF
  9. Holen Sie sich das PDF für die Auswahl dieser Richtung basierend auf dem BRDF
  10. Berechnen Sie das Light-PDF anhand der abgetasteten InputDirection
    • Dies ist der Spiegel der Vergangenheit. Wie wahrscheinlich ist diese Richtung, wenn wir das Licht abtasten
  11. Wenn lightPdf == 0.0f, dann hat der Strahl das Licht verfehlt, also leiten Sie einfach das direkte Licht von der Lichtprobe zurück.
  12. Berechnen Sie andernfalls das Gewicht und addieren Sie die BSDF-Direktbeleuchtung zur Akkumulation
  13. Zuletzt die akkumulierte Direktbeleuchtung zurückgeben

.

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

Es gibt eine Reihe von Optimierungen / Verbesserungen, die Sie an diesen Funktionen vornehmen können, aber ich habe sie reduziert, um sie verständlicher zu machen. Wenn Sie möchten, kann ich einige dieser Verbesserungen mitteilen.

Nur ein Licht abtasten

In SampleLights () durchlaufen wir alle Lichter und erhalten ihren Beitrag. Für eine kleine Anzahl von Lichtern ist dies in Ordnung, aber für Hunderte oder Tausende von Lichtern wird dies teuer. Glücklicherweise können wir die Tatsache ausnutzen, dass die Monte-Carlo-Integration ein gigantischer Durchschnitt ist. Beispiel:

Lassen Sie uns definieren

h(x)=f(x)+G(x)

h(x)

h(x)=1Nich=1Nf(xich)+G(xich)

f(x)G(x)

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

ζr(ζ,x)

r(ζ,x)={f(x),0.0ζ<0,5G(x),0,5ζ<1,0

pdf=12 weil das pdf zu 1 integriert werden muss und 2 Funktionen zur Auswahl stehen.

Auf Englisch:

  1. f(x)G(x)
  2. Teilen Sie das Ergebnis durch 12 (da es zwei Gegenstände gibt)
  3. Durchschnittlich

Wenn N groß wird, konvergiert die Schätzung zur richtigen Lösung.

Wir können dasselbe Prinzip auf die Lichtabtastung anwenden. Anstatt jedes Licht abzutasten, wählen wir zufällig eines aus und multiplizieren das Ergebnis mit der Anzahl der Lichter (Dies entspricht der Division durch das PDF-Bruchteil):

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

Mehrfache Bedeutung Abtastung der "New Ray" -Richtung

Die Bedeutung des aktuellen Codes tastet nur die Richtung "New Ray" ab, basierend auf der BSDF. Was ist, wenn wir auch eine wichtige Stichprobe basierend auf dem Ort der Lichter haben wollen?

Nach dem, was wir oben gelernt haben, besteht eine Methode darin, zu schießen zwei "neue" Strahlen und ein Gewicht jeweils auf ihren PDFS basieren. Dies ist jedoch sowohl rechenintensiv als auch schwer ohne Rekursion zu implementieren.

Um dies zu überwinden, können wir die gleichen Prinzipien anwenden, die wir durch Abtasten nur eines Lichts gelernt haben. Das heißt, wählen Sie nach dem Zufallsprinzip eine aus, die Sie probieren möchten, und dividieren Sie die Auswahl durch das PDF.

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

Wollen wir wirklich die "New Ray" -Richtung anhand des Lichts abtasten? Bei direkter Beleuchtung wird die Radiosität sowohl vom BSDF der Oberfläche als auch von der Richtung des Lichts beeinflusst. Aber für indirekte Beleuchtung wird die Radiosität jedoch fast ausschließlich von der BSDF der zuvor getroffenen Oberfläche bestimmt. Das Hinzufügen von Lichtwertabtastungen gibt uns also nichts.

Daher ist es üblich, nur die "Neue Richtung" mit der BSDF abzutasten, aber die direkte Beleuchtung mit Multiple Wichtigkeit abzutasten.

RichieSams
quelle
Vielen Dank für die klärende Antwort! Ich verstehe, wenn wir einen Pfad-Tracer ohne explizite Lichtabtastung verwenden würden, würden wir niemals eine Punktlichtquelle treffen. Wir können also im Grunde seinen Beitrag hinzufügen. Wenn wir andererseits eine Flächenlichtquelle abtasten, müssen wir sicherstellen, dass wir sie nicht erneut mit der indirekten Beleuchtung treffen, um ein doppeltes Eintauchen zu vermeiden
Mustafa Işık,
Genau! Gibt es einen Teil, den Sie klären müssen? Oder gibt es nicht genug Details?
RichieSams
Wird die Mehrfachauswahl nur für die direkte Beleuchtungsberechnung verwendet? Vielleicht habe ich es verpasst, aber ich habe kein anderes Beispiel dafür gesehen. Wenn ich in meinem Path Tracer nur einen Strahl pro Bounce aufnehme, kann ich das anscheinend nicht für die Berechnung der indirekten Beleuchtung tun.
Mustafa Işık
2
Sampling von Mehrfachbedeutung kann überall dort angewendet werden, wo Sie das Sampling von Wichtigkeit verwenden. Die Kraft der Mehrfachauswahl besteht darin, dass wir die Vorteile der Mehrfachauswahl kombinieren können. In einigen Fällen ist beispielsweise die Abtastung mit geringer Wichtigkeit besser als die BSDF-Abtastung. In anderen Fällen umgekehrt. MIS wird das Beste aus beiden Welten verbinden. Wenn die BSDF-Stichprobe jedoch in 100% der Fälle besser ist, gibt es keinen Grund, die Komplexität von MIS zu erhöhen. Ich habe der Antwort einige Abschnitte hinzugefügt, um diesen Punkt zu
erläutern
1
Anscheinend haben wir eingehende Strahlungsquellen direkt und indirekt in zwei Teile geteilt. Wir beproben Lichter explizit für den direkten Teil und während wir diesen Teil beproben, ist es sinnvoll, sowohl die Lichter als auch die BSDFs zu beproben. Für den indirekten Teil wissen wir jedoch nicht, in welche Richtung sich möglicherweise höhere Strahlungswerte ergeben, da wir das Problem selbst lösen möchten. Wir können jedoch sagen, welche Richtung je nach Kosinusbegriff und BSDF mehr beitragen kann. Das verstehe ich. Korrigiere mich, wenn ich falsch liege und danke dir für deine tolle Antwort.
Mustafa Işık