Wir werden über „Weighted Blended Order-Independent Transparency“ (im Folgenden: WBOIT) sprechen - die Technik, die 2013 in JCGT beschrieben wurde (
Link ).
Wenn mehrere transparente Objekte auf dem Bildschirm angezeigt werden, hängt die Farbe des Pixels davon ab, welches näher am Betrachter liegt. Hier ist eine bekannte Farbmischformel für diesen Fall:
\ begin {matrix} C = C_ {nah} \ alpha + C_ {fern} (1- \ alpha) && (1) \ end {matrix}
Die Reihenfolge der Fragmentanordnung ist dafür wichtig: Die Farbe des nahen Fragments und seine Opazität werden als C
nah und
α bezeichnet , und die resultierende Farbe aller Fragmente, die sich dahinter befinden, wird als C
weit bezeichnet . Deckkraft ist eine Eigenschaft, die Werte von 0 bis 1 annimmt, wobei 0 bedeutet, dass das Objekt so transparent ist, dass es nicht sichtbar ist, und 1 - dass es so undurchsichtig ist, dass nichts
dahinter sichtbar
ist .
Um diese Formel zu verwenden, müssen Sie zuerst die Fragmente nach Tiefe sortieren. Stellen Sie sich vor, wie viel Kopfschmerzen dies mit sich bringt! Im Allgemeinen sollte die Sortierung in jedem Frame erfolgen. Wenn Sie Objekte sortieren, müssen einige Objekte mit komplexer Form in Stücke geschnitten und nach der Tiefe der geschnittenen Teile sortiert werden (insbesondere bei sich überschneidenden Flächen muss dies unbedingt erfolgen). Wenn Sie die Fragmente sortieren, erfolgt die Sortierung in den Shadern. Dieser Ansatz wird als "Auftragsunabhängige Transparenz" (OIT) bezeichnet und verwendet eine verknüpfte Liste, die im Speicher der Grafikkarte gespeichert ist. Es ist fast unrealistisch, vorherzusagen, wie viel Speicher für diese Liste reserviert werden muss. Und wenn nicht genügend Speicher vorhanden ist, werden Artefakte auf dem Bildschirm angezeigt.
Glück für diejenigen, die steuern können, wie viele durchscheinende Objekte auf der Bühne platziert werden und wo sie relativ zueinander sind. Wenn Sie jedoch CAD ausführen, haben Sie so viele transparente Objekte, wie der Benutzer möchte, und diese werden zufällig lokalisiert.
Jetzt verstehen Sie den Wunsch einiger Menschen, ihr Leben zu vereinfachen und eine Formel zum Mischen von Farben zu entwickeln, die nicht sortiert werden muss. Eine solche Formel steht in dem Artikel, auf den ich am Anfang Bezug genommen habe. Es gibt dort sogar mehrere Formeln, aber die beste laut den Autoren (und meiner Meinung nach auch) ist diese:
\ 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}

Im Screenshot befinden sich Gruppen von durchscheinenden Dreiecken, die sich in vier Tiefen befinden. Links werden sie mit der WBOIT-Technik gerendert. Rechts ist ein Bild zu sehen, das unter Verwendung der Formel (1), der klassischen Farbmischung, unter Berücksichtigung der Reihenfolge der Anordnung der Fragmente erhalten wurde. Als nächstes werde ich es CODB (Classic Order-Dependent Blending) nennen.
Bevor wir mit dem Rendern transparenter Objekte beginnen, müssen wir alle undurchsichtigen Objekte rendern. Danach werden transparente Objekte mit einem Tiefentest gerendert, ohne jedoch in den
glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE);
zu schreiben (dies geschieht wie
glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE);
:
glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE);
). Das heißt, dies geschieht an einem Punkt mit einigen Bildschirmkoordinaten (x, y): Transparente Fragmente, die näher als undurchsichtig sind, bestehen den Tiefentest, unabhängig davon, wie sie sich in der Tiefe relativ zu bereits gezeichneten transparenten Fragmenten befinden, und transparente Fragmente, die weiter erscheinen undurchsichtig, bestehen Sie den Tiefentest nicht und werden dementsprechend verworfen.
C
0 in Formel (2) ist die Farbe eines undurchsichtigen Fragments, auf das transparente Fragmente gezeichnet sind, von denen wir n Stücke haben, die durch die Indizes 1 bis n angegeben sind. C
i ist die Farbe des i-ten transparenten Fragments,
α i ist seine Opazität.
Wenn Sie genau hinschauen, ist Formel (2) ein bisschen wie Formel (1). Wenn Sie sich das vorstellen

