Lerne OpenGL. Lektion 6.2 - Physikalisches Rendern. Analytische Lichtquellen

OGL3 Die vorherige Lektion gab einen Überblick über die Grundlagen der Implementierung eines physikalisch plausiblen Rendering-Modells. Dieses Mal werden wir von theoretischen Berechnungen zu einer bestimmten Renderimplementierung übergehen, wobei direkte (analytische) Lichtquellen beteiligt sind: Punkt-, Richtungs- oder Scheinwerfertyp.


Aktualisieren wir zunächst den Ausdruck für die Berechnung des Reflexionsvermögens aus der vorherigen Lektion:

Lo(p, omegao)= int begrenzt Omega(kd fracc pi+ fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai


Zum größten Teil haben wir uns bereits mit den Komponenten dieser Formel befasst, aber es bleibt die Frage, wie die Bestrahlungsstärke , dh die Gesamtenergiehelligkeit ( Strahlung ), spezifisch dargestellt werden kann. Ldie ganze Szene. Wir waren uns einig, dass die Energie Helligkeit L(in Bezug auf die Computergrafik-Terminologie) wird als Verhältnis des Strahlungsflusses ( Strahlungsfluss ) betrachtet.  phi(Strahlungsenergie der Lichtquelle) auf den Wert des Raumwinkels  omega. In unserem Fall der Raumwinkel  omegaWir haben es für infinitesimal gehalten, und daher gibt die Energiehelligkeit eine Vorstellung vom Strahlungsfluss für jeden einzelnen Lichtstrahl (seine Richtung).

Wie können diese Berechnungen mit dem Beleuchtungsmodell verknüpft werden, das wir aus früheren Lektionen kennen? Stellen Sie sich zunächst vor, Sie erhalten eine Einzelpunktlichtquelle (die gleichmäßig in alle Richtungen emittiert) mit einem Strahlungsfluss, der als RGB-Triade definiert ist (23.47, 21.31, 20.79). Die Strahlungsintensität einer solchen Quelle entspricht ihrem Strahlungsfluss in alle Richtungen. Berücksichtigt man jedoch das Problem der Bestimmung der Farbe eines bestimmten Punktes pAuf der Oberfläche können Sie alle möglichen Richtungen des Lichteinfalls in der Hemisphäre erkennen  Omegaeinziger Vektor wiwird offensichtlich von einer Lichtquelle kommen. Da nur eine Lichtquelle dargestellt wird, dargestellt durch einen Punkt im Raum, für alle anderen möglichen Richtungen des Lichteinfalls bei pDie Energiehelligkeit ist gleich Null:

Wenn wir nun das Gesetz der Lichtdämpfung für eine bestimmte Quelle nicht vorübergehend berücksichtigen, stellt sich heraus, dass die Energiehelligkeit für den einfallenden Lichtstrahl dieser Quelle überall dort unverändert bleibt, wo wir die Quelle platzieren (Leuchtkraftskalierung basierend auf dem Kosinus des Einfallswinkels)  phizählt auch nicht). Insgesamt hält eine Punktquelle die Strahlungsstärke unabhängig vom Betrachtungswinkel konstant, was gleichbedeutend damit ist, die Strahlungsstärke gleich dem anfänglichen Strahlungsfluss in Form einer Triadenkonstante (23,47, 21,31, 20,79) zu nehmen.

Die Berechnung der Energiehelligkeit basiert jedoch auch auf der Koordinate des Punktes pZumindest jede physikalisch zuverlässige Lichtquelle zeigt die Dämpfung der Strahlungskraft mit zunehmendem Abstand von einem Punkt zu einer Quelle. Sie sollten auch die Ausrichtung der Oberfläche berücksichtigen, wie aus dem ursprünglichen Ausdruck für die Leuchtkraft hervorgeht: Das Ergebnis der Berechnung der Strahlungskraft muss mit dem Skalarwert des Normalenvektors zur Oberfläche multipliziert werden nund Strahlungseinfallsvektor wi.

