Unity GPU Path Tracing - Teil 2

Bild

"Es gibt nichts Schlimmeres als ein klares Bild eines verschwommenen Konzepts." - Fotograf Ansel Adams

Im ersten Teil des Artikels haben wir einen Whited Ray Tracer erstellt, der perfekte Reflexionen und scharfe Schatten nachzeichnen kann. Uns fehlen jedoch die Auswirkungen von Unschärfe: diffuse Reflexion, glänzende Reflexionen und weiche Schatten.

Basierend auf dem Code, den wir bereits haben , werden wir die 1986 von James Cajia formulierte Rendering-Gleichung iterativ lösen und unseren Renderer in einen Pfad-Tracer verwandeln, der die oben genannten Effekte übertragen kann. Wir werden wieder C # für Skripte und HLSL für Shader verwenden. Der Code wird auf Bitbucket hochgeladen.

Dieser Artikel ist viel mathematischer als der vorherige, aber seien Sie nicht beunruhigt. Ich werde versuchen, jede Formel so klar wie möglich zu erklären. Die Formeln werden hier benötigt, um zu sehen, was passiert und warum unser Renderer funktioniert. Ich empfehle daher, sie zu verstehen. Wenn etwas nicht klar ist, stellen Sie Fragen in den Kommentaren zum Originalartikel.

Das Bild unten wird mithilfe der Graffiti Shelter- Karte von der HDRI Haven-Website gerendert. Andere Bilder in diesem Artikel wurden mit der Kiara 9 Dusk- Karte gerendert.

Bild

Rendering-Gleichung


Aus formaler Sicht besteht die Aufgabe des fotorealistischen Renderers darin, die Rendering-Gleichung zu lösen, die wie folgt geschrieben ist:

L ( x , v e c o m e g a o ) = L e ( x ,  v e c o m e g a o ) + i n t O m e g a f r ( x ,    v e c o m e g a i ,  v e c o m e g a o ) ( vec omegai cdot vecn)L(x, vec omegai)d vec omegai


Lassen Sie es uns analysieren. Unser oberstes Ziel ist es, die Helligkeit des Bildschirmpixels zu bestimmen. Die Rendering-Gleichung gibt uns die Beleuchtungsstärke an L(x, vec omegao) von einem Punkt kommen x (Einfallspunkt des Strahls) in Richtung  vec omegao (die Richtung, in die der Strahl fällt). Die Oberfläche selbst kann eine Lichtquelle sein, die Licht emittiert Le(x, vec omegao) in unsere Richtung. Die meisten Oberflächen tun dies nicht, daher reflektieren sie nur Licht von außen. Deshalb wird das Integral verwendet. Es sammelt Licht aus allen möglichen Richtungen der Hemisphäre.  Omega um das Normale herum (daher berücksichtigen wir dabei die Beleuchtung, die von oben auf die Oberfläche fällt und nicht von innen , was für durchscheinende Materialien erforderlich sein kann).

Der erste Teil ist fr wird als bidirektionale Reflexionsverteilungsfunktion (BRDF) bezeichnet. Diese Funktion beschreibt visuell die Art des Materials, mit dem wir es zu tun haben: Metall oder Dielektrikum, dunkel oder hell, glänzend oder matt. BRDF bestimmt den Anteil der Beleuchtung  vec omegai was sich in der Richtung widerspiegelt  vec omegao . In der Praxis wird dies unter Verwendung eines Dreikomponentenvektors mit Werten von Rot, Grün und Blau im Intervall implementiert [0,1] .

Zweiter Teil - ( vec omegai cdot vecn) Ist das Äquivalent von 1 cos theta wo  theta - Winkel zwischen einfallendem Licht und Oberflächennormale  vecn . Stellen Sie sich eine Säule paralleler Lichtstrahlen vor, die senkrecht auf die Oberfläche fallen. Stellen Sie sich nun denselben Strahl vor, der in einem flachen Winkel auf die Oberfläche fällt. Das Licht wird über einen größeren Bereich verteilt, aber es bedeutet auch, dass jeder Punkt dieses Bereichs dunkler aussieht. Cosine wird benötigt, um dies zu berücksichtigen.

