Lerne OpenGL. Lektion 5.8 - Blüte

OGL3

Blüte


Aufgrund des begrenzten Helligkeitsbereichs, der herkömmlichen Monitoren zur Verfügung steht, ist es per Definition schwierig, helle Lichtquellen und hell beleuchtete Oberflächen überzeugend anzuzeigen. Eine der gebräuchlichen Methoden zum Hervorheben heller Bereiche auf dem Monitor ist eine Technik, bei der helle Objekte mit einem Lichtschein versehen werden, der den Eindruck einer „Ausbreitung“ von Licht außerhalb der Lichtquelle erweckt. Infolgedessen erweckt der Betrachter den Eindruck einer hohen Helligkeit solcher beleuchteten Bereiche oder Lichtquellen.

Der beschriebene Effekt eines Lichthofs und der Austritt von Licht über die Quelle hinaus wird durch eine Nachbearbeitungstechnik erreicht, die als Bloom bezeichnet wird . Durch Anwenden des Effekts wird allen hellen Bereichen der angezeigten Szene ein charakteristischer Lichtschein hinzugefügt, der im folgenden Beispiel zu sehen ist:



Bloom fügt dem Bild einen unverwechselbaren visuellen Hinweis auf die signifikante Helligkeit der Objekte hinzu, die vom Heiligenschein aufgrund des angewendeten Effekts abgedeckt werden. Durch die selektive und präzise Anwendung (die viele Spiele leider nicht bewältigen können) kann der Effekt die visuelle Ausdruckskraft der in der Szene verwendeten Beleuchtung erheblich verbessern und in bestimmten Situationen Drama hinzufügen.

Diese Technik funktioniert in Verbindung mit HDR- Rendering fast als selbstverständliche Ergänzung. Anscheinend mischen aus diesem Grund viele Menschen diese beiden Begriffe fälschlicherweise bis zur vollständigen Austauschbarkeit. Diese Techniken sind jedoch völlig unabhängig und werden für verschiedene Zwecke verwendet. Es ist möglich, Bloom mithilfe des Standard-Bildpuffers mit 8-Bit-Farbtiefe zu implementieren, genau wie beim Anwenden von HDR-Rendering, ohne auf Bloom zurückgreifen zu müssen. Das einzige ist, dass Sie mit dem HDR-Rendering den Effekt effizienter implementieren können (wir werden dies später sehen).

Um die Blüte zu implementieren, wird die beleuchtete Szene zunächst auf die übliche Weise gerendert. Als nächstes werden ein HDR-Farbpuffer und ein Farbpuffer extrahiert, die nur helle Teile der Szene enthalten. Dieses extrahierte helle Teilbild wird dann unscharf und über das ursprüngliche HDR-Bild der Szene gelegt.

Um es klarer zu machen, werden wir den Prozess Schritt für Schritt analysieren. Rendern Sie eine Szene mit 4 hellen Lichtquellen, die als farbige Würfel angezeigt werden. Alle haben einen Helligkeitswert im Bereich von 1,5 bis 15,0. Wenn der Farbpuffer an den HDR ausgegeben wird, ist das Ergebnis wie folgt:


Aus diesem HDR-Farbpuffer extrahieren wir alle Fragmente, deren Helligkeit eine vorgegebene Grenze überschreitet. Es stellt sich heraus, dass ein Bild nur hell beleuchtete Bereiche enthält:


Ferner ist dieses Bild von hellen Bereichen unscharf. Die Schwere des Effekts wird im Wesentlichen durch die Stärke und den Radius des angewendeten Unschärfefilters bestimmt:


Das resultierende verschwommene Bild von hellen Bereichen ist die Grundlage für den endgültigen Effekt von Lichthöfen um helle Objekte. Diese Textur wird einfach mit dem ursprünglichen HDR-Bild der Szene gemischt. Da die hellen Bereiche unscharf waren, nahmen ihre Größen zu, was letztendlich einen visuellen Effekt der Leuchtkraft ergibt, der über die Grenzen von Lichtquellen hinausgeht:


Wie Sie sehen können, ist die Blüte nicht die ausgefeilteste Technik, aber es ist nicht immer einfach, ihre hohe visuelle Qualität und Zuverlässigkeit zu erreichen. Der Effekt hängt größtenteils von der Qualität und Art des angewendeten Unschärfefilters ab. Selbst kleine Änderungen der Filterparameter können die endgültige Qualität der Ausrüstung dramatisch verändern.

Die obigen Aktionen geben uns also einen schrittweisen Algorithmus für den Nachbearbeitungseffekt für den Bloom-Effekt. Das folgende Bild fasst die erforderlichen Aktionen zusammen:


Zunächst benötigen wir Informationen über die hellen Teile der Szene basierend auf einem bestimmten Schwellenwert. Das werden wir tun.

Highlights extrahieren


Für den Anfang müssen wir also zwei Bilder basierend auf unserer Szene erhalten. Es wäre naiv, zweimal zu rendern, aber verwenden Sie die fortgeschrittenere MRT- Methode ( Multiple Render Targets ): Wir geben mehr als eine Ausgabe im endgültigen Fragment-Shader an, und dank dieser können zwei Bilder in einem Durchgang extrahiert werden! Um anzugeben, in welchem ​​Farbpuffer der Shader ausgegeben wird, wird der Layout- Bezeichner verwendet:

layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; 

Natürlich funktioniert die Methode nur, wenn wir mehrere Puffer zum Schreiben vorbereitet haben. Mit anderen Worten, um mehrere Ausgaben vom Fragment-Shader zu implementieren, sollte der in diesem Moment verwendete Bildpuffer eine ausreichende Anzahl verbundener Farbpuffer enthalten. Wenn wir uns der Lektion über den Bildpuffer zuwenden, wird daran erinnert, dass wir beim Binden der Textur als Farbpuffer die Nummer des Farbanhangs angeben können. Bisher mussten wir keinen anderen Anhang als GL_COLOR_ATTACHMENT0 verwenden , aber diesmal ist GL_COLOR_ATTACHMENT1 nützlich, da wir zwei Ziele für die gleichzeitige Aufnahme benötigen:

 //       unsigned int hdrFBO; glGenFramebuffers(1, &hdrFBO); glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); unsigned int colorBuffers[2]; glGenTextures(2, colorBuffers); for (unsigned int i = 0; i < 2; i++) { glBindTexture(GL_TEXTURE_2D, colorBuffers[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //     glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0 ); } 

Wenn Sie glDrawBuffers aufrufen, müssen Sie OpenGL explizit mitteilen, dass wir in mehrere Puffer ausgeben werden. Andernfalls wird die Bibliothek immer noch nur an den ersten Anhang ausgegeben, wobei Schreibvorgänge für andere Anhänge ignoriert werden. Als Argument für die Funktion wird ein Array von Bezeichnern der verwendeten Anhänge aus der entsprechenden Aufzählung übergeben:

 unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; glDrawBuffers(2, attachments); 

Für diesen Frame-Puffer schreibt jeder Fragment-Shader, der einen Standortbezeichner für seine Ausgaben angibt, in den entsprechenden Farbpuffer. Und das sind großartige Neuigkeiten, denn auf diese Weise vermeiden wir den unnötigen Rendering-Durchgang, um Daten über die hellen Teile der Szene zu extrahieren - Sie können alles auf einmal in einem einzigen Shader erledigen:

 #version 330 core layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; [...] void main() { [...] //      FragColor = vec4(lighting, 1.0); //         //   -    ,    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)); if(brightness > 1.0) BrightColor = vec4(FragColor.rgb, 1.0); else BrightColor = vec4(0.0, 0.0, 0.0, 1.0); } 

In diesem Fragment wird der Teil weggelassen, der den typischen Code zur Berechnung der Beleuchtung enthält. Das Ergebnis wird in die erste Ausgabe des Shaders geschrieben - die FragColor- Variable. Als nächstes wird die resultierende Farbe des Fragments verwendet, um den Helligkeitswert zu berechnen. Dazu wird eine gewichtete Graustufenübersetzung durchgeführt (durch Skalarmultiplikation multiplizieren wir die entsprechenden Komponenten der Vektoren und addieren sie, was zu einem einzigen Wert führt). Wenn dann die Helligkeit eines Fragments eines bestimmten Schwellenwerts überschritten wird, zeichnen wir seine Farbe in der zweiten Ausgabe des Shaders auf. Für Würfel, die Lichtquellen ersetzen, wird dieser Shader ebenfalls ausgeführt.

Nachdem wir den Algorithmus herausgefunden haben, können wir verstehen, warum diese Technik beim HDR-Rendering so gut funktioniert. Durch das Rendern im HDR-Format können Farbkomponenten die Obergrenze von 1,0 überschreiten. Dadurch können Sie den Helligkeitsschwellenwert außerhalb des Standardintervalls [0., 1.] flexibler anpassen und genau einstellen, welche Teile der Szene als hell gelten. Ohne HDR müssen Sie sich mit einer Helligkeitsschwelle im Intervall [0., 1.] zufrieden geben, was durchaus akzeptabel ist, aber zu einem „schärferen“ Helligkeitsabfall führt, der die Blüte oft zu aufdringlich und auffällig macht (stellen Sie sich auf einem Schneefeld hoch in den Bergen vor). .

Nachdem der Shader ausgeführt wurde, enthalten zwei Zielpuffer ein normales Bild der Szene sowie ein Bild, das nur helle Bereiche enthält.


Das Bild von hellen Bereichen sollte jetzt mit Unschärfe verarbeitet werden. Sie können dies mit einem einfachen rechteckigen ( Box- ) Filter erreichen, der im Nachbearbeitungsabschnitt der Frame Buffer- Lektion verwendet wurde. Ein viel besseres Ergebnis wird jedoch durch Gauß-Filterung erzielt.

Gaußsche Unschärfe


Die Nachbearbeitungsstunde gab uns eine Idee der Unschärfe durch einfache Farbmittelung benachbarter Bildfragmente. Diese Unschärfemethode ist einfach, aber das resultierende Bild sieht möglicherweise attraktiver aus. Die Gaußsche Unschärfe basiert auf der gleichnamigen glockenförmigen Verteilungskurve: Hohe Werte der Funktion liegen näher an der Mitte der Kurve und fallen auf beide Seiten ab. Mathematisch kann eine Gaußsche Kurve mit verschiedenen Parametern ausgedrückt werden, aber die allgemeine Form der Kurve bleibt wie folgt:


Unschärfe mit Gewichten basierend auf den Werten der Gauß-Kurve sieht viel besser aus als ein Rechteckfilter: Aufgrund der Tatsache, dass die Kurve in der Nähe ihres Zentrums eine größere Fläche hat, was größeren Gewichten für Fragmente nahe der Mitte des Filterkerns entspricht. Nehmen wir zum Beispiel den 32x32-Kern, verwenden wir die Gewichtungsfaktoren, je kleiner das Fragment vom zentralen entfernt ist. Es ist diese Filtercharakteristik, die ein visuell zufriedenstellenderes Ergebnis der Gaußschen Unschärfe ergibt.

Die Implementierung des Filters erfordert eine zweidimensionale Anordnung von Gewichtungskoeffizienten, die auf der Grundlage des zweidimensionalen Ausdrucks, der die Gaußsche Kurve beschreibt, gefüllt werden könnte. Wir werden jedoch sofort auf ein Leistungsproblem stoßen: Selbst ein relativ kleiner Unschärfekern in einem 32x32-Fragment erfordert 1024 Texturmuster für jedes Fragment des verarbeiteten Bildes!

Glücklicherweise hat der Ausdruck der Gaußschen Kurve eine sehr bequeme mathematische Eigenschaft - die Trennbarkeit, die es ermöglicht, zwei eindimensionale Ausdrücke aus einem zweidimensionalen Ausdruck zu erstellen, die die horizontalen und vertikalen Komponenten beschreiben. Dies ermöglicht wiederum eine Unschärfe in zwei Ansätzen: horizontal und dann vertikal mit Gewichtssätzen, die jeder der Richtungen entsprechen. Das resultierende Bild ist das gleiche wie bei der Verarbeitung eines zweidimensionalen Algorithmus, erfordert jedoch viel weniger Verarbeitungsleistung des Videoprozessors: Anstelle von 1024 Samples aus der Textur benötigen wir nur 32 + 32 = 64! Dies ist die Essenz der Zwei-Pass-Gauß-Filtration.


Für uns bedeutet dies alles eines: Das Verwischen eines Bildes muss zweimal erfolgen, und hier ist die Verwendung von Bildpufferobjekten nützlich. Wir wenden die sogenannte Ping-Pong-Technik an: Es gibt einige Bildpufferobjekte und der Inhalt des Farbpuffers eines Bildpuffers wird mit einer gewissen Verarbeitung in den Farbpuffer des aktuellen Bildpuffers gerendert, dann werden der Quellbildpuffer und der Bildpufferempfänger ausgetauscht und dieser Vorgang wird eine bestimmte Anzahl von Malen wiederholt. Tatsächlich wird der aktuelle Bildpuffer zum Anzeigen des Bildes einfach umgeschaltet und damit die aktuelle Textur, aus der das Abtasten zum Rendern durchgeführt wird. Mit diesem Ansatz können Sie das Originalbild verwischen, indem Sie es in den ersten Bildpuffer legen, dann den Inhalt des ersten Bildpuffers verwischen, in das zweite Bild einfügen und das zweite Bild verwischen, in das erste Bild einfügen usw.

Bevor wir zum Tuning-Code für den Frame-Puffer übergehen, werfen wir einen Blick auf den Gaußschen Blur-Shader-Code:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D image; uniform bool horizontal; uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); void main() { //     vec2 tex_offset = 1.0 / textureSize(image, 0); //    vec3 result = texture(image, TexCoords).rgb * weight[0]; if(horizontal) { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i]; } } else { for(int i = 1; i < 5; ++i) { result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i]; result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i]; } } FragColor = vec4(result, 1.0); } 

Wie Sie sehen können, verwenden wir eine relativ kleine Stichprobe von Koeffizienten der Gaußschen Kurve, die als Gewichte für Stichproben horizontal oder vertikal relativ zum aktuellen Fragment verwendet werden. Der Code hat zwei Hauptzweige, die den Algorithmus basierend auf dem Wert der horizontalen Uniform in einen vertikalen und einen horizontalen Durchgang unterteilen. Der Offset für jedes Sample wird gleich der Texelgröße gesetzt, die als Kehrwert der Texturgröße definiert ist (ein Wert vom Typ vec2, der von der Funktion texturSize () zurückgegeben wird).

Erstellen Sie zwei Rahmenpuffer mit einem Farbpuffer basierend auf der Textur:

 unsigned int pingpongFBO[2]; unsigned int pingpongBuffer[2]; glGenFramebuffers(2, pingpongFBO); glGenTextures(2, pingpongBuffer); for (unsigned int i = 0; i < 2; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]); glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0 ); } 

Nachdem wir die HDR-Textur der Szene erhalten und die Textur der hellen Bereiche extrahiert haben, füllen wir den Farbpuffer eines der beiden vorbereiteten Framebuffer mit der Helligkeitstextur und starten den Ping-Pong-Prozess zehnmal (fünfmal vertikal, fünfmal horizontal):

 bool horizontal = true, first_iteration = true; int amount = 10; shaderBlur.use(); for (unsigned int i = 0; i < amount; i++) { glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]); shaderBlur.setInt("horizontal", horizontal); glBindTexture( GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal] ); RenderQuad(); horizontal = !horizontal; if (first_iteration) first_iteration = false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Bei jeder Iteration wählen und verankern wir einen der Frame-Puffer basierend darauf, ob diese Iteration horizontal oder vertikal unscharf wird, und der Farbpuffer des anderen Framebuffers wird dann als Eingabetextur für den Unschärfeshader verwendet. Bei der ersten Iteration müssen wir explizit ein Bild verwenden, das helle Bereiche enthält ( brightnessTexture ) - andernfalls bleiben beide Ping-Pong-Framebuffer leer. Nach zehn Durchgängen wird das Originalbild fünfmal durch einen vollständigen Gauß-Filter verwischt. Der verwendete Ansatz ermöglicht es uns, den Grad der Unschärfe leicht zu ändern: Je mehr Ping-Pong-Iterationen, desto stärker die Unschärfe.

In unserem Fall sieht das Unschärfergebnis ungefähr so ​​aus:


Um den Effekt zu vervollständigen, muss nur das verschwommene Bild mit dem ursprünglichen HDR-Bild der Szene kombiniert werden.

Textur mischen


Wenn Sie die HDR-Textur der gerenderten Szene und die verschwommene Textur der überbelichteten Bereiche zur Hand haben, müssen Sie diese beiden Bilder nur kombinieren, um den berühmten Bloom-Effekt oder das Glühen zu erzielen. Der letzte Fragment-Shader (sehr ähnlich dem in der Lektion über das HDR- Format vorgestellten) macht genau das - er mischt additiv zwei Texturen:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D scene; uniform sampler2D bloomBlur; uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(scene, TexCoords).rgb; vec3 bloomColor = texture(bloomBlur, TexCoords).rgb; hdrColor += bloomColor; // additive blending //   vec3 result = vec3(1.0) - exp(-hdrColor * exposure); //     - result = pow(result, vec3(1.0 / gamma)); FragColor = vec4(result, 1.0); } 

Worauf Sie achten sollten: Das Mischen erfolgt vor dem Anwenden der Tonzuordnung . Dadurch wird die zusätzliche Helligkeit des Effekts korrekt in den LDR-Bereich ( Low Dynamic Range ) übersetzt, während die relative Helligkeitsverteilung in der Szene beibehalten wird.

Das Ergebnis der Verarbeitung - alle hellen Bereiche erhielten einen spürbaren Glüheffekt:


Würfel, die Lichtquellen ersetzen, sehen jetzt viel heller aus und vermitteln besser den Eindruck einer Lichtquelle. Diese Szene ist ziemlich primitiv, da die Umsetzung des Effekts besonderer Begeisterung nicht dazu führt, aber in komplexen Szenen mit durchdachter Beleuchtung kann eine qualitativ realisierte Blüte ein entscheidendes visuelles Element sein, das Drama hinzufügt.

Der Quellcode für das Beispiel ist hier .

Ich stelle fest, dass in der Lektion ein ziemlich einfacher Filter mit nur fünf Proben in jede Richtung verwendet wurde. Indem Sie mehr Samples in einem größeren Radius erstellen oder mehrere Iterationen des Filters durchführen, können Sie den Effekt visuell verbessern. Es ist auch erwähnenswert, dass die Qualität des gesamten Effekts visuell direkt von der Qualität des verwendeten Unschärfealgorithmus abhängt. Durch die Verbesserung des Filters können Sie eine signifikante Verbesserung und den gesamten Effekt erzielen. Ein eindrucksvolleres Ergebnis zeigt beispielsweise die Kombination mehrerer Filter mit unterschiedlichen Kerngrößen oder unterschiedlichen Gaußschen Kurven. Im Folgenden finden Sie zusätzliche Ressourcen von Kalogirou und EpicGames, die sich mit der Verbesserung der Blütenqualität durch Ändern der Gaußschen Unschärfe befassen.

Zusätzliche Ressourcen


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


All Articles