Resultierende Wahrscheinlichkeitsdichte in Path Tracer für Pfade mit Next Event Estimation

7

Ich versuche, meinen eigenen Gradient Domain Path Tracer zu implementieren, indem ich dem Code desjenigen folge, der ihn bereits implementiert hat:

https://gist.github.com/BachiLi/4f5c6e5a4fef5773dab1

Ich habe es bereits geschafft, verschiedene Schritte zu durchlaufen, aber ich wollte etwas mehr tun. Ich habe den Code in der Referenz durch die Implementierung der nächsten Ereignisschätzung erweitert und hier sind einige Ergebnisse.

Normal Path Tracer Bild:

Grundlegender Pfad-Tracer

Ergebnisbild der Verlaufsdomäne:

Gradientendomänenbild ohne Schätzung des nächsten Ereignisses

Die Ergebnisse sind bereits gut. Aber wie gesagt, ich wollte etwas mehr. Also habe ich Next Event Estimation implementiert und hier ist das Ergebnis des grundlegenden Path Tracers:

Path Tracer mit NEE

Hier ist mein Code:

private Vector3 SampleWithNEE( Ray ray )
{
  // prepare
  Vector3 T = (1,1,1), E = (0,0,0), NL = (0,-1,0);
  int depth = 0;
  // random walk
  while (depth++ < MAXDEPTH)
  {
    // find nearest ray/scene intersection
    Scene.Intersect( ray );
    if (ray.objIdx == -1) break; //if there is no intersection
    Vector3 I = ray.O + ray.t * ray.D; //go to the Hit Point on the scene
    Material material = scene.GetMaterial( ray.objIdx, I );
    if (material.emissive) //case of a light
    {
        E += material.diffuse;
        break;
    }
    // next event estimation
    Vector3 BRDF = material.diffuse * 1 / PI;
    float f = RTTools.RandomFloat();
    Vector3 L = Scene.RandomPointOnLight() - I;
    float dist = L.Length();
    L = Vector3.Normalize( L );
    float NLdotL = Math.Abs( Vector3.Dot( NL, -L ) );
    float NdotL = Vector3.Dot( ray.N, L );
    if (NdotL > 0)
    {
        Ray r = new Ray( I + L * EPSILON, L, dist - 2 * EPSILON ); //make it a tiny bit shorter otherwise I risk to hit my starting and destination point
        Scene.Intersect( r );
        if (r.objIdx == -1) //no occlusion towards the light
        {
            float solidAngle= (nldotl * light.getArea()) / (dist * dist);
            E += T * (NdotL) * solidAngle * BRDF * light.emission;
        }
    }
    // sample random direction on hemisphere
    Vector3 R = DiffuseReflectionCosWeighted( ray.N );
    float hemi_PDF = Vector3.Dot( R, ray.N ) / PI;
    T *= (Vector3.Dot( R, ray.N ) / hemiPDF) * BRDF;
    ray = new Ray( I + R * EPSILON, R, 1e34f );
  }
  return E;
}

Die Dinge funktionieren wirklich gut und die Ergebnisse werden mit dem Bild oben gezeigt. Noch etwas: Ich habe nur diffuse Oberflächen in meiner Szene.

Das Problem ist nun, dass ich bei dieser Methode zwei Arten von PDFs verwende:

  • Eine davon wird durch zufälliges Abtasten des Lichts in der direkten Beleuchtung der nächsten Ereignisschätzung gegeben. Tatsächlich ist SolidAngle unser PDF oder besser 1 / PDF.
  • Das zweite PDF wird von DiffuseReflectionCosWeighted verwendet, wodurch ein PDF mit CosTheta / PI erstellt wird .

Bisher ist alles in Ordnung und für alle Implementierungsdetails können Sie sich nur meinen Code ansehen, aber Probleme treten mit meinem Gradient Domain Path Tracer auf. In der Tat benötige ich dort, wie auch in dem oben von Tzu-Mao Li implementierten Referenzlink, die endgültige Wahrscheinlichkeitsdichte eines ganzen Pfades , um das endgültige Gradientenbild zu berechnen. Wie habe ich es für den Fall ohne Next Event Estimation (NEE) berechnet? In diesem Fall (da ich nur diffuse Oberflächen habe) ist diese Wahrscheinlichkeit das Produkt von CosTheta / PI bei jedem Sprung in der Szene. Alles ist in Ordnung und das resultierende Verlaufsbild ist oben dargestellt.

Wenn ich NEE verwende, funktionieren die Dinge stattdessen nicht mehr, da sich die Wahrscheinlichkeitsdichte meines gesamten Pfads ändert und ich nicht verstehe, wie es ist. Das resultierende Gradientendomänenbild mit der Schätzung des nächsten Ereignisses lautet:

Geben Sie hier die Bildbeschreibung ein

Ich muss verstehen, wie man die endgültige Dichtewahrscheinlichkeit eines Pfades berechnet. Kannst du mir dabei helfen? Danke im Voraus!

Tarta
quelle

Antworten:

4

Ich habe keine Erfahrung mit Gradient Domain Path Tracing, aber hier sind meine Gedanken:

Es scheint ein anderes Problem zu geben

Wenn Sie sich die kleinen Verzerrungsspitzen im endgültigen Bild genau ansehen, werden Sie feststellen, dass sie alle aus derselben Richtung beleuchtet sind - oben links bei gleichmäßigen 45 Grad. Die Kugel scheint auch aus diesem Winkel und nicht von oben durch die Lichtquelle beleuchtet zu werden.

Es ist unwahrscheinlich, dass dies durch eine falsche Wahrscheinlichkeitsschätzung für einen Pfad erklärt wird. Ich würde erwarten, dass es ein anderes Problem mit dem Code gibt, auf das diese Verzerrungen hinweisen.

Ich werde daher auf diese beiden getrennten Punkte eingehen:

  1. Sie möchten wissen, wie die Wahrscheinlichkeitsdichte eines Pfads bei Verwendung der Schätzung des nächsten Ereignisses berechnet wird.
  2. Es gibt Hinweise auf ein Problem, das damit nichts zu tun hat.

Ich werde den Code auch auf nicht wesentliche Punkte überprüfen - aber ich werde dies bis nach dem Wesentlichen belassen.

Wahrscheinlichkeitsdichte eines Pfades bei Verwendung der Schätzung des nächsten Ereignisses

Wenn man sich das Papier ansieht, auf dem der Code basiert , dem Sie folgen , scheint es, dass die in Abschnitt 5.2 beschriebene neuartige Verschiebungsabbildung in Bezug auf die Reflexionseigenschaften der Oberflächen definiert ist, die sich an den Eckpunkten des Pfades befinden. Ich muss betonen, dass ich dies nicht vollständig verstehe, aber es deutet darauf hin, dass die Schätzung des nächsten Ereignisses möglicherweise keine Änderung dieses Ansatzes erfordert, da die angetroffenen Oberflächen dieselben sind. Wenn die anderen Probleme behoben sind, ist es hoffentlich einfacher zu beurteilen, ob das Bild korrekt aussieht.

Beachten Sie, dass in Abschnitt 5.2 des Dokuments bereits erwähnt wird (direkt unter Abbildung 10), dass die Abtastung des Emitters " entweder mit BSDF oder mit Flächenabtastung " berücksichtigt wird .

Der Unterschied zur Schätzung des nächsten Ereignisses besteht darin, dass die Flächenabtastung an jedem Scheitelpunkt des Pfades erfolgt, aber es ist mir nicht klar, dass dies ein Problem verursachen sollte.

Die Tatsache, dass Ihre Szene nur diffuse Flächen verwendet, bedeutet, dass der Versatzpfad in den meisten Fällen wieder mit dem Basispfad am zweiten Scheitelpunkt verbunden werden sollte, sodass Sie nur die Flächenabtastung für den ersten Scheitelpunkt des Versatzpfads neu berechnen müssen.

Die Ursache für die falsche Beleuchtungsrichtung

Beim Lesen des Codes, um mich mit der Funktionsweise vertraut zu machen, habe ich festgestellt, dass dieser NLdotLberechnet, aber dann nicht verwendet wird. Eine Textsuche ergab, dass das einzige andere Vorkommen einen anderen Fall hat : nldotl. Hier sind die beiden Variablen im Kontext (erste und neunte Zeile dieses Auszugs):

float NLdotL = Math.Abs( Vector3.Dot( NL, -L ) );
float NdotL = Vector3.Dot( ray.N, L );
if (NdotL > 0)
{
    Ray r = new Ray( I + L * EPSILON, L, dist - 2 * EPSILON ); //make it a tiny bit shorter otherwise I risk to hit my starting and destination point
    Scene.Intersect( r );
    if (r.objIdx == -1) //no occlusion towards the light
    {
        float solidAngle= (nldotl * light.getArea()) / (dist * dist);
        E += T * (NdotL) * solidAngle * BRDF * light.emission;
    }
}