Schließlich wird die Beleuchtung selbst von erhalten  vec omegai wird rekursiv unter Verwendung der gleichen Gleichung bestimmt. Das heißt, die Beleuchtung am Punkt x Hängt vom einfallenden Licht aus allen möglichen Richtungen in der oberen Hemisphäre ab. In jede dieser Richtungen von einem Punkt x Es gibt noch einen anderen Punkt x prime deren Helligkeit wiederum von dem Licht abhängt, das aus allen möglichen Richtungen der oberen Hemisphäre dieses Punktes fällt. Alle Berechnungen werden wiederholt.

Hier ist, was hier passiert: Dies ist eine unendlich rekursive Integralgleichung mit einer unendlichen Anzahl von halbkugelförmigen Integrationsbereichen. Wir können diese Gleichung nicht direkt lösen, aber es gibt eine ziemlich einfache Lösung.



1 Vergiss es nicht! Wir werden oft über Kosinus sprechen und immer das Skalarprodukt im Auge behalten. Als  veca cdot vecb= | veca |  | vecb | cos( theta) und wir haben es mit Richtungen (Einheitsvektoren) zu tun, dann ist das Skalarprodukt der Kosinus bei den meisten Computergrafikaufgaben.

Monte Carlo kommt zur Rettung


Die Monte-Carlo-Integration ist eine numerische Integrationstechnik, mit der wir jedes Integral anhand einer endlichen Anzahl von Zufallsstichproben näherungsweise berechnen können. Darüber hinaus garantiert Monte Carlo die Konvergenz zur richtigen Entscheidung - je mehr Proben wir nehmen, desto besser. Hier ist seine verallgemeinerte Form:

F N a p p r o x f r ein C 1 N s u m N n = 0 f r a c f ( x n ) , p ( x n )    


Daher das Integral der Funktion f ( x n ) kann ungefähr berechnet werden, indem Zufallsstichproben in der Integrationsdomäne gemittelt werden. Jede Stichprobe wird durch die Wahrscheinlichkeit ihrer Auswahl geteilt. p ( x n ) . Aus diesem Grund hat die am häufigsten ausgewählte Probe mehr Gewicht als die am seltensten ausgewählte.

Bei einheitlichen Proben in der Hemisphäre (jede Richtung hat die gleiche Wahrscheinlichkeit, ausgewählt zu werden) ist die Wahrscheinlichkeit von Proben konstant: p ( o m e g a ) = f r a c 1 2 p i    (weil 2 p i  Ist die Oberfläche einer einzelnen Hemisphäre). Wenn wir das alles zusammenfügen, erhalten wir Folgendes:

L(x, vec omegao) ungefährLe(x, vec omegao)+ frac1N sumN.n=0 colorGreen2 pifr(x, vec omegai, vec omegao)( vec omegai cdot vecn)L(x, vec omegai)


Strahlung Le(x, vec omegao) Ist nur der Wert, der von unserer Shade Funktion zurückgegeben wird.  frac1N läuft bereits in unserer AddShader Funktion. Multiplikation mit L(x, vec omegai) passiert, wenn wir den Strahl reflektieren und weiter verfolgen. Unsere Aufgabe ist es, dem grünen Teil der Gleichung Leben einzuhauchen.

Voraussetzungen


Bevor wir uns auf eine Reise begeben, kümmern wir uns um einige Aspekte: Sammeln von Samples, deterministische Szenen und Shader-Zufälligkeit.

Akkumulation


Aus irgendeinem Grund OnRenderImage mir Unity die HDR-Textur nicht als destination in OnRenderImage . Das Format R8G8B8A8_Typeless hat bei mir funktioniert, sodass die Genauigkeit schnell zu niedrig wird, um eine große Anzahl von Samples zu akkumulieren. Um dies zu handhaben, fügen private RenderTexture _converged zum private RenderTexture _converged C # private RenderTexture _converged . Dies ist unser Puffer, der die Ergebnisse mit hoher Genauigkeit sammelt, bevor sie auf dem Bildschirm angezeigt werden. Wir initialisieren / geben die Textur auf die gleiche Weise wie _target in der InitRenderTexture Funktion frei. Verdoppeln Sie in der Render Funktion das Blitting:

 Graphics.Blit(_target, _converged, _addMaterial); Graphics.Blit(_converged, destination); 

Deterministische Szenen


Wenn Sie Änderungen am Rendering vornehmen, um den Effekt zu bewerten, ist es hilfreich, diese mit früheren Ergebnissen zu vergleichen. Bisher erhalten wir mit jedem Neustart des Wiedergabemodus oder jeder Neukompilierung des Skripts eine neue zufällige Szene. Um dies zu vermeiden, fügen Sie das public int SphereSeed dem public int SphereSeed C # public int SphereSeed und der folgenden Zeile am Anfang von SetUpScene :

 Random.InitState(SphereSeed); 

Jetzt können wir die Seed-Szenen manuell einstellen. Geben Sie eine beliebige Zahl ein und schalten Sie RayTracingMaster / wieder ein, bis Sie die richtige Szene erhalten.

Die folgenden Parameter wurden für Beispielbilder verwendet: Sphere Seed 1223832719, Sphere Radius [5, 30], Spheres Max 10000, Sphere Placement Radius 100.

Shader Zufälligkeit


Bevor wir mit der stochastischen Abtastung beginnen, müssen wir dem Shader Zufälligkeit hinzufügen. Ich werde die kanonische Zeichenfolge verwenden, die ich im Netzwerk gefunden habe und die der Einfachheit halber geändert wurde:

 float2 _Pixel; float _Seed; float rand() { float result = frac(sin(_Seed / 100.0f * dot(_Pixel, float2(12.9898f, 78.233f))) * 43758.5453f); _Seed += 1.0f; return result; } 

Initialisieren Sie _Pixel direkt in CSMain als _Pixel = id.xy damit jedes Pixel unterschiedliche Zufallswerte verwenden kann. _Seed in der Funktion _Seed von C # aus initialisiert.

 RayTracingShader.SetFloat("_Seed", Random.value); 

Die Qualität der hier erzeugten Zufallszahlen ist instabil. In Zukunft lohnt es sich, diese Funktion zu untersuchen und zu testen, indem der Einfluss von Parametern analysiert und mit anderen Ansätzen verglichen wird. Aber im Moment werden wir es einfach benutzen und auf das Beste hoffen.

Probenahme auf der Hemisphäre


Fangen wir noch einmal an: Wir brauchen zufällige Richtungen, die gleichmäßig in der Hemisphäre verteilt sind. Diese nicht triviale Aufgabe für den gesamten Umfang wird in diesem Artikel von Corey Simon ausführlich beschrieben. Es ist leicht, sich an die Hemisphäre anzupassen. So sieht der Shader-Code aus:

 float3 SampleHemisphere(float3 normal) { //     float cosTheta = rand(); float sinTheta = sqrt(max(0.0f, 1.0f - cosTheta * cosTheta)); float phi = 2 * PI * rand(); float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); //      return mul(tangentSpaceDir, GetTangentSpace(normal)); } 

Richtungen werden für eine auf der positiven Z-Achse zentrierte Halbkugel erzeugt, daher müssen wir sie so transformieren, dass sie auf der gewünschten Normalen zentriert sind. Wir erzeugen eine Tangente und eine Binormale (zwei Vektoren orthogonal zur Normalen und orthogonal zueinander). Zuerst wählen wir einen Hilfsvektor aus, um die Tangente zu erzeugen. Dazu nehmen wir die positive X-Achse und kehren nur dann zum positiven Z zurück, wenn es normalerweise (ungefähr) mit der X-Achse ausgerichtet ist. Dann können wir das Vektorprodukt verwenden, um die Tangente und dann die Binormale zu erzeugen.

 float3x3 GetTangentSpace(float3 normal) { //       float3 helper = float3(1, 0, 0); if (abs(normal.x) > 0.99f) helper = float3(0, 0, 1); //   float3 tangent = normalize(cross(normal, helper)); float3 binormal = normalize(cross(normal, tangent)); return float3x3(tangent, binormal, normal); } 

Lambert-Streuung


Nachdem wir nun einheitliche zufällige Richtungen haben, können wir mit der Implementierung des ersten BRDF fortfahren. Für die diffuse Reflexion wird am häufigsten das Lambert BRDF verwendet, das überraschend einfach ist: fr(x, vec omegai, vec omegao)= frackd pi wo kd - Dies ist Albedo-Oberfläche. Fügen wir es in unsere Monte-Carlo-Rendering-Gleichung ein (ich werde den Emissionsgrad noch nicht berücksichtigen) und sehen, was passiert:

L(x, vec omegao) approx frac1N sumNn=0 colorBlueViolet2kd( vec omegai cdot vecn)L(x, vec omegai)


Fügen wir diese Gleichung sofort in den Shader ein. Ersetzen Sie in der Shade Funktion den Code im if (hit.distance < 1.#INF) durch die folgenden Zeilen:

 //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal); ray.energy *= 2 * hit.albedo * sdot(hit.normal, ray.direction); return 0.0f; 

Die neue Richtung des reflektierten Strahls wird unter Verwendung unserer Funktion homogener Halbkugelproben bestimmt. Die Energie des Strahls wird mit dem entsprechenden Teil der oben gezeigten Gleichung multipliziert. Da die Oberfläche keine Beleuchtung AddShader (sie reflektiert nur direkt oder indirekt vom Himmel empfangenes Licht), geben wir 0 zurück. Vergessen Sie hier nicht, dass AddShader die Proben mittelt, sodass wir uns keine Sorgen machen müssen  frac1N sum . CSMain enthält bereits eine Multiplikation mit L(x, vec omegai) (der nächste reflektierte Strahl), so dass wir nicht mehr viel Arbeit haben.

sdot ist eine sdot , die ich für mich selbst definiert habe. Es gibt einfach das Ergebnis des Skalarprodukts mit einem zusätzlichen Koeffizienten zurück und begrenzt es dann auf das Intervall [0,1] ::

 float sdot(float3 x, float3 y, float f = 1.0f) { return saturate(dot(x, y) * f); } 

Lassen Sie uns zusammenfassen, was unser Code bisher tut. CSMain erzeugt die Primärstrahlen der Kamera und ruft Shade . Beim Überqueren der Oberfläche erzeugt diese Funktion wiederum einen neuen Strahl (gleichmäßig zufällig in der Halbkugel um die Normalen) und berücksichtigt den BRDF des Materials und den Kosinus in der Energie des Strahls. Am Schnittpunkt des Strahls mit dem Himmel nehmen wir HDRI (unsere einzige Beleuchtungsquelle) auf und geben die Beleuchtung zurück, die mit der Energie des Strahls multipliziert wird (d. H. Das Ergebnis aller vorherigen Schnittpunkte, beginnend mit der Kamera). Dies ist eine einfache Stichprobe, die sich mit einem konvergenten Ergebnis mischt. Infolgedessen wird die Auswirkung in jeder Probe berücksichtigt.  frac1N .

Es ist Zeit, alles in Arbeit zu überprüfen. Da Metalle keine diffuse Reflexion haben, SetUpScene wir sie zunächst in der SetUpScene Funktion eines C # Random.value (rufen Random.value hier jedoch Random.value auf, um den Determinismus der Szene Random.value ):

 bool metal = Random.value < 0.0f; 

Starten Sie den Wiedergabemodus und sehen Sie, wie das anfänglich verrauschte Bild gelöscht wird und zu einem schönen Rendering konvergiert:

Phong Spiegelbild


Nicht schlecht für nur ein paar Codezeilen (und einen kleinen Teil der Mathematik). Lassen Sie uns das Bild verfeinern, indem wir Spiegelreflexionen mit Phongs BRDF hinzufügen. Fongs ursprüngliche Formulierung hatte ihre Probleme (mangelnde Beziehungen und Energieeinsparung), aber glücklicherweise haben andere Menschen sie beseitigt . Das erweiterte BRDF wird unten gezeigt.  vec omegar Ist die Richtung des perfekt reflektierten Lichts und  alpha Ist ein Phong-Indikator, der die Rauheit steuert:

fr(x, vec omegai, vec omegao)=ks frac alpha+22 pi( vec omegar cdot vec omegao) alpha


Ein interaktives zweidimensionales Diagramm zeigt, wie das BRDF für Phong wann aussieht  alpha=15 für einen in einem Winkel von 45 ° einfallenden Strahl. Versuchen Sie, den Wert zu ändern.  alpha .

Fügen Sie dies in unsere Monte-Carlo-Rendering-Gleichung ein:

L(x, vec omegao) approx frac1N sumNn=0 colorbrownks( alpha+2)( vec omegar cdot vec omegao) alpha( vec omegai cdot vecn)L(x, vec omegai)


Und zum Schluss fügen wir dies dem bestehenden Lambert BRDF hinzu:

L(x, vec omegao) approx frac1N sumNn=0[ colorBlueViolet2kd+ colorbrownks( alpha+2)( vec omegar cdot vec omegao) alpha]( vec omegai cdot vecn)L(x, vec omegai)


Und so sehen sie im Code zusammen mit Lambert-Streuung aus:

 //    ray.origin = hit.position + hit.normal * 0.001f; float3 reflected = reflect(ray.direction, hit.normal); ray.direction = SampleHemisphere(hit.normal); float3 diffuse = 2 * min(1.0f - hit.specular, hit.albedo); float alpha = 15.0f; float3 specular = hit.specular * (alpha + 2) * pow(sdot(ray.direction, reflected), alpha); ray.energy *= (diffuse + specular) * sdot(hit.normal, ray.direction); return 0.0f; 

Beachten Sie, dass wir das Skalarprodukt durch ein etwas anderes, aber äquivalentes (reflektiertes) Produkt ersetzt haben  omegao statt  omegai ) SetUpScene nun Metallmaterialien wieder in die SetUpScene Funktionen und überprüfen Sie, wie es funktioniert.

Experimentieren mit verschiedenen Werten  alpha Möglicherweise stellen Sie ein Problem fest: Selbst eine geringe Leistung erfordert viel Zeit für die Konvergenz, und bei hoher Leistung ist das Rauschen besonders auffällig. Selbst nach ein paar Minuten Wartezeit ist das Ergebnis alles andere als ideal, was für eine so einfache Szene nicht akzeptabel ist.  alpha=15 und  alpha=300 mit 8192 Beispielen sieht das so aus:



Warum ist das passiert? Immerhin hatten wir vorher so schöne ideale Reflexionen (  alpha= infty )! .. Das Problem ist, dass wir homogene Proben erzeugen und diese nach BRDF gewichten. Mit hohen Phong-Werten ist der BRDF für alle klein, aber diese Richtungen kommen einer perfekten Reflexion sehr nahe, und es ist sehr unwahrscheinlich, dass wir sie anhand unserer homogenen Proben zufällig auswählen. Wenn wir dagegen tatsächlich eine dieser Richtungen kreuzen, ist der BRDF riesig, um alle anderen kleinen Stichproben zu kompensieren. Das Ergebnis ist eine sehr große Dispersion. Pfade mit mehreren Spiegelreflexionen sind noch schlechter und führen zu Rauschen, das in den Bildern sichtbar ist.

Verbesserte Probenahme


Um unseren Pfad-Tracer praktisch zu machen, müssen wir das Paradigma ändern. Anstatt wertvolle Proben in Bereichen zu verschwenden, in denen sie unwichtig sind (da sie einen sehr niedrigen BRDF- und / oder Kosinuswert erhalten), lassen Sie uns wichtige Proben generieren .

In einem ersten Schritt werden wir unsere idealen Überlegungen zurückgeben und dann sehen, wie diese Idee verallgemeinert werden kann. Dazu unterteilen wir die Schattierungslogik in diffuse und spiegelnde Reflexion. Für jede Stichprobe wählen wir zufällig die eine oder andere aus (abhängig vom Verhältnis kd und ks ) Im Falle einer diffusen Reflexion werden wir an homogenen Proben haften, aber für das Spiegelbild werden wir den Strahl explizit in die einzig wichtige Richtung reflektieren. Da jetzt weniger Proben für jede Art von Reflexion ausgegeben werden, müssen wir den Einfluss entsprechend erhöhen, um den gleichen Gesamtwert zu erhalten:

 //       hit.albedo = min(1.0f - hit.specular, hit.albedo); float specChance = energy(hit.specular); float diffChance = energy(hit.albedo); float sum = specChance + diffChance; specChance /= sum; diffChance /= sum; //     float roulette = rand(); if (roulette < specChance) { //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = reflect(ray.direction, hit.normal); ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction); } else { //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal); ray.energy *= (1.0f / diffChance) * 2 * hit.albedo * sdot(hit.normal, ray.direction); } return 0.0f; 

energy ist eine kleine Hilfsfunktion, die Farbkanäle mittelt:

 float energy(float3 color) { return dot(color, 1.0f / 3.0f); } 

Also haben wir einen schöneren Whited Ray Tracer aus dem vorherigen Teil erstellt, aber jetzt mit wirklich diffuser Schattierung (was weiche Schatten, Umgebungsokklusion und diffuse globale Beleuchtung impliziert):

Bild

Wichtigkeitsbeispiel


Schauen wir uns noch einmal die grundlegende Monte-Carlo-Formel an:

FN approx frac1N sumNn=0 fracf(xn)p(xn)


Wie Sie sehen können, teilen wir den Einfluss jeder Stichprobe (Stichprobe) auf die Wahrscheinlichkeit der Auswahl dieser bestimmten Stichprobe. Bisher haben wir homogene Hemisphärenproben verwendet, also hatten wir eine Konstante p( omega)= frac12 pi . Wie wir oben gesehen haben, ist dies beispielsweise beim Phong BRDF, der in einer sehr kleinen Anzahl von Richtungen groß ist, alles andere als optimal.

Stellen Sie sich vor, wir könnten eine Wahrscheinlichkeitsverteilung finden, die genau zur integrierbaren Funktion passt: p(x)=f(x) . Dann passiert folgendes:

FN approx frac1N sumNn=01


Jetzt haben wir keine Proben, die sehr wenig dazu beitragen. Es ist weniger wahrscheinlich, dass diese Stichproben ausgewählt werden. Dies verringert die Varianz des Ergebnisses erheblich und beschleunigt die Konvergenz des Renderings.

In der Praxis ist es unmöglich, eine solche ideale Verteilung zu finden, da einige Teile der integrierbaren Funktion (in unserem Fall BRDF × Kosinus × einfallendes Licht) unbekannt sind (dies ist am offensichtlichsten für einfallendes Licht), aber die Verteilung der Proben nach BRDF × Kosinus oder sogar nur nach BRDF hilft uns. Dieses Prinzip nennt man Stichproben nach Wichtigkeit.

Kosinusprobe


In den folgenden Schritten müssen wir unsere homogene Verteilung der Proben durch die Verteilung gemäß der Kosinusregel ersetzen. Vergessen Sie nicht, anstatt homogene Proben mit Kosinus zu multiplizieren und ihren Einfluss zu verringern , möchten wir eine proportional geringere Anzahl von Proben erzeugen.

Dieser Artikel von Thomas Poole beschreibt, wie das geht. Wir werden den alpha Parameter zu unserer SampleHemisphere Funktion SampleHemisphere . Die Funktion bestimmt den Index der Cosinusauswahl: 0 für eine einheitliche Stichprobe, 1 für die Cosinusauswahl oder höher für höhere Phong-Werte. Im Code sieht es so aus:

 float3 SampleHemisphere(float3 normal, float alpha) { //  ,      float cosTheta = pow(rand(), 1.0f / (alpha + 1.0f)); float sinTheta = sqrt(1.0f - cosTheta * cosTheta); float phi = 2 * PI * rand(); float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); //      return mul(tangentSpaceDir, GetTangentSpace(normal)); } 

Jetzt ist die Wahrscheinlichkeit jeder Probe gleich p( omega)= frac alpha+12 pi( vec omega cdot vecn) alpha . Die Schönheit dieser Gleichung mag nicht sofort offensichtlich erscheinen, aber wenig später werden Sie sie verstehen.

Lambert-Probe nach Bedeutung


Für den Anfang werden wir das Rendering der diffusen Reflexion verfeinern. In unserer homogenen Verteilung wird bereits die Lambert-Konstante BRDF verwendet, die wir jedoch durch Hinzufügen des Kosinus verbessern können. Die Wahrscheinlichkeitsverteilung der Stichprobe nach Kosinus (wobei  alpha=1 ) ist gleich  frac( vec omegai cdot vecn) pi , was unsere Monte-Carlo-Formel für diffuse Reflexion vereinfacht:

L(x, vec omegao) approx frac1N sumNn=0 colorBlueVioletkdL(x, vec omegai)


 //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal, 1.0f); ray.energy *= (1.0f / diffChance) * hit.albedo; 

Dies wird unsere diffuse Schattierung etwas beschleunigen. Kommen wir nun zum eigentlichen Thema.

Fongov-Probenahme nach Wichtigkeit


Für Phong BRDF ist das Verfahren ähnlich. Diesmal haben wir das Produkt zweier Kosinusse: den Standardkosinus aus der Rendering-Gleichung (wie im Fall der diffusen Reflexion), multipliziert mit dem BRDF-Eigenkosinus. Wir werden uns nur mit dem letzten befassen.

Fügen wir die Wahrscheinlichkeitsverteilung aus den obigen Beispielen in die Phong-Gleichung ein. Eine detaillierte Schlussfolgerung findet sich in Lafortune und Willems: Verwendung des modifizierten Phong-Reflexionsmodells für physikalisch basiertes Rendering (1994) :

L(x, vec omegao) approx frac1N sumNn=0 colorbrownks frac alpha+2 alpha+1( vec omegai cdot vecn)L(x, vec omegai)


 //   float alpha = 15.0f; ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(reflect(ray.direction, hit.normal), alpha); float f = (alpha + 2) / (alpha + 1); ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction, f); 

Diese Änderungen reichen aus, um Probleme mit der hohen Leistung in Phong zu beseitigen und unser Rendering in einer viel vernünftigeren Zeit zu konvergieren.

Material


Lassen Sie uns abschließend unsere Szenengenerierung erweitern, um sich ändernde Werte für die Glätte und das Emissionsvermögen der Kugeln zu schaffen! public Vector3 emission in struct Sphere aus einem C # public Vector3 emission public float smoothness public Vector3 emission und die public Vector3 emission . Da wir die Größe der Struktur geändert haben, müssen wir den Schritt beim Erstellen des Rechenpuffers ändern (4 × die Anzahl der Gleitkommazahlen, erinnerst du dich?). SetUpScene Sie die SetUpScene Funktion Werte für Glätte und SetUpScene einfügen.

struct RayHit im Shader beide Variablen zu struct Sphere und struct RayHit und initialisieren Sie sie dann in CreateRayHit . Und schließlich legen Sie beide Werte in IntersectGroundPlane (fest codiert, fügen Sie alle Werte ein) und IntersectSphere (Abrufen von Werten aus Sphere ) fest.

Ich möchte Glättungswerte auf die gleiche Weise wie im Standard-Unity-Shader verwenden, der sich von einem eher willkürlichen Fong-Exponenten unterscheidet. Hier ist eine gute Konvertierung, die in der Shade Funktion verwendet werden kann:

 float SmoothnessToPhongAlpha(float s) { return pow(1000.0f, s * s); } 

 float alpha = SmoothnessToPhongAlpha(hit.smoothness); 



Die Verwendung des Emissionsvermögens erfolgt durch Rückgabe eines Werts in Shade :

 return hit.emission; 

Ergebnisse


Atme tief ein. Entspannen Sie sich und warten Sie, bis das Bild zu einem so schönen Bild wird:

Bild

Glückwunsch! Sie haben es geschafft, durch das Dickicht mathematischer Ausdrücke zu kommen. Wir haben einen Pfad-Tracer implementiert, der diffuse und gespiegelte Schattierungen durchführt, die Stichproben nach Wichtigkeit kennenlernen und dieses Konzept sofort anwenden, sodass das Rendering in Minuten und nicht in Stunden oder Tagen konvergiert.

Im Vergleich zum vorherigen Artikel war dieser Artikel ein großer Schritt in Bezug auf die Komplexität, verbesserte aber auch die Qualität des Ergebnisses erheblich. Das Arbeiten mit mathematischen Berechnungen braucht Zeit, rechtfertigt sich jedoch, da es Ihr Verständnis für das Geschehen erheblich vertiefen kann und es Ihnen ermöglicht, den Algorithmus zu erweitern, ohne die physikalische Zuverlässigkeit zu beeinträchtigen.

Danke fürs Lesen! Im dritten Teil werden wir (für eine Weile) den Dschungel der Probenahme und Beschattung verlassen und in die Zivilisation zurückkehren, um die Herren Möller und Trumbor zu treffen. Wir müssen mit ihnen über Dreiecke sprechen.

Über den Autor: David Curie ist Entwickler von Three Eyed Games, Volkswagen Virtual Engineering Lab-Programmierer, Computergrafikforscher und Heavy-Metal-Musiker.

Bild

Source: https://habr.com/ru/post/de412685/


All Articles