Ist C
nah , C
0 ist C
fern und

- das ist
α , dann ist dies die 1. Formel, eins zu eins. Und wirklich

- Dies ist der gewichtete Durchschnitt der Farben der transparenten Fragmente (der Schwerpunkt wird in der Mechanik nach derselben Formel bestimmt). Dies ist die Farbe des nächsten Fragments C in der
Nähe . C
0 ist die Farbe des undurchsichtigen Fragments, das sich hinter allen Fragmenten befindet, für die wir diesen gewichteten Durchschnitt berechnet haben, und es wird für C
weit durchgehen . Das heißt, wir haben alle transparenten Fragmente durch ein „gemitteltes“ Fragment ersetzt und die Standardformel zum Mischen von Farben angewendet - Formel (1). Was ist diese listige Formel für
α , die uns die Autoren des Originalartikels anbieten?
Dies ist eine Skalarfunktion im n-dimensionalen Raum. Erinnern wir uns also an die Differentialanalyse der Funktionen mehrerer Variablen. Da alle
α i zum Bereich von 0 bis 1 gehören, ist die partielle Ableitung in Bezug auf eine der Variablen immer eine nicht negative Konstante. Dies bedeutet, dass die Opazität des „durchschnittlichen“ Fragments mit zunehmender Opazität eines der transparenten Fragmente zunimmt, und genau das brauchen wir. Außerdem steigt sie linear an.
Wenn die Deckkraft eines Fragments 0 ist, ist es überhaupt nicht sichtbar und hat keinen Einfluss auf die resultierende Farbe.
Wenn die Opazität von mindestens einem Fragment 1 ist, ist
α 1. Das heißt, das opake Fragment wird unsichtbar, was im Allgemeinen gut ist. Nur die transparenten Fragmente hinter dem Fragment mit der Deckkraft = 1 leuchten noch durch und beeinflussen die resultierende Farbe:

Hier liegt ein orangefarbenes Dreieck darüber, grün darunter und grau und cyan unter grün, und das alles vor einem schwarzen Hintergrund. Blaue Deckkraft = 1, alle anderen - 0,5. Das Bild rechts ist so, wie es sein sollte. Wie Sie sehen können, sieht WBOIT ekelhaft aus. Der einzige Ort, an dem die normale orange Farbe erscheint, ist der Rand des grünen Dreiecks, umgeben von einer undurchsichtigen weißen Linie. Wie ich gerade sagte, ist ein undurchsichtiges Fragment unsichtbar, wenn die Deckkraft des transparenten Fragments 1 beträgt.
Dies ist hier noch besser zu sehen:

Das orangefarbene Dreieck hat eine Deckkraft von 1, das grüne mit ausgeschalteter Transparenz wird einfach mit den undurchsichtigen Objekten gezeichnet. Es sieht so aus, als ob das GRÜNE Dreieck durch ORANGE durch das orangefarbene Dreieck scheint.
Um das Bild anständig aussehen zu lassen, ist es am einfachsten, Objekten keine hohe Deckkraft zuzuweisen. In meinem Arbeitsprojekt darf die Deckkraft nicht größer als 0,5 eingestellt werden. Hierbei handelt es sich um 3D-CAD, bei dem Objekte schematisch gezeichnet werden und kein besonderer Realismus erforderlich ist. Daher ist eine solche Einschränkung dort zulässig.
Bei niedrigen Deckkraftwerten sehen die Bilder links und rechts fast gleich aus:

Und mit hoch unterscheiden sie sich deutlich:

So sieht ein transparentes Polyeder aus:


Das Polyeder hat orange seitliche und grüne horizontale Flächen. Leider werden Sie dies auf den ersten Blick nicht verstehen, d. H. Das Bild sieht nicht überzeugend aus. Wo sich vorne eine orangefarbene Wand befindet, brauchen Sie mehr als Orange, und wo Grün mehr als Grün ist. Es ist viel besser, Gesichter in einer Farbe zu zeichnen:

Tiefenbasiertes WBOIT
Um die fehlende Sortierung nach Tiefe irgendwie auszugleichen, haben die Autoren des Artikels verschiedene Optionen zum Hinzufügen von Tiefe zu Formel (2) entwickelt. Dies erschwert die Implementierung und das Ergebnis ist weniger vorhersehbar und hängt von den Eigenschaften einer bestimmten dreidimensionalen Szene ab. Ich habe mich nicht mit diesem Thema befasst, also wen interessiert das? Ich schlage vor, den Artikel zu lesen.
Es wird argumentiert, dass WBOIT manchmal zu etwas fähig ist, was die klassische Sortiertransparenz nicht kann. Zum Beispiel zeichnen Sie Rauch als Partikelsystem mit nur zwei Partikeln - mit dunklem und hellem Rauch. Wenn ein Partikel ein anderes passiert, führt die klassische Farbmischung mit Sortierung zu einem hässlichen Ergebnis - die Farbe des Rauches aus dem Licht wird scharf dunkel. Der Artikel besagt, dass tiefenempfindliches WBOIT einen reibungslosen Übergang ermöglicht und glaubwürdiger aussieht. Gleiches gilt für das Modellieren von Fell und Haaren in Form dünner Röhren.
Code
Nun zur Implementierung von Formel (2) in OpenGL. Der Beispielcode befindet sich auf Github (
Link ), und die meisten Bilder im Artikel stammen von dort. Du kannst meine Dreiecke sammeln und damit spielen. Das Qt-Framework wird verwendet.
Für diejenigen, die gerade erst anfangen, das Rendern transparenter Objekte zu studieren, empfehle ich diese beiden Artikel:
→
OpenGL lernen. Lektion 4.3 - Farben mischen→
Auftragsunabhängiger Transparenzalgorithmus mit verknüpften Listen in Direct3D 11 und OpenGL 4Das zweite ist jedoch nicht so wichtig für das Verständnis dieses Materials, aber das erste ist ein Muss.
Zur Berechnung der Formel (2) benötigen wir 2 zusätzliche Framebuffer, 3 Multisample-Texturen und einen Renderpuffer, in den wir die Tiefe schreiben. In der ersten Textur - colorTextureNT (NT bedeutet nicht transparent) - werden undurchsichtige Objekte gerendert. Es hat den Typ GL_RGB10_A2. Die zweite Textur (colorTexture) ist vom Typ GL_RGBA16F; In den ersten 3 Komponenten dieser Textur schreiben wir dieses Stück Formel (2):

im vierten -

. Eine andere Textur vom Typ GL_R16 (alphaTexture) enthält

