
Beim Schreiben in den Framebuffer werden die Werte für die Helligkeit der Farben auf das Intervall von 0,0 bis 1,0 reduziert. Aus diesem auf den ersten Blick harmlosen Merkmal müssen wir immer solche Werte für Beleuchtung und Farben wählen, die in diese Einschränkung passen. Dieser Ansatz funktioniert und liefert anständige Ergebnisse. Was passiert jedoch, wenn wir auf einen besonders hellen Bereich mit vielen hellen Lichtquellen treffen und die Gesamthelligkeit 1,0 überschreitet? Infolgedessen werden alle Werte größer als 1.0 in 1.0 konvertiert, was nicht sehr gut aussieht:

Da die Farbwerte für eine große Anzahl von Fragmenten auf 1,0 reduziert werden, werden große Bereiche des Bildes mit derselben weißen Farbe ausgefüllt, eine erhebliche Anzahl von Bilddetails geht verloren und das Bild selbst sieht unnatürlich aus.
Die Lösung für dieses Problem könnte darin bestehen, die Helligkeit von Lichtquellen so zu verringern, dass keine Fragmente auf der Bühne heller als 1,0 sind. Dies ist nicht die beste Lösung, die die Verwendung unrealistischer Beleuchtungswerte erzwingt. Der beste Ansatz besteht darin, die Helligkeitswerte vorübergehend die Helligkeit von 1,0 überschreiten zu lassen und im letzten Schritt die Farben so zu ändern, dass die Helligkeit in den Bereich von 0,0 bis 1,0 zurückkehrt, jedoch ohne Verlust von Bilddetails.
Das Computerdisplay kann Farben mit einer Helligkeit im Bereich von 0,0 bis 1,0 anzeigen, aber wir haben keine solche Einschränkung bei der Berechnung der Beleuchtung. Indem wir zulassen, dass die Farben des Fragments heller als eins sind, erhalten wir einen viel höheren Helligkeitsbereich für die Arbeit - HDR (High Dynamic Range) . Mit hdr sehen helle Dinge hell aus, dunkle Dinge können wirklich dunkel sein, und dabei werden wir die Details sehen.
Anfänglich wurde in der Fotografie ein hoher Dynamikbereich verwendet: Der Fotograf machte mehrere identische Fotos der Szene mit unterschiedlichen Belichtungen und nahm Farben mit nahezu jeder Helligkeit auf. Die Kombination dieser Fotos bildet ein HDR-Bild, in dem die meisten Details aufgrund der Kombination von Bildern mit unterschiedlichen Belichtungsverlusten sichtbar werden. Zum Beispiel sind unten links auf dem linken Bild stark beleuchtete Fragmente des Bildes deutlich sichtbar (siehe Fenster), aber diese Details verschwinden, wenn Sie eine hohe Belichtung verwenden. Durch die hohe Belichtung werden jedoch Details in dunklen Bereichen des Bildes angezeigt, die zuvor nicht sichtbar waren.

Dies ähnelt der Funktionsweise des menschlichen Auges. Bei Lichtmangel passt sich das Auge an, so dass die dunklen Details deutlich sichtbar werden, ähnlich wie bei hellen Bereichen. Man kann sagen, dass das menschliche Auge abhängig von der Helligkeit der Szene eine automatische Belichtungssteuerung hat.
Das HDR-Rendering funktioniert ähnlich. Beim Rendern erlauben wir die Verwendung eines großen Bereichs von Helligkeitswerten, um Informationen über die hellen und dunklen Details der Szene zu sammeln, und am Ende konvertieren wir die Werte aus dem HDR-Bereich zurück in LDR (niedriger Dynamikbereich, Bereich von 0 bis 1). Diese Transformation wird als Tonabbildung bezeichnet . Es gibt eine große Anzahl von Algorithmen, die darauf abzielen, die meisten Bilddetails bei der Konvertierung in LDR beizubehalten. Diese Algorithmen haben häufig eine Belichtungseinstellung, mit der sie helle oder dunkle Bereiche des Bildes besser anzeigen können.
Durch die Verwendung von HDR beim Rendern können wir nicht nur den LDR-Bereich von 0 bis 1 überschreiten und mehr Bilddetails speichern, sondern auch die tatsächliche Helligkeit der Lichtquellen anzeigen. Zum Beispiel hat die Sonne eine viel größere Helligkeit als eine Taschenlampe. Warum also nicht die Sonne darauf einstellen (zum Beispiel eine Helligkeit von 10,0)? Auf diese Weise können wir die Beleuchtung der Szene mit realistischeren Helligkeitsparametern besser anpassen, was mit LDR-Rendering und einem Helligkeitsbereich von 0 bis 1 unmöglich wäre.
Da das Display nur eine Helligkeit von 0 bis 1 anzeigt, müssen wir den verwendeten HDR-Wertebereich wieder in den Bereich des Monitors konvertieren. Eine einfache Skalierung des Bereichs ist keine gute Lösung, da helle Bereiche das Bild dominieren. Wir können jedoch verschiedene Gleichungen oder Kurven verwenden, um die HDR-Werte in LDR umzuwandeln, wodurch wir die volle Kontrolle über die Helligkeit der Szene erhalten. Diese Transformation wird als Tonzuordnung bezeichnet und ist der letzte Schritt beim HDR-Rendering.
Gleitkomma-Framebuffer
Um das HDR-Rendering zu implementieren, müssen wir verhindern, dass Werte vom Fragment-Shader in einen Bereich von 0 bis 1 gebracht werden. Wenn der Framebuffer das normalisierte Festkommaformat (GL_RGB) für Farbpuffer verwendet, begrenzt OpenGL die Werte automatisch, bevor sie im Framebuffer gespeichert werden. Diese Einschränkung gilt für die meisten Framebuffer-Formate mit Ausnahme von Gleitkommaformaten.
Zum Speichern von Werten, die außerhalb des Bereichs [0.0..1.0] liegen, können Sie einen GL_RGB16F, GL_RGBA16F, GL_RGB32F or GL_RGBA32F
mit den folgenden Formaten verwenden: GL_RGB16F, GL_RGBA16F, GL_RGB32F or GL_RGBA32F
. Dies ist ideal für das HDR-Rendering. Ein solcher Puffer wird als Gleitkomma-Framebuffer bezeichnet.
Das Erstellen eines Gleitkommapuffers unterscheidet sich von einem regulären Puffer nur dadurch, dass ein anderes internes Format verwendet wird:
glBindTexture(GL_TEXTURE_2D, colorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
Der OpenGL-Framebuffer verwendet standardmäßig nur 8 Bit zum Speichern jeder Farbe. Im Gleitkomma-Framebuffer mit den Formaten GL_RGB32F
oder GL_RGBA32F
werden 32 Bit zum Speichern jeder Farbe verwendet - viermal mehr. Wenn keine sehr hohe Genauigkeit erforderlich ist, ist das Format GL_RGBA16F
völlig ausreichend.
Wenn für die Farbe ein Gleitkommapuffer an den Framebuffer angehängt ist, können wir die Szene darin rendern, wobei zu berücksichtigen ist, dass die Farbwerte nicht auf den Bereich von 0 bis 1 beschränkt sind. Im Code für diesen Artikel rendern wir die Szene zuerst in den Gleitkomma-Framebuffer und zeigen dann den Inhalt an Farbpuffer auf einem Halbbild-Rechteck. Es sieht ungefähr so aus:
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // [...] hdr glBindFramebuffer(GL_FRAMEBUFFER, 0); // hdr 2 hdrShader.use(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture); RenderQuad();
Hier können die im Farbpuffer enthaltenen Farbwerte größer als 1 sein. Für diesen Artikel wurde eine Szene mit einem großen länglichen Würfel erstellt, der wie ein Tunnel mit vier Punktlichtquellen aussieht, von denen sich eine am Ende des Tunnels befindet und eine große Helligkeit aufweist.
std::vector<glm::vec3> lightColors; lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f)); lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f)); lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f)); lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));
Das Rendern in den Gleitkommapuffer ist genau das gleiche, als würden wir die Szene in einen regulären Framebuffer rendern. Das einzig Neue ist der fragmentierte HDR-Shader, der sich mit einer einfachen Schattierung eines Vollbild-Rechtecks mit Werten aus einer Textur befasst, die ein Gleitkomma-Farbpuffer ist. Schreiben wir zunächst einen einfachen Shader, der die Eingabedaten unverändert überträgt:
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D hdrBuffer; void main() { vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; FragColor = vec4(hdrColor, 1.0); }
Wir nehmen die Eingabe vom Gleitkomma des Farbpuffers und verwenden sie als Ausgabewert des Shaders. Da das 2D-Rechteck jedoch standardmäßig in den Framebuffer gerendert wird, sind die Ausgabewerte des Shaders auf ein Intervall von 0 bis 1 begrenzt, obwohl die Werte an einigen Stellen größer als 1 sind.

Es wird deutlich, dass zu große Farbwerte am Ende des Tunnels auf Eins beschränkt sind, da ein erheblicher Teil des Bildes vollständig weiß ist und wir Bilddetails verlieren, die heller als Einheit sind. Da wir HDR-Werte direkt als LDR verwenden, entspricht dies dem Fehlen von HDR. Um dies zu beheben, müssen wir die verschiedenen Farbwerte wieder im Bereich von 0 bis 1 anzeigen, ohne dass Details im Bild verloren gehen. Wenden Sie dazu die Tonkomprimierung an.
Tonkomprimierung
Die Tonkomprimierung ist die Konvertierung von Farbwerten, um sie in den Bereich von 0 bis 1 anzupassen, ohne Bilddetails zu verlieren, häufig in Kombination mit dem gewünschten Weißabgleich für das Bild.
Der einfachste Tonabbildungsalgorithmus ist als Reinhard-Tonabbildungsalgorithmus bekannt . Es werden alle HDR-Werte im LDR-Bereich angezeigt. Fügen Sie diesen Algorithmus dem vorherigen Fragment-Shader hinzu und wenden Sie auch die Gammakorrektur (und die Verwendung von SRGB-Texturen) an.
void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); }
Hinweis trans. - Für kleine Werte von x verhält sich die Funktion x / (1 + x) ungefähr wie x, für große x tendiert sie zur Einheit. Funktionsgraph:

Mit der Reinhardt-Tonkomprimierung verlieren wir keine Details mehr in hellen Bereichen des Bildes. Der Algorithmus bevorzugt helle Bereiche, wodurch dunkle Bereiche weniger deutlich werden.

Hier sehen Sie am Ende des Bildes wieder Details wie die Holzstruktur. Mit diesem relativ einfachen Algorithmus können wir alle Farben aus dem HDR-Bereich klar erkennen und die Beleuchtung der Szene steuern, ohne Bilddetails zu verlieren.
Es ist erwähnenswert, dass wir die Tonwertkomprimierung direkt am Ende unseres Shaders verwenden können, um die Beleuchtung zu berechnen, und dann benötigen wir überhaupt keinen Gleitkomma-Framebuffer. In komplexeren Szenen müssen Sie jedoch häufig HDR-Zwischenwerte in Gleitkommapuffern speichern, was sich als nützlich erweist.
Ein weiteres interessantes Merkmal der Tonkomprimierung ist die Verwendung eines Belichtungsparameters. Sie können sich daran erinnern, dass in den Bildern am Anfang des Artikels verschiedene Details bei unterschiedlichen Belichtungswerten sichtbar waren. Wenn wir eine Szene haben, in der sich Tag und Nacht ändern, ist es sinnvoll, tagsüber eine niedrige Belichtung und nachts eine hohe Belichtung zu verwenden, was der Anpassung des menschlichen Auges ähnelt. Mit diesem Belichtungsparameter können wir Beleuchtungsparameter konfigurieren, die Tag und Nacht unter verschiedenen Lichtbedingungen funktionieren.
Ein relativ einfacher Tonkomprimierungsalgorithmus mit Belichtung sieht folgendermaßen aus:
uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); }
Hinweis per: Fügen Sie ein Diagramm für diese Funktion mit Belichtung 1 und 2 hinzu:

Hier haben wir eine Variable für die Belichtung definiert, die standardmäßig 1 ist und es uns ermöglicht, das Gleichgewicht zwischen der Anzeigequalität von dunklen und hellen Bereichen des Bildes genauer zu wählen. Bei einer großen Belichtung sehen wir beispielsweise viel mehr Details in den dunklen Bereichen des Bildes. Umgekehrt macht eine geringe Belichtung dunkle Bereiche nicht unterscheidbar, ermöglicht es Ihnen jedoch, helle Bereiche des Bildes besser zu sehen. Unten sehen Sie Bilder eines Tunnels mit unterschiedlichen Belichtungsstufen.

Diese Bilder zeigen deutlich die Vorteile des HDR-Renderings. Wenn sich die Belichtungsstufe ändert, sehen wir mehr Details der Szene, die beim normalen Rendern verloren gehen würden. Nehmen Sie als Beispiel das Ende des Tunnels - bei normaler Belichtung ist die Textur des Baumes kaum sichtbar, bei geringer Belichtung ist die Textur jedoch perfekt sichtbar. In ähnlicher Weise sind bei hoher Belichtung Details in dunklen Bereichen sehr deutlich sichtbar.
Der Quellcode für die Demo ist hier.
Mehr HDR
Diese beiden gezeigten Tonkomprimierungsalgorithmen sind nur ein kleiner Teil einer großen Anzahl fortgeschrittener Algorithmen, von denen jeder seine eigenen Stärken und Schwächen hat. Einige Algorithmen betonen bestimmte Farben / Helligkeiten besser, andere Algorithmen zeigen gleichzeitig dunkle und helle Bereiche an und liefern farbenfrohere und detailliertere Bilder. Es gibt auch viele Methoden, die als automatische Belichtungsanpassung oder Augenanpassung bekannt sind . Sie bestimmen die Helligkeit der Szene im vorherigen Bild und ändern (langsam) den Belichtungsparameter, so dass die dunkle Szene langsam heller und die hell-dunkler wird: ähnlich der Gewöhnung des menschlichen Auges.
Die wirklichen Vorteile von HDR lassen sich am besten an großen und komplexen Szenen mit seriösen Beleuchtungsalgorithmen erkennen. In diesem Artikel wurde zu Schulungszwecken die einfachste Szene verwendet, da das Erstellen einer großen Szene schwierig sein kann. Trotz der Einfachheit der Szene sind einige Vorteile des HDR-Renderings sichtbar: In den dunklen und hellen Bereichen des Bildes gehen keine Details verloren, da sie mithilfe der Tonkomprimierung gespeichert werden, das Hinzufügen mehrerer Lichtquellen nicht zum Auftreten weißer Bereiche führt und die Werte nicht in LDR passen müssen Reichweite.
Darüber hinaus macht das HDR-Rendering einige interessante Effekte glaubwürdiger und realistischer. Einer dieser Effekte ist die Blüte, die wir in einem zukünftigen Artikel diskutieren werden.
Zusätzliche Ressourcen:
PS Wir haben ein Telegramm conf für die Koordination von Überweisungen. Wenn Sie ernsthaft bei der Übersetzung helfen möchten, sind Sie herzlich willkommen!