Um das Obige umzuschreiben: Für eine direkte Punktlichtquelle gilt die Strahlungsfunktion Lbestimmt die Farbe des einfallenden Lichts unter Berücksichtigung der Dämpfung in einem bestimmten Abstand vom Punkt pund Berücksichtigung der Skalierung um einen Faktor n cdotwiaber nur für einen einzigen Lichtstrahl wiauf den Punkt kommen p- im Wesentlichen der einzige Vektor, der die Quelle und den Punkt verbindet. In Form von Quellcode wird dies wie folgt interpretiert:

vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta; 

Wenn Sie Ihre Augen vor einer leicht modifizierten Terminologie schließen, sollte dieser Code Sie an etwas erinnern. Ja, ja, dies ist alles der gleiche Code für die Berechnung der diffusen Komponente in dem uns bekannten Beleuchtungsmodell. Für die direkte Beleuchtung wird die Energiehelligkeit durch einen einzelnen Vektor für die Lichtquelle bestimmt, da die Berechnung auf eine Weise durchgeführt wird, die der uns noch bekannten ähnlich ist.

Ich stelle fest, dass diese Aussage nur unter der Annahme wahr ist, dass eine Punktlichtquelle infinitesimal ist und durch einen Punkt im Raum dargestellt wird. Bei der Modellierung einer Volumenquelle unterscheidet sich ihre Leuchtkraft in vielen Richtungen von Null und nicht nur in einem Strahl.

Für andere Lichtquellen, die Strahlung von einem einzelnen Punkt emittieren, wird die Energiehelligkeit auf die gleiche Weise berechnet. Beispielsweise hat eine gerichtete Lichtquelle eine konstante Richtung wiund verwendet keine Dämpfung, und die Projektionsquelle zeigt abhängig von der Richtung der Quelle eine unterschiedliche Strahlungsleistung.

Hier kehren wir zum Wert des Integrals zurück  intauf der Oberfläche der Hemisphäre  Omega. Da wir die Positionen aller Lichtquellen, die an der Schattierung eines bestimmten Punktes beteiligt sind, im Voraus kennen, müssen wir nicht versuchen, das Integral zu lösen. Wir können die Gesamtbestrahlung dieser Anzahl von Lichtquellen direkt berechnen, da die Energiehelligkeit der Oberfläche für jede Quelle durch eine einzige Richtung beeinflusst wird.

Infolgedessen ist die PBR-Berechnung für direkte Lichtquellen eine ziemlich einfache Angelegenheit, da alles auf eine sequentielle Suche nach den an der Beleuchtung beteiligten Quellen hinausläuft. Später wird eine Komponente aus der Umgebung im Beleuchtungsmodell angezeigt, an der wir im Tutorial zur bildbasierten Beleuchtung ( Image-Based Lighting , IBL ) arbeiten werden. Der Schätzung des Integrals kann man sich nicht entziehen, da das Licht in einem solchen Modell aus vielen Richtungen fällt.

PBR-Oberflächenmodell


Beginnen wir mit dem Fragment-Shader, der das oben beschriebene PBR-Modell implementiert. Zunächst legen wir die für die Oberflächenschattierung erforderlichen Eingabedaten fest:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; 

Hier sehen Sie die übliche Eingabe, die mit dem einfachsten Vertex-Shader berechnet wurde, sowie eine Reihe von Uniformen, die die Oberflächeneigenschaften des Objekts beschreiben.

Außerdem führen wir ganz am Anfang des Shader-Codes Berechnungen durch, die aus der Implementierung des Blinn-Fong-Beleuchtungsmodells so vertraut sind:

 void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] } 

Direkte Beleuchtung


Das Beispiel für diese Lektion enthält nur vier Punktlichtquellen, die die Bestrahlung der Szene klar spezifizieren. Um den Ausdruck des Reflexionsvermögens zu befriedigen, gehen wir iterativ jede Lichtquelle durch, berechnen die individuelle Energiehelligkeit und fassen diesen Beitrag zusammen, wobei wir gleichzeitig den BRDF-Wert und den Einfallswinkel des Lichtstrahls modulieren. Sie können sich diese Iteration als Lösung des Integrals über der Oberfläche vorstellen  OmegaNur für analytische Lichtquellen.

Also berechnen wir zuerst die berechneten Werte für jede Quelle:

 vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...] 

Da die Berechnungen im linearen Raum durchgeführt werden (die Gammakorrektur wird am Ende des Shaders durchgeführt), wird ein physikalisch korrekteres Dämpfungsgesetz gemäß dem umgekehrten Quadrat der Entfernung verwendet:

Angenommen, das Gesetz des inversen Quadrats ist physikalisch korrekter. Um die Art der Dämpfung besser steuern zu können, ist es durchaus möglich, die bereits bekannte Formel zu verwenden, die konstante, lineare und quadratische Terme enthält.

Außerdem berechnen wir für jede Quelle den Wert des Spiegels Cook-Torrance BRDF:

 fracDFG4( omegao cdotn)( omegai cdotn)


Der erste Schritt besteht darin, das Verhältnis zwischen spiegelnder und diffuser Reflexion oder mit anderen Worten das Verhältnis zwischen der Menge des reflektierten Lichts und der Menge des von der Oberfläche gebrochenen Lichts zu berechnen. Aus der vorherigen Lektion wissen wir, wie die Berechnung des Fresnel-Koeffizienten aussieht:

 vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); } 

Die Fresnel-Schlick-Näherung erwartet den Parameter F0 am Eingang, der den Grad der Oberflächenreflexion bei einem Lichteinfallswinkel von Null zeigt, d.h. Reflexionsgrad, wenn Sie die Oberfläche entlang der Normalen von oben nach unten betrachten. Der Wert von F0 variiert je nach Material und erhält einen Farbstich für Metalle, wie aus den Katalogen der PBR-Materialien hervorgeht. Für den metallischen Workflow- Prozess (Authoring-Prozess von PBR-Materialien, Unterteilung aller Materialien in Klassen von Dielektrika und Leitern) wird angenommen, dass alle Dielektrika bei einem konstanten Wert von F0 = 0,04 ziemlich zuverlässig aussehen, während für Metalloberflächen F0 basierend auf der Oberflächenalbedo festgelegt wird. In Form von Code:

 vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); 

Wie Sie sehen können, wird F0 für streng nichtmetallische Oberflächen gleich 0,04 gesetzt. Gleichzeitig kann es jedoch aufgrund der „Metallizität“ der Oberfläche reibungslos von diesem Wert zum Albedowert wechseln. Dieser Indikator wird normalerweise als separate Textur dargestellt (von hier aus wird tatsächlich ein metallischer Workflow durchgeführt, ca. Trans. ).

Erhalten FWir müssen den Wert der Normalverteilungsfunktion berechnen Dund Geometriefunktionen G::

Funktionscode für den Fall mit analytischer Beleuchtung:

 float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / denom; } float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } 

Ein wichtiger Unterschied zu dem im theoretischen Teil beschriebenen : Hier übergeben wir den Rauheitsparameter direkt an alle genannten Funktionen. Dies geschieht, damit jede Funktion den ursprünglichen Rauheitswert auf ihre eigene Weise ändern kann. Zum Beispiel haben Disney-Studien, die sich in der Engine von Epic Games widerspiegeln, gezeigt, dass das Beleuchtungsmodell visuell korrektere Ergebnisse liefert, wenn wir das Quadrat der Rauheit in der Geometriefunktion und der Normalverteilungsfunktion verwenden.

Wenn Sie alle Funktionen eingestellt haben, können Sie direkt die Werte von NDF und G erhalten:

 float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); 

Insgesamt haben wir alle Werte zur Berechnung des gesamten Cook-Torrance BRDF zur Hand:

 vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator; 