Da nldotlnicht definiert ist, ist das Ergebnis des Codes undefiniertes Verhalten. In der Praxis verhält sich das Programm wahrscheinlich so, als wäre nldotles Null, oder für einige Compiler möglicherweise ein konstanter beliebiger Wert oder sogar ein anderer beliebiger Wert bei jeder Iteration. Für Ihren speziellen Compiler scheint es sich um einen konstanten Wert zu handeln, und ich vermute sehr, dass dies die Ursache für die deutliche Ausrichtung des Beleuchtungswinkels auf allen Flecken und der Kugel ist. Wenn es auch ein anderes Problem gibt, das dazu beiträgt, ist es einfacher, es in einer separaten Frage zu analysieren, sobald dieses anfängliche Problem behoben wurde.

Es kann sinnvoll sein, eine Compiler- und / oder Flag-Einstellung zu verwenden, die einen Fehler oder zumindest eine Warnung für undefinierte Variablen ausgibt, da diese Art von Fehler sowohl sehr leicht zu machen als auch später leicht zu übersehen ist.

Zusätzlicher Beitrag der Lichtquelle

Es scheint ein weiteres Problem zu geben, das dazu führt, dass die Ergebnisse auf subtilere Weise ohne offensichtliche Verzerrung falsch sind. Aufgrund der Schätzung des nächsten Ereignisses trägt die Lichtquelle zu jedem Schritt entlang des Pfades bei. Dies bedeutet, dass es keinen Beitrag leisten sollte, wenn der Pfad selbst direkt auf die Lichtquelle trifft. Andernfalls trägt die Lichtquelle für diesen Pfad zweimal bei. Sie können dies korrigieren, indem Sie Folgendes ändern:

if (material.emissive) //case of a light
{
    E += material.diffuse;
    break;
}

zu:

if (material.emissive) //case of a light
{
    break;
}

Dies ergibt einen Nullbeitrag vom Schnittpunkt mit dem Licht.

Da ich nur diese eine Funktion sehen kann, kann ich nicht erraten, ob dadurch auch das Licht im Bild schwarz erscheint. Möglicherweise müssen Sie sich auf Strahlen einstellen, die an der Kamera beginnen und direkt auf das Licht treffen.


Code-Review

Doppelt endliche Strahlen

Ich bin es gewohnt, einen Strahl als ein halb unendliches Liniensegment zu definieren - mit einem Startpunkt, aber ohne Endpunkt. Ich stelle fest, dass dieser Code einem Strahl sowohl einen Startpunkt als auch eine Länge gibt. Der einzige Ort, an dem ich einen Grund dafür sehen kann, ist das Testen eines Schattenstrahls gegen die Lichtquelle: Der Code überprüft, ob es auf dem Weg zum Licht keine Schnittpunkte gibt, daher müssen sich Schnittpunkte hinter dem Licht (oder auf dem Licht selbst) befinden ausgeschlossen. An allen anderen Stellen wird der Strahl mit einer pseudo-unendlichen Länge definiert ( 1e34f).

Der folgende Vorschlag hat keinen Einfluss auf die Richtigkeit Ihres Codes, ist jedoch möglicherweise besser lesbar und verhindert, dass Sie die Notwendigkeit der Unendlichkeit umgehen und epsilon zweimal berücksichtigen müssen.

Wenn der Strahl lediglich ein Startpunkt und eine Richtung ist, können Schattenstrahlen einfach überprüfen, ob der erste Schnittpunkt das Licht ist, anstatt zu überprüfen, ob es keinen Schnittpunkt gibt. Zum Beispiel durch Ersetzen von:

if (r.objIdx == -1) //no occlusion towards the light

mit:

if (r.objIdx == LIGHT) // There is no object intersected before the light

Hier verwende ich LIGHTals Platzhalter für die ID der Lichtquelle, da dieser Teil des Codes nicht in der Frage enthalten ist.

  • Dies gibt immer falsch aus, wenn das Licht von einem näheren Objekt verdeckt wird.
  • Dies gilt immer dann, wenn der Strahl vor weiter entfernten Objekten auf das Licht trifft.

Dies entspricht daher dem aktuellen Code, erfordert jedoch nicht, dass der Strahl eine Länge speichert.

Lichter, die nicht reflektieren

Der Code modelliert derzeit Lichter und Oberflächen separat. Dies bedeutet, dass ein Objekt, wenn es ein Licht ist, nur emittiert und kein Licht von anderen Objekten reflektiert.

Dies verursacht einen vernachlässigbaren Unterschied in der Beispielszene in der Frage, die ein einzelnes helles Licht hat. Bei Verwendung mit mehreren Dimmerlichtern fällt jedoch auf, dass sie sich nicht gegenseitig beleuchten. In vielen Fällen ist der Unterschied nicht erkennbar, sodass dies kein Problem darstellt, wenn er für die Szenen funktioniert, die Sie rendern möchten.

Trichoplax
quelle