.
Zuerst müssen Sie diese Objekte erstellen, um ihre Bezeichner von OpenGL zu 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 gesagt, hier wird das Qt-Framework verwendet, und alle OpenGL-Aufrufe durchlaufen ein Objekt vom Typ QOpenGLFunctions_4_5_Core, das für mich immer als f bezeichnet wird.
Jetzt sollten Sie Speicher zuweisen:
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 );
Und konfigurieren Sie Framebuffer:
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 );
Beim zweiten Rendering-Durchlauf wird die Ausgabe des Fragment-Shaders gleichzeitig an zwei Texturen gesendet, und dies muss explizit mit glDrawBuffers angegeben werden.
Der größte Teil dieses Codes wird beim Programmstart einmal ausgeführt. Code, der Speicher für Texturen und Renderpuffer zuweist, wird bei jeder Größenänderung des Fensters aufgerufen. Als nächstes kommt der Rendering-Code, der jedes Mal aufgerufen wird, wenn das Fenster neu gezeichnet wird.
f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT);
Wir haben gerade alle undurchsichtigen Objekte auf die colorTextureNT-Textur gezeichnet und die Tiefen in den Renderpuffer geschrieben. Bevor Sie in der nächsten Phase des Zeichnens denselben Renderpuffer verwenden, müssen Sie sicherstellen, dass alle Tiefen undurchsichtiger Objekte bereits dort geschrieben sind. Hierzu wird GL_FRAMEBUFFER_BARRIER_BIT verwendet. Nach dem Rendern transparenter Objekte rufen wir die Funktion ApplyTextures () auf, mit der die letzte Phase des Renderns gestartet wird, in der der Fragment-Shader Daten aus den Texturen colorTextureNT, colorTexture und alphaTexture liest, um Formel (2) anzuwenden. Die Texturen sollten bis dahin vollständig geschrieben sein. Vor dem Aufruf von ApplyTextures () verwenden wir GL_TEXTURE_FETCH_BARRIER_BIT.
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(); {
defaultFBO ist der Framebuffer, über den das Bild angezeigt wird. In den meisten Fällen ist es 0, in Qt jedoch QOpenGLWidget :: defaultFramebufferObject ().
Jedes Mal, wenn der Fragment-Shader aufgerufen wird, erhalten wir Informationen über die Farbe und Deckkraft des aktuellen Fragments. Bei der Ausgabe in der colorTexture-Textur möchten wir jedoch die Summe (und in der alphaTexture-Textur das Produkt) einiger Funktionen dieser Größen erhalten. Hierfür wird das Mischen verwendet. Da wir für die erste Textur die Summe und für die zweite - das Produkt - berechnen, müssen die Mischungseinstellungen (glBlendFunc und glBlendEquation) für jeden Anhang separat festgelegt werden.
Hier ist der Inhalt der PrepareToTransparentRendering () -Funktion:
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 der Inhalt der Funktion CleanupAfterTransparentRendering ():
f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND);
In meinem Fragment-Shader wird die Deckkraft durch den Buchstaben w angezeigt. Das Produkt der Farbe von w und w selbst geben wir an einen Ausgabeparameter und 1 - w an einen anderen aus. Für jeden Ausgabeparameter wird ein Layoutqualifizierer in der Form "location = X" festgelegt, wobei X der Index des Elements im Anhangsarray ist, das wir in der dritten Liste an glDrawBuffers übergeben haben (insbesondere wird der Ausgabeparameter mit location = 0 an die an GL_COLOR_ATTACHMENT0 gebundene Textur gesendet und der Parameter mit location = 1 - in der an GL_COLOR_ATTACHMENT1) angehängten Textur. Die gleichen Nummern werden in den Funktionen glBlendFunci und glBlendEquationi verwendet, um die Anhangsnummer anzugeben, für die wir die Mischparameter festlegen.
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 über das gesamte Fenster. Der Fragment-Shader fordert Daten von allen von uns erstellten Texturen an, wobei die aktuellen Bildschirmkoordinaten als Texturkoordinaten und die aktuelle Probennummer (gl_SampleID) als Probennummer in der Multisample-Textur verwendet werden. Wenn Sie die Variable gl_SampleID im Shader verwenden, wird der Modus automatisch aktiviert, wenn der Fragment-Shader für jedes Sample einmal aufgerufen wird (unter normalen Bedingungen wird er einmal für das gesamte Pixel aufgerufen und das Ergebnis wird in alle Samples geschrieben, die sich innerhalb des Grundelements befanden).
Der Vertex-Shader enthält nichts Bemerkenswertes:
#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); }
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 der Inhalt der 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);
Nun, es wäre schön, OpenGL-Ressourcen freizugeben, nachdem es vorbei ist. Ich habe diesen Code im Destruktor meines OpenGL-Widgets aufgerufen:
f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture);