WBOIT in OpenGL: Transparenz ohne Sortierung

In diesem Beitrag geht es um gewichtete gemischte auftragsunabhängige Transparenz (WBOIT) - der Trick, der 2013 in JCGT behandelt wurde.

Wenn mehrere transparente Objekte auf einem Bildschirm angezeigt werden, hängt die Pixelfarbe davon ab, welches näher am Betrachter liegt. Hier ist ein bekannter Mischoperator, der in diesem Fall verwendet wird:

\ begin {matrix} C = C_ {nah} \ alpha + C_ {fern} (1- \ alpha) && (1) \ end {matrix}



Die Bestellung von Fragmenten ist wichtig. Der Operator enthält die Farbe (C nah ) und die Opazität ( α ) eines nahen Fragments sowie die Gesamtfarbe (C fern ) aller dahinter liegenden Fragmente. Die Deckkraft kann zwischen 0 und 1 liegen. 0 bedeutet, dass das Objekt vollständig transparent (unsichtbar) ist und 1 bedeutet, dass es vollständig undurchsichtig ist.

Um diesen Operator verwenden zu können, müssen Sie Fragmente nach Tiefe sortieren. Stellen Sie sich vor, was für ein Fluch es ist. Im Allgemeinen müssen Sie eine Sortierung pro Frame durchführen. Wenn Sie Objekte sortieren, müssen Sie möglicherweise mit unregelmäßig geformten Flächen umgehen, die in Abschnitte geschnitten werden müssen, und dann müssen abgeschnittene TEILE dieser Flächen sortiert werden (Sie müssen dies definitiv für sich überschneidende Flächen tun). Wenn Sie Fragmente sortieren, platzieren Sie die eigentliche Sortierung in Ihren Shadern. Diese Methode wird als "Auftragsunabhängige Transparenz" (OIT) bezeichnet und basiert auf einer verknüpften Liste, die im Videospeicher gespeichert ist. Es ist fast unmöglich vorherzusagen, wie viel Speicher für diese Liste reserviert werden muss. Und wenn Sie nicht genügend Speicher haben, werden Artefakte auf dem Bildschirm angezeigt.

Betrachten Sie sich als glücklich, wenn Sie die Anzahl der transparenten Objekte in Ihrer Szene regulieren und ihre relativen Positionen anpassen können. Wenn Sie jedoch einen CAD entwickeln, müssen die Benutzer ihre Objekte positionieren, sodass so viele Objekte vorhanden sind, wie sie möchten, und ihre Platzierung ist einfach willkürlich.

Jetzt sehen Sie, warum es so verlockend ist, einen Mischoperator zu finden, der keine vorläufige Sortierung erfordert. Und es gibt einen solchen Operator - in einem Artikel, den ich am Anfang erwähnt habe. Tatsächlich gibt es mehrere Formeln, aber eine davon betrachten die Autoren (und ich) als die besten:

\ begin {matrix} C = {{\ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}} (1- \ prod_ {i = 1} ^ {n} (1- \ alpha_i)) + C_0 \ prod_ {i = 1} ^ {n} (1- \ alpha_i) && (2) \ end {matrix}





Auf dem Screenshot sieht man Gruppen transparenter Dreiecke, die auf vier Tiefenebenen angeordnet sind. Auf der linken Seite wurden sie mit WBOIT gerendert, und auf der rechten Seite wurde die klassische auftragsabhängige Mischung - mit Formel (1) - verwendet (ich werde sie von nun an CODB nennen).

Bevor wir mit dem Rendern transparenter Objekte beginnen können, müssen wir alle nicht transparenten Objekte rendern. Danach werden transparente Objekte mit einem Tiefentest gerendert, ohne jedoch etwas in einen glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); zu schreiben (dies kann folgendermaßen erfolgen: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ).

Schauen wir uns nun an, was irgendwann mit Bildschirmraumkoordinaten (x, y) passiert. Transparente Fragmente - die zufällig näher als der nicht transparente sind - bestehen den Tiefentest, unabhängig davon, wie sie relativ zu den bereits gerenderten transparenten Fragmenten platziert sind. Diese transparenten Fragmente, die hinter das nicht transparente fallen - nun, sie bestehen den Tiefentest nicht und werden natürlich verworfen.

C 0 in Formel (2) ist die Farbe des nicht transparenten Fragments, das an diesem Punkt (x, y) gerendert wird. Wir haben insgesamt n transparente Fragmente, die den Tiefentest bestanden haben, und sie haben Indizes i ∈ [1, n]. C i ist die Farbe des transparenten Fragments und α i ist seine Opazität.

Formel (2) ist Formel (1) etwas ähnlich, obwohl es nicht sehr offensichtlich ist. Ersetzen mit C nah , C 0 mit C fern und mit α und Formel (1) wird genau das sein, was Sie bekommen. In der Tat, ist das gewichtete arithmetische Mittel der Farben aller transparenten Fragmente (es gibt eine ähnliche Formel in der Mechanik für "Massenschwerpunkt"), und es wird für die Farbe des nahen Fragments C nahe gewählt . C 0 ist die Farbe des nicht transparenten Fragments hinter all den transparenten Fragmenten, für die wir das gewichtete arithmetische Mittel berechnen. Mit anderen Worten, wir ersetzen alle transparenten Fragmente durch ein "gewichtetes Mittel" -Fragment und verwenden den Standard-Mischoperator - Formel (1). Nun gibt es eine etwas ausgefeilte Formel für α , und wir müssen ihre Bedeutung noch herausfinden.

 alpha=1 prodi=1n(1 alphai)


Es ist eine Skalarfunktion im n-dimensionalen Raum. Alle α i sind in [0, 1] enthalten, so dass seine partielle Ableitung in Bezug auf eines von α i eine nicht negative Konstante ist. Dies bedeutet, dass die Opazität des "gewichteten Mittelwerts" -Fragments zunimmt, wenn Sie die Opazität eines der transparenten Fragmente erhöhen, was genau das ist, was wir wollen. Darüber hinaus steigt es linear an.

Wenn die Deckkraft eines Fragments 0 ist, ist es vollständig unsichtbar. Es trägt überhaupt nicht zur resultierenden Farbe bei.

Wenn mindestens ein Fragment eine Opazität von 1 hat, ist auch α 1. Das heißt, ein nicht transparentes Fragment wird unsichtbar, was gut ist. Das Problem ist, dass die anderen transparenten Fragmente (hinter diesem Fragment mit Opazität = 1) immer noch durchsichtig sind und zur resultierenden Farbe beitragen:



Das orangefarbene Dreieck auf diesem Bild liegt oben, das grüne Dreieck liegt darunter und unter dem grünen Dreieck liegen graue und cyanfarbene Dreiecke. Der Hintergrund ist schwarz. Die Deckkraft des orangefarbenen Dreiecks beträgt 1; Alle anderen haben eine Deckkraft von 0,5. Hier können Sie sehen, dass WBOIT sehr schlecht aussieht. Der einzige Ort, an dem echte orange Farbe erscheint, ist der Rand des grünen Dreiecks, der mit einer nicht transparenten weißen Linie umrandet ist. Wie bereits erwähnt, ist ein nicht transparentes Fragment unsichtbar, wenn ein transparentes Fragment mit einer Deckkraft von 1 darüber liegt.

Auf dem nächsten Bild ist es besser zu sehen:



Die Deckkraft des orangefarbenen Dreiecks beträgt 1, das grüne Dreieck mit deaktivierter Transparenz wird nur mit nicht transparenten Objekten gerendert. Es sieht so aus, als würde die GRÜNE Farbe des Dreiecks hinter dem oberen Dreieck als ORANGE-Farbe angezeigt.

Der einfachste Weg, um Ihr Bild plausibel erscheinen zu lassen, besteht darin, Ihren Objekten keine hohe Deckkraft zu verleihen. In einem Projekt, in dem ich diese Technik verwende, stelle ich die Deckkraft nicht mehr als 0,5 ein. Es handelt sich um 3D-CAD, bei dem Objekte schematisch gezeichnet werden und nicht sehr realistisch aussehen müssen. Daher ist diese Einschränkung akzeptabel.

Bei geringer Deckkraft sehen die linken und rechten Bilder sehr ähnlich aus:



Und sie unterscheiden sich merklich bei hohen Trübungen:



Hier ist ein transparentes Polyeder:




Es hat orangefarbene Seitenflächen und grüne horizontale Flächen, was leider nicht offensichtlich ist, was bedeutet, dass das Bild nicht glaubwürdig aussieht. Wo immer ein orangefarbenes Gesicht oben ist, muss die Farbe orange sein, und wo es sich hinter einem grünen Gesicht befindet, muss die Farbe grüner sein. Besser mit einer Farbe zeichnen:



Injizieren Sie die Tiefe in den Mischoperator


Um den Mangel an Tiefensortierung auszugleichen, haben die Autoren des oben genannten JCGT-Papiers verschiedene Möglichkeiten gefunden, die Tiefe in Formel (2) zu injizieren. Dies erschwert die Implementierung und macht das Ergebnis weniger vorhersehbar. Damit es funktioniert, müssen die Mischparameter auf eine bestimmte 3D-Szene abgestimmt werden. Ich habe mich nicht eingehend mit diesem Thema befasst. Wenn Sie also mehr wissen möchten, lesen Sie die Zeitung.

Autoren behaupten, dass WBOIT manchmal in der Lage ist, etwas zu tun, was CODB nicht kann. Betrachten Sie beispielsweise das Zeichnen eines Rauches als Partikelsystem mit zwei Partikeln: dunklem Rauch und hellerem Rauch. Wenn sich die Partikel bewegen und ein Partikel durch ein anderes hindurchgeht, wechselt ihre Mischfarbe sofort von dunkel zu hell, was nicht gut ist. Der WBOIT-Operator mit Tiefe erzeugt ein bevorzugteres Ergebnis mit einem reibungslosen Farbübergang. Haare oder Fell, die als dünne Röhrchen modelliert sind, haben die gleichen Eigenschaften.

Der Code


Nun zur OpenGL-Implementierung der Formel (2). Sie können die Implementierung auf GitHub sehen. Es ist eine Qt-basierte App, und die Bilder, die Sie hier sehen, stammen größtenteils davon.

Wenn Sie mit transparentem Rendern noch nicht vertraut sind, finden Sie hier ein gutes Einstiegsmaterial:
Lerne OpenGL. Mischen

Ich empfehle es zu lesen, bevor Sie mit diesem Beitrag fortfahren.

Um Formel (2) auszuwerten, benötigen wir 2 zusätzliche Framebuffer, 3 Multisamle-Texturen und einen Tiefen-Renderpuffer. Nicht transparente Objekte werden in der ersten Textur, colorTextureNT, gerendert. Sein Typ ist GL_RGB10_A2. Die zweite Textur (colorTexture) ist vom Typ GL_RGBA16F. Die ersten drei Komponenten von colorTexture enthalten diesen Teil der Formel (2): und wird in die vierte Komponente geschrieben. Die letzte Textur, alphaTexture, vom Typ GL_R16 enthält .

Zuerst müssen wir alle diese Objekte erstellen und ihre Bezeichner von OpenGL erhalten:
  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

Wie Sie sich erinnern, verwende ich Qt framewok und alle Aufrufe von OpenGL erfolgen von einem Objekt vom Typ QOpenGLFunctions_4_5_Core, für das ich immer den Namen f verwende.

Die Speicherzuweisung erfolgt als Nächstes:
  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

Framebuffer-Setup:
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

Während des zweiten Rendering-Durchlaufs erfolgt die Ausgabe des Fragment-Shaders in zwei Texturen, die mit glDrawBuffers explizit angegeben werden müssen.
Der größte Teil dieses Codes wird einmalig ausgeführt, wenn das Programm gestartet wird. Der Code für die Textur- und Renderpufferspeicherzuordnung wird jedes Mal ausgeführt, wenn die Fenstergröße geändert wird. Nun fahren wir mit dem Code fort, der jedes Mal ausgeführt wird, wenn der Inhalt des Fensters aktualisiert wird.
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ... rendering non-transparent objects ... // ....... // done! (you didn't expect me to explain how do I render primitives in OpenGL, did you? // It's not relevant for this topic 

Wir haben gerade alle nicht transparenten Objekte in colorTextureNT gerendert und Tiefen in den Renderpuffer geschrieben. Bevor Sie denselben Renderpuffer beim nächsten Rendering-Durchgang verwenden, müssen Sie sicherstellen, dass alle Schreibvorgänge im Tiefen-Renderpuffer von nicht transparenten Objekten abgeschlossen sind. Dies wird mit GL_FRAMEBUFFER_BARRIER_BIT erreicht. Nachdem die transparenten Objekte gerendert wurden, rufen wir die Funktion ApplyTextures () auf, die den endgültigen Rendering-Durchgang ausführt, bei dem der Fragment-Shader aus den Texturen colorTextureNT, colorTexture und alphaTexture abtastet, um die Formel (2) anzuwenden. Die Texturen müssen zu diesem Zeitpunkt fertig sein, daher verwenden wir GL_TEXTURE_FETCH_BARRIER_BIT, bevor wir ApplyTextures () aufrufen.
  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ... rendering transparent objects ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO ist ein Framebuffer, mit dem wir das Bild auf dem Bildschirm anzeigen. In den meisten Fällen ist es 0, in Qt jedoch QOpenGLWidget :: defaultFramebufferObject ().

Bei jedem Aufruf eines Fragment-Shaders haben wir Zugriff auf Farbe und Deckkraft des aktuellen Fragments. In colorTexture muss jedoch eine Summe (und in alphaTexture ein Produkt) dieser Entitäten erscheinen. Dafür verwenden wir Blending. Wenn wir berücksichtigen, dass wir für die erste Textur eine Summe berechnen, während wir für die zweite ein Produkt berechnen, müssen wir für jeden Anhang unterschiedliche Mischungseinstellungen (glBlendFunc und glBlendEquation) bereitstellen.

Hier ist der Inhalt der Funktion PrepareToTransparentRendering ():
  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 


Und Inhalt der Funktion CleanupAfterTransparentRendering ():
  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

In meinem Fragment-Shader steht w für Deckkraft. Das Produkt aus Farbe und w - und w selbst - geht zum ersten Ausgabeparameter und 1 - w geht zum zweiten Ausgabeparameter. Für jeden Ausgabeparameter muss ein Layoutqualifizierer in Form von "location = X" festgelegt werden, wobei X ein Index eines Elements im Anhangsarray ist - das, das wir der Funktion glDrawBuffers gegeben haben. Um genau zu sein, geht der Ausgabeparameter mit location = 0 an die an GL_COLOR_ATTACHMENT1 gebundene Textur und der Parameter mit location = 1 an die an GL_COLOR_ATTACHMENT1 gebundene Textur. Dieselben Zahlen werden in den Funktionen glBlendFunci und glBlendEquationi verwendet, um anzugeben, für welchen Farbanhang wir die Mischparameter festlegen.

Der Fragment-Shader:
 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

In der ApplyTextures () -Funktion zeichnen wir einfach ein Rechteck, das das gesamte Ansichtsfenster abdeckt. Der Fragment-Shader tastet Daten aus allen drei Texturen ab, wobei aktuelle Bildschirmraumkoordinaten als Texturkoordinaten und ein aktueller Beispielindex (gl_SampleID) als Beispielindex für Mehrfachmustertexturen verwendet werden. Das Vorhandensein der Variablen gl_SampleID im Shader-Code veranlasst das System, den Fragment-Shader einmal pro Sample aufzurufen (während er normalerweise einmal pro Pixel aufgerufen wird und seine Ausgabe in alle Samples schreibt, die in ein Grundelement fallen).

Der Vertex-Shader ist einfach trivial:
 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 


Der Fragment-Shader:
 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

Und schließlich - ApplyTextures () -Funktion:
  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 


Am Ende müssen OpenGL-Ressourcen freigegeben werden. Ich mache es im Destruktor meines OpenGL-Widgets:
  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

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


All Articles