Bitte beachten Sie, dass wir den Nenner auf einen Mindestwert von 0,001 beschränken, um eine Division durch Null zu verhindern, wenn das Skalarprodukt auf Null gesetzt wird.

Nun berechnen wir den Beitrag jeder Quelle zur Reflektivitätsgleichung. Da der Fresnel-Koeffizient direkt eine Variable ist KsDann können wir den Wert von F verwenden, um den Beitrag der Quelle zur Spiegelreflexion der Oberfläche anzuzeigen. Aus der Menge Kserhalten werden kann und der Brechungsindex Kd::

 vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; 

Da wir die Größe kS, die die Menge der Lichtenergie darstellt, als reflektierte Oberfläche betrachten und von der Einheit subtrahieren, erhalten wir die Restenergie des von der Oberfläche gebrochenen Lichts kD . Da Metalle kein Licht brechen und keine diffuse Komponente von wieder emittiertem Licht aufweisen, wird die Komponente kD für ein Ganzmetallmaterial auf Null moduliert. Nach diesen Berechnungen stehen uns alle Daten zur Verfügung, um das Reflexionsvermögen jeder Lichtquelle zu berechnen:

 const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } 

Der Endwert Lo oder die Helligkeit der ausgehenden Energie ist im Wesentlichen eine Lösung für den Ausdruck des Reflexionsvermögens, d.h. Ergebnis der Oberflächenintegration  Omega. In diesem Fall müssen wir nicht versuchen, das Integral in einer allgemeinen Form für alle möglichen Richtungen zu lösen, da in diesem Beispiel nur vier Lichtquellen das zu verarbeitende Fragment beeinflussen. Deshalb beschränkt sich jede „Integration“ auf einen einfachen Zyklus vorhandener Lichtquellen.

Es bleibt nur die Ähnlichkeit der Hintergrundbeleuchtungskomponente mit den Ergebnissen der Berechnung der direkten Lichtquelle hinzuzufügen, und die endgültige Farbe des Fragments ist fertig:

 vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; 

Lineares Rendering und HDR


Bisher gingen wir davon aus, dass alle Berechnungen in einem linearen Farbraum durchgeführt werden, und verwendeten daher die Gammakorrektur als Endakkord in unserem Shader. Die Durchführung von Berechnungen im linearen Raum ist für die korrekte Simulation von PBR äußerst wichtig, da das Modell die Linearität aller Eingabedaten erfordert. Stellen Sie sicher, dass die Linearität der Parameter nicht gewährleistet ist und das Schattierungsergebnis falsch ist. Darüber hinaus wäre es schön, die Lichtquellen mit Eigenschaften einzustellen, die denen realer Quellen nahe kommen: Beispielsweise können die Farbe ihrer Strahlung und die Energiehelligkeit über einen weiten Bereich frei variieren. Infolgedessen kann Lo recht leicht große Werte akzeptieren, fällt jedoch aufgrund des niedrigen Dynamikbereichs ( LDR ) des Standard- Bildpuffers unweigerlich unter den Grenzwert im Intervall [0., 1.].

Um den Verlust von HDR-Werten zu vermeiden, muss vor der Gammakorrektur eine Tonkomprimierung durchgeführt werden:

 color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); 

Hier wird der bekannte Reinhardt-Operator verwendet, der es uns ermöglicht, einen weiten Dynamikbereich unter Bedingungen einer sich stark ändernden Bestrahlung verschiedener Teile des Bildes aufrechtzuerhalten. Da wir hier keinen separaten Shader für die Nachbearbeitung verwenden, können die beschriebenen Operationen einfach am Ende des Shader-Codes hinzugefügt werden.


Ich wiederhole, dass es für die korrekte Modellierung von PBR äußerst wichtig ist, die Merkmale der Arbeit mit linearem Farbraum und HDR-Rendering zu berücksichtigen und zu berücksichtigen. Die Vernachlässigung dieser Aspekte führt zu falschen Berechnungen und visuell unästhetischen Ergebnissen.

PBR-Shader für analytische Beleuchtung


Zusammen mit dem letzten Schliff in Form von Tonkomprimierung und Gammakorrektur bleibt also nur die endgültige Farbe des Fragments auf die Ausgabe des Fragment-Shaders zu übertragen, und der PBR-Shader-Code für die direkte Beleuchtung kann als abgeschlossen betrachtet werden. Lassen Sie uns abschließend den gesamten Code der main () - Funktion dieses Shaders betrachten:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; //   uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; //   uniform vec3 lightPositions[4]; uniform vec3 lightColors[4]; uniform vec3 camPos; const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness); float GeometrySchlickGGX(float NdotV, float roughness); float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness); vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness); void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); //    vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { //        vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; // Cook-Torrance BRDF float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); vec3 specular = numerator / max(denominator, 0.001); //       Lo float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0); } 

Ich hoffe, dass diese Auflistung nach dem Lesen des theoretischen Teils und der heutigen Analyse des Ausdrucks der Reflexionsfähigkeit nicht mehr einschüchternd wirkt.

Wir verwenden diesen Shader in einer Szene mit vier Punktlichtquellen, einer bestimmten Anzahl von Kugeln, deren Oberflächeneigenschaften den Grad ihrer Rauheit und Metallizität entlang der horizontalen bzw. vertikalen Achse ändern. Am Ausgang erhalten wir folgendes Bild:


Die Metallizität ändert sich von unten nach oben von null auf eins, und die Rauheit ist ähnlich, jedoch von links nach rechts. Es zeigt sich, dass durch die Änderung nur dieser beiden Oberflächeneigenschaften bereits eine Vielzahl von Materialien eingestellt werden kann.

Der vollständige Quellcode ist hier .

PBR und Texturierung


Wir werden unser Oberflächenmodell erweitern, indem wir Eigenschaften in Form von Texturen übertragen. Auf diese Weise können wir die Parameter des Oberflächenmaterials pro Fragment steuern:

 [...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] } 

Beachten Sie, dass die Oberflächenalbedotextur normalerweise von Künstlern im sRGB-Farbraum erstellt wird. Im obigen Code geben wir die Texelfarbe in den linearen Raum zurück, damit sie für weitere Berechnungen verwendet werden kann. Abhängig davon, wie die Künstler die Textur erstellen, die die Umgebungsokklusionskartendaten enthält, muss sie möglicherweise auch in den linearen Raum gebracht werden. Metallizitäts- und Rauheitskarten werden fast immer im linearen Raum erstellt.

Die Verwendung von Texturen anstelle von festen Oberflächenparametern in Kombination mit dem PBR-Algorithmus führt zu einer signifikanten Erhöhung der visuellen Zuverlässigkeit im Vergleich zu zuvor verwendeten Beleuchtungsalgorithmen:


Der vollständige Code für die Texturierung ist hier und die verwendeten Texturen sind hier (zusammen mit der Hintergrundschattierungstextur). Ich mache Sie darauf aufmerksam, dass stark metallische Oberflächen unter direkten Lichtverhältnissen abgedunkelt erscheinen, da der Beitrag der diffusen Reflexion gering ist (im Grenzbereich gibt es überhaupt keinen). Ihre Schattierung wird erst korrekter, wenn die Spiegelreflexion der Beleuchtung aus der Umgebung berücksichtigt wird, die wir in den nächsten Lektionen durchführen werden.

Derzeit ist das Ergebnis möglicherweise nicht so beeindruckend wie bei einigen PBR-Demonstrationen. Wir haben jedoch noch kein bildbasiertes Beleuchtungssystem ( IBL ) implementiert. Trotzdem wird unser Rendering jetzt als auf physikalischen Prinzipien basierend betrachtet und zeigt auch ohne IBL ein Bild, das zuverlässiger ist als zuvor.

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


All Articles