
Beleuchtung basierend auf dem Bild oder
IBL (
Image Based Lighting ) ist eine Kategorie von Beleuchtungsmethoden, die nicht auf der Berücksichtigung analytischer Lichtquellen (in der
vorherigen Lektion beschrieben ) basieren, sondern die gesamte Umgebung beleuchteter Objekte als eine kontinuierliche Lichtquelle betrachten. Im Allgemeinen liegt die technische Grundlage solcher Methoden in der Verarbeitung einer kubischen Karte der Umgebung (in der realen Welt erstellt oder auf der Grundlage einer dreidimensionalen Szene erstellt), sodass die in der Karte gespeicherten Daten direkt für Beleuchtungsberechnungen verwendet werden können: Tatsächlich wird jedes Texel der kubischen Karte als Lichtquelle betrachtet . Im Allgemeinen können Sie so den Effekt der globalen Beleuchtung in der Szene erfassen. Dies ist eine wichtige Komponente, die den gesamten "Ton" der aktuellen Szene vermittelt und dazu beiträgt, dass die beleuchteten Objekte besser in sie "eingebettet" werden.
Da IBL-Algorithmen die Beleuchtung aus einer bestimmten „globalen“ Umgebung berücksichtigen, wird ihr Ergebnis als genauere Simulation der Hintergrundbeleuchtung oder sogar als sehr grobe Annäherung an die globale Beleuchtung angesehen. Dieser Aspekt macht IBL-Methoden im Hinblick auf die Integration in das PBR-Modell interessant, da die Verwendung von Umgebungslicht im Beleuchtungsmodell es Objekten ermöglicht, physikalisch viel korrekter auszusehen.
Um den Einfluss von IBL in das bereits beschriebene PBR-System einzubeziehen, kehren wir zur bekannten Reflexionsgleichung zurück:
Lo(p, omegao)= int begrenzt Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Wie bereits beschrieben, besteht das Hauptziel darin, das Integral für alle einfallenden Strahlungsrichtungen zu berechnen
wi Hemisphäre
Omega . In der
letzten Lektion war die Berechnung des Integrals nicht lästig, da wir die Anzahl der Lichtquellen und damit all die verschiedenen Richtungen des Lichteinfalls, die ihnen entsprechen, im Voraus kannten. Gleichzeitig kann das Integral nicht mit einem Schnappschuss gelöst werden:
einem fallenden Vektor
wi aus der Umgebung kann Energie ungleich Null Helligkeit tragen. Für die praktische Anwendbarkeit des Verfahrens ist es daher erforderlich, die folgenden Anforderungen zu erfüllen:
- Sie müssen einen Weg finden, um die Energiehelligkeit der Szene für einen beliebigen Richtungsvektor zu erhalten wi ;;
- Es ist notwendig, dass die Lösung des Integrals in Echtzeit erfolgen kann.
Nun, der erste Punkt ist von selbst gelöst. Ein Hinweis auf eine Lösung ist hier bereits verrutscht: Eine der Methoden zur Darstellung der Bestrahlung einer Szene oder Umgebung ist eine kubische Karte, die einer speziellen Verarbeitung unterzogen wurde. Jedes Texel in einer solchen Karte kann als separate emittierende Quelle betrachtet werden. Durch Abtasten von einer solchen Karte gemäß einem beliebigen Vektor
wi Wir erhalten leicht die Energiehelligkeit der Szene in diese Richtung.
Wir erhalten also die Energiehelligkeit der Szene für einen beliebigen Vektor
wi ::
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
Bemerkenswerterweise müssen wir zum Lösen des Integrals jedoch Proben aus der Umgebungskarte nicht aus einer Richtung, sondern aus allen möglichen Bereichen der Hemisphäre erstellen. Und so - für jedes schattierte Fragment. Offensichtlich ist dies für Echtzeitaufgaben praktisch nicht praktikabel. Eine effektivere Methode wäre, einen Teil der Integrandenoperationen auch außerhalb unserer Anwendung im Voraus zu berechnen. Aber dafür müssen Sie die Ärmel hochkrempeln und tiefer in die Essenz des Ausdrucks des Reflexionsvermögens eintauchen:
Lo(p, omegao)= int begrenzt Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Es ist ersichtlich, dass sich die Teile des Ausdrucks auf das Diffuse beziehen
kd und Spiegel
ks BRDF-Komponenten sind unabhängig. Sie können das Integral in zwei Teile teilen:
Lo(p, omegao)= int Grenzen Omega(kd fracc pi)Li(p, omegai)n cdot omegaid omegai+ int Grenzen Omega(ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Eine solche Aufteilung in Teile ermöglicht es uns, jeden einzelnen einzeln zu behandeln, und in dieser Lektion werden wir uns mit dem Teil befassen, der für die diffuse Beleuchtung verantwortlich ist.
Nachdem wir die Form des Integrals über der diffusen Komponente analysiert haben, können wir schließen, dass die diffuse Lambert-Komponente im Wesentlichen konstant ist (Farbe)
s Brechungsindex
kd und
pi sind unter den Bedingungen des Integranden konstant) und hängen nicht von anderen Variablen ab. Angesichts dieser Tatsache können wir die Konstanten über das Vorzeichen des Integrals hinaus setzen:
Lo(p, omegao)=kd fracc pi int begrenzt OmegaLi(p, omegai)n cdot omegaid omegai
So bekommen wir ein Integral nur abhängig von
wi (Es wird angenommen, dass
p entspricht dem Zentrum der kubischen Karte der Umgebung). Basierend auf dieser Formel können Sie eine neue kubische Karte berechnen oder noch besser vorberechnen, in der das Ergebnis der Berechnung des Integrals der diffusen Komponente für jede Richtung der Stichprobe (oder Texelkarte) gespeichert ist.
wo unter Verwendung der Faltungsoperation.
Faltung ist die Operation, bei der auf jedes Element in einem Datensatz eine Berechnung angewendet wird, wobei die Daten aller anderen Elemente in dem Datensatz berücksichtigt werden. In diesem Fall sind solche Daten die Energiehelligkeit der Szene oder der Umgebungskarte. Um einen Wert in jeder Richtung der Probe in der kubischen Karte zu berechnen, müssen wir die Werte berücksichtigen, die aus allen anderen möglichen Richtungen der Probe in der um den Probenpunkt liegenden Halbkugel entnommen wurden.
Um die Umgebungskarte zu falten, müssen Sie das Integral für jede resultierende Richtung der Probe lösen
wo durch Durchführen mehrerer diskreter Abtastwerte entlang von Richtungen
wi Zugehörigkeit zur Hemisphäre
Omega und Mitteln der Gesamtenergiehelligkeit. Die Hemisphäre, auf deren Grundlage die Probenahmerichtungen genommen werden
wi entlang des Vektors orientiert
wo Darstellen der Zielrichtung, für die die aktuelle Faltung berechnet wird. Schauen Sie sich das Bild zum besseren Verständnis an:
Eine solche vorberechnete kubische Karte, die das Integrationsergebnis für jede Richtung der Probe speichert
wo kann auch als Speicherung des Ergebnisses der Summierung der gesamten indirekten diffusen Beleuchtung in der Szene betrachtet werden, die auf eine bestimmte Oberfläche fällt, die entlang der Richtung ausgerichtet ist
wo . Mit anderen Worten, solche kubischen Karten werden Bestrahlungsstärkenkarten genannt, da Sie mit der kubischen Umgebungskarte vor der Faltung die Bestrahlungsmenge der Szene aus einer beliebigen Richtung direkt abtasten können
wo ohne zusätzliche Berechnungen.
Der Ausdruck, der die Energiehelligkeit bestimmt, hängt auch von der Position des Abtastpunkts ab p was wir genau in der Mitte der Bestrahlungskarte liegend genommen haben. Diese Annahme führt zu einer Einschränkung in dem Sinne, dass die Quelle aller indirekten diffusen Beleuchtung auch eine einzige Umgebungskarte sein wird. In Szenen mit heterogener Beleuchtung kann dies die Illusion der Realität zerstören (insbesondere in Innenszenen). Moderne Rendering-Engines lösen dieses Problem, indem sie spezielle Hilfsobjekte in die Szenenreflexionssonden einfügen . Jedes dieser Objekte hat eine Aufgabe: Es bildet eine eigene Bestrahlungskarte für seine unmittelbare Umgebung. Bei dieser Technik erfolgt die Bestrahlung (und Energiehelligkeit) an einem beliebigen Punkt p wird durch einfache Interpolation zwischen den nächsten Reflexionsproben bestimmt. Für aktuelle Aufgaben sind wir uns jedoch einig, dass die Umgebungskarte von ihrem Zentrum aus abgetastet wird, und wir werden in weiteren Lektionen Reflexionsmuster analysieren.
Unten finden Sie ein Beispiel für eine kubische Karte der Umgebung und eine daraus abgeleitete Bestrahlungskarte (basierend auf der
Wellenmaschine ), die die Energiehelligkeit der Umgebung für jede Ausgangsrichtung mittelt
wo .
Diese Karte speichert also das Faltungsergebnis in jedem Texel (entsprechend der Richtung)
wo ), und äußerlich sieht eine solche Karte so aus, als würde sie die durchschnittliche Farbe der Umgebungskarte speichern. Eine Probe in einer beliebigen Richtung aus einer solchen Karte gibt den Wert der aus dieser Richtung ausgehenden Bestrahlung zurück.
PBR und HDR
In der
vorherigen Lektion wurde bereits kurz darauf hingewiesen, dass es für den korrekten Betrieb des PBR-Beleuchtungsmodells äußerst wichtig ist, den HDR-Helligkeitsbereich der vorhandenen Lichtquellen zu berücksichtigen. Da das PBR-Modell am Eingang Parameter auf die eine oder andere Weise akzeptiert, die auf sehr spezifischen physikalischen Größen und Eigenschaften basieren, ist es logisch, dass die Energiehelligkeit der Lichtquellen mit ihren tatsächlichen Prototypen übereinstimmt. Es spielt keine Rolle, wie wir den spezifischen Wert des Strahlungsflusses für jede Quelle rechtfertigen: Machen Sie eine grobe technische Schätzung oder wenden Sie sich
physikalischen Größen zu - der Unterschied in den Eigenschaften zwischen einer Raumlampe und der Sonne wird in jedem Fall enorm sein. Ohne die Verwendung des
HDR- Bereichs ist es einfach unmöglich, die relative Helligkeit einer Vielzahl von Lichtquellen genau zu bestimmen.
PBR und HDR sind also für immer Freunde, das ist verständlich, aber wie hängt diese Tatsache mit bildbasierten Beleuchtungsmethoden zusammen? In der letzten Lektion wurde gezeigt, dass die Konvertierung von PBR in den HDR-Rendering-Bereich einfach ist. Es bleibt ein „aber“: Da die indirekte Beleuchtung aus der Umgebung auf einer kubischen Karte der Umgebung basiert, ist ein Weg erforderlich, um die HDR-Eigenschaften dieser Hintergrundbeleuchtung in der Umgebungskarte beizubehalten.
Bisher haben wir Umgebungskarten verwendet, die im LDR-Format erstellt wurden (z. B.
Skyboxes ). Wir haben das Farbmuster von ihnen beim Rendern so wie es ist verwendet und dies ist für die direkte Schattierung von Objekten durchaus akzeptabel. Und es ist völlig ungeeignet, wenn Umgebungskarten als Quellen für physikalisch zuverlässige Messungen verwendet werden.
RGBE - HDR-Bildformat
Machen Sie sich mit dem RGBE-Bilddateiformat vertraut. Dateien mit der Erweiterung "
.hdr " werden zum Speichern von Bildern mit einem großen Dynamikbereich verwendet, wobei jedem Element der
Farbtriade ein Byte und dem gemeinsamen Exponenten ein weiteres Byte zugewiesen wird. Das Format ermöglicht es Ihnen auch, kubische Umgebungskarten mit einem Farbintensitätsbereich außerhalb des LDR-Bereichs [0., 1.] zu speichern. Dies bedeutet, dass Lichtquellen ihre tatsächliche Intensität beibehalten können, die durch eine solche Umgebungskarte dargestellt wird.
Das Netzwerk verfügt über eine Vielzahl kostenloser Umgebungskarten im RGBE-Format, die unter verschiedenen realen Bedingungen aufgenommen wurden. Hier ist ein Beispiel von der
sIBL-Archivseite :
Sie werden überrascht sein, was Sie gesehen haben: Schließlich sieht dieses verzerrte Bild überhaupt nicht aus wie eine normale kubische Karte mit ihrer ausgeprägten Aufteilung in 6 Gesichter. Die Erklärung ist einfach: Diese Karte der Umgebung wurde von einer Kugel auf eine Ebene projiziert - ein
gleich rechteckiger Scan wurde angewendet. Dies geschieht, um in einem Format speichern zu können, das den Speichermodus von kubischen Karten nicht unterstützt. Natürlich hat diese Projektionsmethode ihre Nachteile: Die horizontale Auflösung ist viel höher als die vertikale. In den meisten Fällen der Anwendung beim Rendern ist dies ein akzeptables Verhältnis, da sich normalerweise interessante Details der Umgebung und der Beleuchtung genau in der horizontalen Ebene und nicht in der vertikalen Ebene befinden. Nun, zu allem brauchen wir den Konvertierungscode zurück zur kubischen Karte.
Unterstützung für das RGBE-Format in stb_image.h
Das Herunterladen dieses Bildformats auf eigene Faust erfordert Kenntnisse
der Formatspezifikation , die nicht schwierig, aber dennoch mühsam ist. Zum Glück unterstützt die in einer einzelnen Header-Datei implementierte Bildladebibliothek stb_image.h das Laden von RGBE-Dateien und die Rückgabe eines Arrays von Gleitkommazahlen - was wir für unsere Zwecke benötigen! Das Hinzufügen einer Bibliothek zu Ihrem Projekt und das Laden von Bilddaten ist äußerst einfach:
#include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; }
Die Bibliothek konvertiert automatisch Werte aus dem internen HDR-Format in reguläre echte 32-Bit-Zahlen mit standardmäßig drei Farbkanälen. Es reicht aus, die Daten des ursprünglichen HDR-Bildes in einer normalen 2D-Gleitkomma-Textur zu speichern.
Konvertieren Sie einen Scan mit gleichem Winkel in eine kubische Karte
Ein ebenso rechteckiger Scan kann verwendet werden, um Proben direkt aus der Umgebungskarte auszuwählen. Dies würde jedoch teure mathematische Operationen erfordern, während das Abrufen von einer normalen kubischen Karte praktisch frei von Leistung wäre. Genau aus diesen Überlegungen heraus werden wir uns in dieser Lektion mit der Umwandlung eines ebenso rechteckigen Bildes in eine kubische Karte befassen, die später verwendet wird. Hier wird jedoch auch die direkte Abtastmethode aus einer ebenso rechteckigen Karte unter Verwendung eines dreidimensionalen Vektors gezeigt, damit Sie die für Sie geeignete Arbeitsmethode auswählen können.
Zum Konvertieren müssen Sie einen Würfel in Einheitsgröße zeichnen, ihn von innen betrachten, eine gleich rechteckige Karte auf seine Flächen projizieren und dann sechs Bilder aus den Flächen als Flächen der kubischen Karte extrahieren. Der Scheitelpunkt-Shader dieser Stufe ist recht einfach: Er verarbeitet einfach die Scheitelpunkte des Würfels wie sie sind und übergibt ihre nicht reformierten Positionen zur Verwendung als dreidimensionaler Beispielvektor an den Fragment-Shader:
#version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); }
Im Fragment-Shader schattieren wir jede Seite des Würfels, als wollten wir den Würfel vorsichtig mit einem Blatt mit einer ebenso rechteckigen Karte umwickeln. Dazu wird die auf den Fragment-Shader übertragene Abtastrichtung genommen, durch spezielle trigonometrische Magie verarbeitet, und schließlich wird die Auswahl aus einer gleich rechteckigen Karte getroffen, als wäre es tatsächlich eine kubische Karte. Das Auswahlergebnis wird direkt als Farbe des Fragments der Würfelfläche gespeichert:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); }
Wenn Sie mit diesem Shader und einer zugehörigen HDR-Umgebungskarte tatsächlich einen Würfel zeichnen, erhalten Sie Folgendes:
Das heißt, Es ist zu sehen, dass wir tatsächlich eine rechteckige Textur auf einen Würfel projiziert haben. Großartig, aber wie hilft uns das bei der Erstellung einer echten kubischen Karte? Um diese Aufgabe zu beenden, muss derselbe Würfel sechsmal mit einer Kamera gerendert werden, die auf jedes der Gesichter schaut, während die Ausgabe in ein separates
Bildpufferobjekt geschrieben wird:
unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
Natürlich werden wir nicht vergessen, den Speicher für die Speicherung der sechs Gesichter der zukünftigen kubischen Karte zu organisieren:
unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) {
Nach dieser Vorbereitung muss nur noch die Übertragung von Teilen einer gleich rechteckigen Karte direkt am Rande einer kubischen Karte durchgeführt werden.
Wir werden nicht zu sehr ins Detail gehen, zumal der Code viel wiederholt, was in den Lektionen über den
Bildspeicher und die
omnidirektionalen Schatten zu sehen ist . Im Prinzip kommt es darauf an, sechs separate Ansichtsmatrizen vorzubereiten, die die Kamera streng auf jede der Würfelflächen ausrichten, sowie eine spezielle Projektionsmatrix mit einem Blickwinkel von 90 °, um die gesamte Fläche des Würfels zu erfassen. Dann wird nur sechsmal gerendert und das Ergebnis in einem Gleitkomma-Framebuffer gespeichert:
glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); glm::mat4 captureViews[] = { glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) };
Hier wird die Farbe des Bildpuffers angehängt und abwechselnd die verbundene Fläche der kubischen Karte geändert, was zur direkten Ausgabe des Renderings auf eine der Flächen der Umgebungskarte führt. Dieser Code muss nur einmal ausgeführt werden. Danach
haben wir
noch eine vollständige
envCubemap- Umgebungskarte, die das Ergebnis der Konvertierung der ursprünglichen
gleichrechteckigen Version der HDR-Umgebungskarte enthält.
Wir werden die resultierende kubische Karte testen, indem wir den einfachsten Skybox-Shader skizzieren:
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; // mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; }
Achten Sie auf den Trick mit den Komponenten des
clipPos- Vektors: Wir verwenden die
xyww- Tetrade, wenn wir die transformierte Koordinate des Scheitelpunkts aufzeichnen, um sicherzustellen, dass alle Fragmente der Skybox eine maximale Tiefe von 1,0 haben (der Ansatz wurde bereits in der
entsprechenden Lektion verwendet ). Vergessen Sie nicht, die Vergleichsfunktion in
GL_LEQUAL zu
ändern :
glDepthFunc(GL_LEQUAL);
Der Fragment-Shader wählt einfach aus einer kubischen Karte aus:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); }
Die Auswahl aus der Karte basiert auf den interpolierten lokalen Koordinaten der Eckpunkte des Würfels, was in diesem Fall die richtige Richtung der Auswahl ist (wiederum in der Lektion über Skyboxen,
ca. Per. ). Da die Transportkomponenten in der Ansichtsmatrix ignoriert wurden, hängt das Rendern der Skybox nicht von der Position des Betrachters ab, wodurch die Illusion eines unendlich entfernten Hintergrunds entsteht. Da wir hier Daten direkt von der HDR-Karte an den Standard-Framebuffer ausgeben, der der LDR-Empfänger ist, muss die Tonkomprimierung abgerufen werden. Und schließlich werden fast alle HDR-Karten im linearen Raum gespeichert, was bedeutet, dass die
Gammakorrektur als endgültiger Verarbeitungsakkord angewendet werden muss.
Wenn also die erhaltene Skybox zusammen mit der bereits bekannten Anordnung von Kugeln ausgegeben wird, wird etwas Ähnliches erhalten:
Nun, es wurde viel Aufwand betrieben, aber am Ende haben wir uns erfolgreich daran gewöhnt, die HDR-Umgebungskarte zu lesen, sie von einer gleichseitigen in eine kubische Karte zu konvertieren und die kubische HDR-Karte als Skybox in der Szene auszugeben. Darüber hinaus ist der Code zum Konvertieren in eine kubische Karte durch Rendern auf sechs Seiten einer kubischen Karte für uns weiter nützlich bei der Aufgabe der
Faltung einer Umgebungskarte . Der Code für den gesamten Konvertierungsprozess ist
hier .
Faltung einer kubischen Karte
Wie zu Beginn der Lektion gesagt wurde, besteht unser Hauptziel darin, das Integral für alle möglichen Richtungen indirekter diffuser Beleuchtung zu lösen, wobei die gegebene Bestrahlung der Szene in Form einer kubischen Karte der Umgebung berücksichtigt wird. Es ist bekannt, dass wir den Wert der Energiehelligkeit der Szene erhalten können
L(p,wi) für beliebige Richtung
wi durch Abtasten einer kubischen Karte der Umgebung in dieser Richtung aus dem HDR. Um das Integral zu lösen, muss die Energiehelligkeit der Szene aus allen möglichen Richtungen in der Hemisphäre abgetastet werden
Omega jedes überprüfte Fragment.
Offensichtlich ist die Aufgabe, Licht aus der Umgebung aus allen möglichen Richtungen in der Hemisphäre abzutasten
Omega ist rechnerisch nicht praktikabel - es gibt unendlich viele solcher Richtungen. Es ist jedoch möglich, die Näherung anzuwenden, indem eine endliche Anzahl von Richtungen gewählt wird, die zufällig ausgewählt werden oder sich gleichmäßig innerhalb der Halbkugel befinden.
Auf diese Weise können wir eine ziemlich gute Annäherung an die tatsächliche Bestrahlung erhalten und das für uns interessante Integral im Wesentlichen in Form einer endlichen Summe lösen.Aber für Echtzeitaufgaben ist selbst ein solcher Ansatz immer noch unglaublich auferlegt, da die Proben für jedes Fragment entnommen werden und die Anzahl der Proben hoch genug sein muss, um ein akzeptables Ergebnis zu erzielen. Daher wäre es schön, die Daten für diesen Schritt außerhalb des Renderprozesses im Voraus vorzubereiten . Da die Ausrichtung der Halbkugel bestimmt, aus welchem Raumbereich wir die Bestrahlung erfassen, ist es möglich, die Bestrahlung für jede mögliche Ausrichtung der Halbkugel basierend auf allen möglichen Ausgangsrichtungen im Voraus zu berechnenw o ::
L o ( p , ω o ) = k d cπ ∫ΩLi(p,ωi)n⋅ωidωi
Als Ergebnis für einen gegebenen beliebigen Vektor w i können wir aus der berechneten Bestrahlungsstärkekarte abtasten, um die diffuse Bestrahlungsstärke in dieser Richtung zu erhalten. Um die Größe der indirekten diffusen Strahlung am Punkt des aktuellen Fragments zu bestimmen, nehmen wir die Gesamtbestrahlung von einer Hemisphäre, die entlang der Normalen zur Fragmentoberfläche ausgerichtet ist. Mit anderen Worten, um die Bestrahlung einer Szene zu erhalten, kommt es auf eine einfache Auswahl an: vec3 irradiance = texture(irradianceMap, N);
Um eine Bestrahlungskarte zu erstellen, muss die Umgebungskarte gefaltet und in eine kubische Karte konvertiert werden. Wir wissen, dass für jedes Fragment seine Halbkugel als entlang der Normalen zur Oberfläche ausgerichtet betrachtet wirdN. .
In diesem Fall wird die Faltung der kubischen Karte auf die Berechnung der durchschnittlichen Menge an Energiehelligkeit aus allen Richtungen reduziert w i innerhalb der HemisphäreΩ entlang der Normalen ausgerichtetN. ::
Glücklicherweise macht es die zeitaufwändige Vorarbeit, die wir zu Beginn der Lektion durchgeführt haben, jetzt ziemlich einfach, die Umgebungskarte in einem speziellen Fragment-Shader in eine kubische Karte umzuwandeln, deren Ausgabe zur Erstellung einer neuen kubischen Karte verwendet wird. Hierfür ist genau der Code nützlich, mit dem eine gleich rechteckige Umgebungskarte in eine kubische Karte übersetzt wurde.Es bleibt nur ein weiterer Verarbeitungs-Shader: #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { // vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] // FragColor = vec4(irradiance, 1.0); }
Hier ist der Umgebungskarten- Sampler eine kubische HDR-Karte der Umgebung, die zuvor aus einem Gleichseitigen abgeleitet wurde.Es gibt viele Möglichkeiten, die Umgebungskarte zu falten. In diesem Fall werden für jedes Texel der kubischen Karte mehrere Hemisphären-Probenvektoren generiertΩ , entlang der Richtung der Probe ausgerichtet, und mitteln Sie die Ergebnisse. Die Anzahl der Probenvektoren wird festgelegt und die Vektoren selbst werden gleichmäßig innerhalb der Hemisphäre verteilt. Ich stelle fest, dass der Integrand eine stetige Funktion ist und eine diskrete Schätzung dieser Funktion nur eine Annäherung ist. Und je mehr Abtastvektoren wir nehmen, desto näher sind wir der analytischen Lösung des Integrals. Der Integrand des Ausdrucks für das Reflexionsvermögen hängt vom Raumwinkel abd w - Werte, mit denen nicht sehr bequem gearbeitet werden kann. Anstatt über einen Raumwinkel zu integrierend w wir ändern den Ausdruck, was zur Integration über sphärische Koordinaten führtθ und
ϕ ::
Der Winkel Phi repräsentiert den Azimut in der Ebene der Basis der Hemisphäre und variiert von 0 bis 2 π .
Winkel θ repräsentiert den Höhenwinkel, der von 0 bis variiert12 π .
Der modifizierte Ausdruck für das Reflexionsvermögen in solchen Begriffen lautet wie folgt:L o ( p , ϕ o , θ o ) = k d cπ ∫ 2 π ϕ = 0 ∫ 12 πθ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ
Die Lösung eines solchen Integrals erfordert die Entnahme einer endlichen Anzahl von Proben in der Hemisphäre Ω und Mittelung der Ergebnisse. Die Anzahl der Proben kennenn 1 und
n 2 für jede der sphärischen Koordinaten können wir das Integral in dieRiemannsche Summe übersetzen:L o ( p , ϕ o , θ o ) = k d cπ 1n 1 n 2 n 1 ∑ ϕ=0 n 2 ∑ θ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ
Da beide sphärischen Koordinaten zu jedem Zeitpunkt diskret variieren, wird die Abtastung mit einem bestimmten gemittelten Bereich in der Hemisphäre durchgeführt, wie in der obigen Abbildung zu sehen ist. Aufgrund der Beschaffenheit der sphärischen Oberfläche nimmt die Größe des diskreten Abtastbereichs mit zunehmendem Elevationswinkel zwangsläufig abθ und Annäherung an den Zenit. Um diesen Effekt der Flächenreduzierung zu kompensieren, haben wir dem Ausdruck einen Gewichtskoeffizienten hinzugefügts i n θ .
Infolgedessen ist die Implementierung einer diskreten Abtastung in der Hemisphäre basierend auf sphärischen Koordinaten für jedes Fragment in Form von Code wie folgt: vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { // . ( -) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples));
Die Variable sampleDelta bestimmt die Größe des diskreten Schritts entlang der Oberfläche der Hemisphäre. Durch Ändern dieses Werts können Sie die Genauigkeit des Ergebnisses erhöhen oder verringern.Innerhalb beider Zyklen wird ein regulärer dreidimensionaler Probenvektor aus sphärischen Koordinaten gebildet, von der Tangente in den Weltraum übertragen und dann zum Abtasten einer kubischen Umgebungskarte aus dem HDR verwendet. Das Ergebnis der Proben wird in der Bestrahlungsstärkenvariablen akkumuliert , die am Ende der Verarbeitung durch die Anzahl der Proben dividiert wird, um einen durchschnittlichen Bestrahlungswert zu erhalten. Beachten Sie, dass das Ergebnis der Abtastung aus der Textur durch zwei Größen moduliert wird: cos (Theta) - um die Abschwächung des Lichts bei großen Winkeln zu berücksichtigen, und sin (Theta)- um die Verringerung der Probenfläche bei Annäherung an den Zenit auszugleichen.Es bleibt nur der Code zu behandeln, der die Ergebnisse der Faltung der envCubemap- Umgebungskarte rendert und erfasst . Erstellen Sie zunächst eine kubische Karte, um die Bestrahlung zu speichern (Sie müssen dies einmal tun, bevor Sie in den Haupt-Renderzyklus eintreten): unsigned int irradianceMap; glGenTextures(1, &irradianceMap); glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Da die Bestrahlungskarte durch Mittelung gleichmäßig verteilter Abtastwerte der Energiehelligkeit der Umgebungskarte erhalten wird, enthält sie praktisch keine hochfrequenten Teile und Elemente - eine Textur mit relativ kleiner Auflösung (hier 32 x 32) und eine aktivierte lineare Filterung reichen aus, um sie zu speichern.Stellen Sie als Nächstes den Capture-Framebuffer auf diese Auflösung ein: glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
Der Code zum Erfassen der Faltungsergebnisse ähnelt dem Code zum Übertragen einer Umgebungskarte von einer gleichseitigen zu einer kubischen, es wird nur ein Faltungs-Shader verwendet: irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
Nach Abschluss dieser Phase haben wir eine vorberechnete Bestrahlungskarte auf unseren Händen, die direkt zur Berechnung der indirekten diffusen Beleuchtung verwendet werden kann. Um zu überprüfen, wie die Faltung verlaufen ist, versuchen wir, die Skybox-Textur aus der Umgebungskarte durch die Bestrahlungskarte zu ersetzen:Wenn Sie als Ergebnis etwas gesehen haben, das wie eine sehr verschwommene Karte der Umgebung aussah, war die Faltung höchstwahrscheinlich erfolgreich.Züchterrechte und indirekte Beleuchtung
Die resultierende Bestrahlungskarte wird im diffusen Teil des geteilten Ausdrucks des Reflexionsvermögens verwendet und repräsentiert den akkumulierten Beitrag aus allen möglichen Richtungen der indirekten Beleuchtung. Da in diesem Fall das Licht nicht aus bestimmten Quellen stammt, sondern aus der gesamten Umgebung, betrachten wir diffuse und spiegelnde indirekte Beleuchtung als Hintergrund ( Umgebungslicht ) und ersetzen den zuvor verwendeten konstanten Wert.Vergessen Sie zunächst nicht, einen neuen Probenehmer mit einer Bestrahlungskarte hinzuzufügen: uniform samplerCube irradianceMap;
Mit einer Bestrahlungskarte, die alle Informationen über indirekte diffuse Strahlung von der Szene und normal zur Oberfläche speichert, ist das Abrufen von Daten zur Bestrahlung eines bestimmten Fragments so einfach wie das Erstellen einer Probe aus der Textur: // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb;
Da indirekte Strahlung jedoch Daten sowohl für die diffuse als auch für die Spiegelkomponente enthält (wie wir in der Komponentenversion des Ausdrucks des Reflexionsvermögens gesehen haben), müssen wir die diffuse Komponente auf besondere Weise modulieren. Wie in der vorherigen Lektion verwenden wir den Fresnel-Ausdruck, um den Reflexionsgrad des Lichts für eine bestimmte Oberfläche zu bestimmen, woher wir den Brechungsgrad des Lichts oder den diffusen Koeffizienten erhalten: vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
Als Hintergrundbeleuchtung fällt aus allen Richtungen in der Hemisphäre basierend auf der Normalen zur Oberfläche N , es ist unmöglich, den einzigen Median (aufhalber Strecke)zu bestimmen) Vektor zur Berechnung des Fresnel-Koeffizienten. Um den Fresnel-Effekt unter solchen Bedingungen zu simulieren, muss der Koeffizient basierend auf dem Winkel zwischen der Normalen und dem Beobachtungsvektor berechnet werden. Zuvor verwendeten wir jedoch als Parameter für die Berechnung des Fresnel-Koeffizienten den Medianvektor, der auf der Grundlage des Modells der Mikrooberflächen und in Abhängigkeit von der Oberflächenrauheit erhalten wurde. Da in diesem Fall die Rauheit nicht in den Berechnungsparametern enthalten ist, wird der Reflexionsgrad des Lichts an der Oberfläche immer überschätzt. Indirekte Beleuchtung als Ganzes sollte sich genauso verhalten wie direkte Beleuchtung, d. H. Von rauen Oberflächen erwarten wir einen geringeren Reflexionsgrad an den Rändern. Da aber die Rauheit nicht berücksichtigt wird,dann erscheint der Grad der Spiegelreflexion nach Fresnel für indirekte Beleuchtung auf rauen nichtmetallischen Oberflächen unrealistisch (im Bild unten ist der beschriebene Effekt zur besseren Übersichtlichkeit übertrieben):
Sie können dieses Ärgernis umgehen, indem Sie dem Fremlin-Schlick-Ausdruck, einem von Sébastien Lagarde beschriebenen Prozess, Rauheit verleihen : vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
Angesichts der Oberflächenrauheit bei der Berechnung des Fresnel-Sets hat der Code zur Berechnung der Hintergrundkomponente folgende Form: vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
Wie sich herausstellte, läuft die Verwendung von bildbasierter Beleuchtung von Natur aus auf ein Beispiel aus einer kubischen Karte hinaus. Alle Schwierigkeiten hängen hauptsächlich mit der vorläufigen Erstellung und Übertragung der Umgebungskarte auf die Bestrahlungskarte zusammen.Wenn Sie eine vertraute Szene aus einer Lektion über analytische Lichtquellen mit einer Reihe von Kugeln mit unterschiedlicher Metallizität und Rauheit nehmen und diffuse Hintergrundbeleuchtung aus der Umgebung hinzufügen, erhalten Sie ungefähr Folgendes:Es sieht immer noch seltsam aus, da Materialien mit einem hohen Grad an Metallizität immer noch reflektiert werden müssen, um wirklich auszusehen, hmm, Metall (Metalle reflektieren schließlich kein diffuses Licht). Und in diesem Fall die einzigen Reflexionen, die von punktanalytischen Lichtquellen erhalten werden. Und doch können wir bereits sagen, dass die Kugeln stärker in die Umgebung eingetaucht aussehen (besonders beim Wechseln von Umgebungskarten), da die Oberflächen jetzt korrekt auf Hintergrundbeleuchtung aus der Szenenumgebung reagieren.Der vollständige Quellcode für die Lektion ist hier.. In der nächsten Lektion werden wir uns schließlich mit der zweiten Hälfte des Ausdrucks des Reflexionsvermögens befassen, der für die indirekte Spiegelbeleuchtung verantwortlich ist. Nach diesem Schritt werden Sie die Kraft des PBR-Ansatzes in der Beleuchtung wirklich spüren.Zusätzliche Materialien
PS : Wir haben ein Telegramm Conf für die Koordination der Überweisungen. Wenn Sie ernsthaft bei der Übersetzung helfen möchten, sind Sie herzlich willkommen!