Lerne OpenGL. Lektion 5.10 - Umgebungsokklusion des Bildschirmbereichs

OGL3

SSAO


Das Thema Hintergrundbeleuchtung wurde von uns in einer Lektion ĂŒber die Grundlagen der Beleuchtung angesprochen, jedoch nur nebenbei. Ich möchte Sie daran erinnern: Die Hintergrundkomponente der Beleuchtung ist im Wesentlichen ein konstanter Wert, der zu allen Berechnungen der Szenenbeleuchtung hinzugefĂŒgt wird, um den Prozess der Lichtstreuung zu simulieren. In der realen Welt erfĂ€hrt das Licht viele Reflexionen mit unterschiedlicher IntensitĂ€t, was zu einer ebenso ungleichmĂ€ĂŸigen Beleuchtung indirekt beleuchteter Teile der Szene fĂŒhrt. Offensichtlich ist eine Fackel mit konstanter IntensitĂ€t nicht sehr plausibel.

Eine Art der ungefĂ€hren Berechnung der Schattierung durch indirekte Beleuchtung ist der Umgebungsokklusionsalgorithmus (AO ), der die DĂ€mpfung der indirekten Beleuchtung in der NĂ€he von Ecken, Falten und anderen OberflĂ€chenunregelmĂ€ĂŸigkeiten simuliert. Solche Elemente ĂŒberlappen sich im Allgemeinen erheblich mit der angrenzenden Geometrie und lassen daher weniger Lichtstrahlen nach außen entweichen, wodurch diese Bereiche verdeckt werden.

Nachfolgend finden Sie einen Vergleich des Renderns ohne und unter Verwendung des AO-Algorithmus. Achten Sie darauf, wie die IntensitĂ€t der Hintergrundbeleuchtung in der NĂ€he der Ecken der WĂ€nde und anderer scharfer BrĂŒche in der OberflĂ€che abnimmt:


Obwohl der Effekt nicht sehr auffĂ€llig ist, fĂŒgt das Vorhandensein des Effekts in der gesamten Szene Realismus hinzu, da durch kleine Details des Selbstschattierungseffekts eine zusĂ€tzliche Tiefenillusion entsteht.


Es ist erwĂ€hnenswert, dass die Algorithmen zur Berechnung von AO ziemlich ressourcenintensiv sind, da sie eine Analyse der umgebenden Geometrie erfordern. In einer naiven Implementierung wĂ€re es möglich, einfach an jedem Punkt der OberflĂ€che viele Strahlen zu emittieren und den Grad der Abschattung zu bestimmen. Dieser Ansatz erreicht jedoch sehr schnell die fĂŒr interaktive Anwendungen akzeptable ressourcenintensive Grenze. GlĂŒcklicherweise veröffentlichte Crytek 2007 ein Papier, in dem sein eigener Ansatz zur Implementierung des in der Release-Version von Crysis verwendeten SSAO- Algorithmus (Screen-Space Ambient Occlusion ) beschrieben wurde. Der Ansatz berechnete den Grad der Abschattung im Bildschirmbereich, wobei nur der aktuelle Tiefenpuffer anstelle von realen Daten ĂŒber die umgebende Geometrie verwendet wurde. Eine solche Optimierung beschleunigte den Algorithmus im Vergleich zur Referenzimplementierung radikal und lieferte gleichzeitig meist plausible Ergebnisse, was diesen Ansatz der ungefĂ€hren Berechnung der Hintergrundschattierung zu einer Standard-De-facto-Industrie machte.

Das Prinzip, auf dem der Algorithmus basiert, ist recht einfach: FĂŒr jedes Fragment eines Vollbild-Quad wird der Okklusionsfaktor basierend auf den Tiefenwerten der umgebenden Fragmente berechnet. Der berechnete Schattierungskoeffizient wird dann verwendet, um die IntensitĂ€t der Hintergrundbeleuchtung zu reduzieren (bis zum vollstĂ€ndigen Ausschluss). Um einen Koeffizienten zu erhalten, mĂŒssen Tiefendaten von mehreren Proben aus dem das betreffende Fragment umgebenden sphĂ€rischen Bereich gesammelt und diese Tiefenwerte mit der Tiefe des betreffenden Fragments verglichen werden. Die Anzahl der Proben mit einer Tiefe, die grĂ¶ĂŸer als das aktuelle Fragment ist, bestimmt direkt den Schattierungskoeffizienten. Schauen Sie sich dieses Diagramm an:


Hier liegt jeder graue Punkt innerhalb eines bestimmten geometrischen Objekts und trÀgt daher zum Wert des Schattierungskoeffizienten bei. Je mehr Proben sich in der Geometrie der umgebenden Objekte befinden, desto geringer ist die RestintensitÀt der Hintergrundschattierung in diesem Bereich.

Offensichtlich hĂ€ngt die QualitĂ€t und der Realismus des Effekts direkt von der Anzahl der entnommenen Proben ab. Bei einer kleinen Anzahl von Abtastwerten nimmt die Genauigkeit des Algorithmus ab und fĂŒhrt aufgrund abrupter ÜbergĂ€nge zwischen Regionen mit sehr unterschiedlichen Schattierungskoeffizienten zum Auftreten eines Streifen- oder " Streifen " -Artefakts. Eine große Anzahl von Samples beeintrĂ€chtigt einfach die Leistung. Die Randomisierung des Kerns der Proben ermöglicht etwas Ă€hnliche Ergebnisse, um die Anzahl der erforderlichen Proben geringfĂŒgig zu verringern. Eine Neuorientierung durch Rotation eines Satzes von Abtastvektoren auf einen zufĂ€lligen Winkel ist impliziert. Die EinfĂŒhrung der ZufĂ€lligkeit bringt jedoch sofort ein neues Problem in Form eines wahrnehmbaren Rauschmusters mit sich, das die Verwendung von UnschĂ€rfefiltern erfordert, um das Ergebnis zu glĂ€tten. Nachfolgend finden Sie ein Beispiel fĂŒr den Algorithmus (Autor - John Chapman ) und seine typischen Probleme: Streifenbildung und Rauschmuster.


Wie zu sehen ist, wird eine merkliche Streifenbildung aufgrund der geringen Anzahl von Proben durch EinfĂŒhren einer Randomisierung der Orientierung der Proben gut entfernt.

Cryteks spezifische SSAO-Implementierung hatte einen erkennbaren visuellen Stil. Da Crytek-Spezialisten einen kugelförmigen Kern der Probe verwendeten, wirkte sich dies sogar auf flache OberflÀchen wie WÀnde aus und machte sie schattiert, da die HÀlfte des Volumens des Kerns der Probe unter die Geometrie getaucht war. Unten sehen Sie einen Screenshot einer Szene aus Crysis in Graustufen, basierend auf dem Wert des Schattierungsfaktors. Hier ist der Effekt von "Grauheit" deutlich sichtbar:


Um diesen Effekt zu vermeiden, bewegen wir uns vom kugelförmigen Kern der Probe zu einer Halbkugel, die entlang der Normalen zur OberflÀche ausgerichtet ist:


Bei der Probenahme von einer solchen normal ausgerichteten Halbkugel mĂŒssen wir bei der Berechnung des Schattierungskoeffizienten keine Fragmente berĂŒcksichtigen, die unter der OberflĂ€che der angrenzenden OberflĂ€che liegen. Dieser Ansatz beseitigt unnötige Schattierungen und fĂŒhrt im Allgemeinen zu realistischeren Ergebnissen. Diese Lektion verwendet den HemisphĂ€re-Ansatz und etwas verfeinerten Code aus der brillanten SSAO-Lektion von John Chapman .

Rohdatenpuffer


Die Berechnung des Schattierungsfaktors in jedem Fragment erfordert die VerfĂŒgbarkeit von Daten ĂŒber die umgebende Geometrie. Insbesondere benötigen wir folgende Daten:

  • Positionsvektor fĂŒr jedes Fragment;
  • Normaler Vektor fĂŒr jedes Fragment;
  • Diffuse Farbe fĂŒr jedes Fragment;
  • Der Kern der Probe
  • Ein zufĂ€lliger Rotationsvektor fĂŒr jedes Fragment, das bei der Neuorientierung des Probenkerns verwendet wird.

Mithilfe von Daten zu den Koordinaten des Fragments im Artenraum können wir die Halbkugel des Probenkerns entlang des im Artenraum fĂŒr das aktuelle Fragment angegebenen Normalenvektors ausrichten. Dann wird der resultierende Kern verwendet, um Proben mit verschiedenen Offsets aus einer Textur zu erstellen, die Daten auf den Koordinaten von Fragmenten speichert. Wir machen viele Proben in jedem Fragment und vergleichen fĂŒr jede Probe, die wir machen, seinen Tiefenwert mit dem Tiefenwert aus dem Fragmentkoordinatenpuffer, um das Ausmaß der Schattierung abzuschĂ€tzen. Der resultierende Wert wird dann verwendet, um den Beitrag der Hintergrundkomponente bei der endgĂŒltigen Beleuchtungsberechnung zu begrenzen. Mit einem fragmentweisen zufĂ€lligen Rotationsvektor können wir die erforderliche Anzahl von Proben erheblich reduzieren, um ein anstĂ€ndiges Ergebnis zu erzielen. Dies wird dann demonstriert.


Da SSAO ein im Bildschirmbereich realisierter Effekt ist, ist es möglich, eine direkte Berechnung durch Rendern eines Vollbild-Quad durchzufĂŒhren. Dann haben wir aber keine Daten zur Geometrie der Szene. Um diese EinschrĂ€nkung zu umgehen, werden alle erforderlichen Informationen in der Textur gerendert, die spĂ€ter im SSAO-Shader verwendet werden, um auf geometrische und andere Informationen ĂŒber die Szene zuzugreifen. Wenn Sie diese Lektionen sorgfĂ€ltig befolgt haben, sollten Sie im beschriebenen Ansatz bereits das Erscheinungsbild des Algorithmus fĂŒr verzögerte Schattierung kennen. Dies ist vor allem der Grund, warum der SSAO-Effekt als Native im Rendering mit verzögerter Schattierung angezeigt wird - schließlich sind Texturen, die Koordinaten und Normalen speichern, bereits im G-Puffer verfĂŒgbar.

In dieser Lektion wird der Effekt zusĂ€tzlich zu einer leicht vereinfachten Version des Codes aus der Lektion ĂŒber verzögerte Beleuchtung implementiert. Wenn Sie sich noch nicht mit den Prinzipien der verzögerten Beleuchtung vertraut gemacht haben, empfehle ich Ihnen dringend, sich dieser Lektion zuzuwenden.

Da der Zugriff auf Fragmentinformationen ĂŒber Koordinaten und Normalen aufgrund des G-Puffers bereits verfĂŒgbar sein sollte, ist der Fragment-Shader der Geometrieverarbeitungsstufe recht einfach:

#version 330 core layout (location = 0) out vec4 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; void main() { //        gPosition = FragPos; //       gNormal = normalize(Normal); //    -   gAlbedoSpec.rgb = vec3(0.95); } 

Da der SSAO-Algorithmus eine Auswirkung auf den Bildschirmbereich hat und der Schattierungsfaktor basierend auf dem sichtbaren Bereich der Szene berechnet wird, ist es sinnvoll, Berechnungen im Ansichtsbereich durchzufĂŒhren. In diesem Fall speichert die vom Vertex-Shader erhaltene FragPos- Variable die Position genau im Ansichtsfenster. Es lohnt sich sicherzustellen, dass die Koordinaten und Normalen im G-Puffer im Ansichtsraum gespeichert sind, da alle weiteren Berechnungen darin durchgefĂŒhrt werden.

Es besteht die Möglichkeit, den Positionsvektor nur auf der Grundlage einer bekannten Fragmenttiefe und einer bestimmten Menge mathematischer Magie wiederherzustellen, die beispielsweise in Matt Pettineos Blog beschrieben wird . Dies erfordert natĂŒrlich einen hohen Rechenaufwand, macht jedoch das Speichern von Positionsdaten im G-Puffer ĂŒberflĂŒssig, was viel Videospeicher beansprucht. Der Einfachheit halber werden wir diesen Ansatz jedoch dem persönlichen Studium ĂŒberlassen.

Die gPosition- Farbpuffertextur ist wie folgt konfiguriert:

 glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); 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_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 

Diese Textur speichert die Koordinaten von Fragmenten und kann verwendet werden, um Tiefendaten fĂŒr jeden Punkt aus dem Kern der Proben zu erhalten. Ich stelle fest, dass die Textur ein Gleitkomma-Datenformat verwendet - dadurch können die Koordinaten von Fragmenten nicht auf das Intervall [0., 1.] reduziert werden. Beachten Sie auch den Wiederholungsmodus - GL_CLAMP_TO_EDGE ist eingestellt. Dies ist erforderlich, um die Möglichkeit auszuschließen, dass der Bildschirmbereich nicht absichtlich ĂŒberabgetastet wird. Wenn Sie ĂŒber das Hauptintervall der Texturkoordinaten hinausgehen, erhalten Sie falsche Positions- und Tiefendaten.

Als nÀchstes werden wir uns mit der Bildung eines halbkugelförmigen Kerns der Proben und der Schaffung einer Methode zur zufÀlligen Orientierung befassen.

Schaffung einer normal ausgerichteten HemisphÀre


Die Aufgabe besteht also darin, eine Reihe von Probenpunkten zu erstellen, die sich innerhalb einer Halbkugel befinden, die entlang der Normalen zur OberflĂ€che ausgerichtet ist. Da die Erstellung eines Beispielkerns fĂŒr alle möglichen Richtungen der Normalen rechnerisch nicht erreichbar ist, verwenden wir den Übergang zum Tangentenraum , in dem die Normalen immer als Vektor in Richtung der positiven Halbachse Z dargestellt werden.


Unter der Annahme, dass der Radius der Halbkugel ein einzelner Prozess ist, sieht die Bildung eines Kerns einer Stichprobe von 64 Punkten folgendermaßen aus:

 //      0.0 - 1.0 std::uniform_real_distribution<float> randomFloats(0.0, 1.0); std::default_random_engine generator; std::vector<glm::vec3> ssaoKernel; for (unsigned int i = 0; i < 64; ++i) { glm::vec3 sample( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) ); sample = glm::normalize(sample); sample *= randomFloats(generator); float scale = (float)i / 64.0; ssaoKernel.push_back(sample); } 

Hier wĂ€hlen wir zufĂ€llig die x- und y- Koordinaten im Intervall [-1., 1.] und die z- Koordinate im Intervall [0., 1.] aus (wenn das Intervall das gleiche wie fĂŒr x und y ist , wĂŒrden wir einen sphĂ€rischen Kern erhalten Probenahme). Die resultierenden Probenvektoren sind auf Halbkugeln beschrĂ€nkt, da der Kern der Probe letztendlich entlang der Normalen zur OberflĂ€che ausgerichtet ist.

Im Moment sind alle Stichprobenpunkte zufĂ€llig im Kern verteilt, aber aus GrĂŒnden der QualitĂ€t des Effekts sollten die Stichproben, die nĂ€her am Ursprung des Kernels liegen, einen grĂ¶ĂŸeren Beitrag zur Berechnung des Schattierungskoeffizienten leisten. Dies kann durch Ändern der Verteilung der gebildeten Probenpunkte durch Erhöhen ihrer Dichte in der NĂ€he des Ursprungs realisiert werden. Diese Aufgabe kann einfach mit der Beschleunigungsinterpolationsfunktion ausgefĂŒhrt werden:

 scale = lerp(0.1f, 1.0f, scale * scale); sample *= scale; ssaoKernel.push_back(sample); } 

Die Funktion lerp () ist definiert als:

 float lerp(float a, float b, float f) { return a + f * (b - a); } 

Ein solcher Trick ergibt eine modifizierte Verteilung, bei der die meisten Stichprobenpunkte nahe dem Ursprung des Kernels liegen.


Jeder der erhaltenen Probenvektoren wird verwendet, um die Koordinate des Fragments im Speziesraum zu verschieben, um Daten ĂŒber die umgebende Geometrie zu erhalten. Um bei der Arbeit im Ansichtsfenster anstĂ€ndige Ergebnisse zu erzielen, benötigen Sie möglicherweise eine beeindruckende Anzahl von Samples, die sich zwangslĂ€ufig auf die Leistung auswirken. Die EinfĂŒhrung von pseudozufĂ€lligem Rauschen oder die Drehung der Abtastvektoren in jedem verarbeiteten Fragment verringert jedoch die erforderliche Anzahl von Abtastwerten mit vergleichbarer QualitĂ€t erheblich.

ZufÀllige Drehung des Probenkerns


Die EinfĂŒhrung einer ZufĂ€lligkeit bei der Verteilung der Punkte im Kern der Stichprobe kann daher die Anforderung an die Anzahl dieser Punkte erheblich verringern, um einen angemessenen QualitĂ€tseffekt zu erzielen. Es wĂ€re möglich, einen zufĂ€lligen Rotationsvektor fĂŒr jedes Fragment der Szene zu erstellen, aber der Speicher ist zu teuer. Es ist effizienter, eine kleine Textur zu erstellen, die einen Satz zufĂ€lliger Rotationsvektoren enthĂ€lt, und sie dann einfach mit dem Wiederholungsmodus GL_REPEAT zu verwenden .

Erstellen Sie ein 4x4-Array und fĂŒllen Sie es mit zufĂ€lligen Rotationsvektoren, die entlang des Normalenvektors im Tangentenraum ausgerichtet sind:

 std::vector<glm::vec3> ssaoNoise; for (unsigned int i = 0; i < 16; i++) { glm::vec3 noise( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise); } 

Da der Kern entlang der positiven Halbachse Z im Tangentenraum ausgerichtet ist, belassen wir die z- Komponente gleich Null - dies stellt eine Drehung nur um die Z- Achse sicher.

Erstellen Sie als NĂ€chstes eine 4x4-Textur und fĂŒllen Sie sie mit unserem Array von Rotationsvektoren. Stellen Sie sicher, dass Sie den GL_REPEAT- Wiedergabemodus fĂŒr Texturkacheln verwenden :

 unsigned int noiseTexture; glGenTextures(1, &noiseTexture); glBindTexture(GL_TEXTURE_2D, noiseTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

Nun haben wir alle Daten, die fĂŒr die direkte Implementierung des SSAO-Algorithmus erforderlich sind!

Shader SSAO


FĂŒr jedes Fragment eines Vollbild-Quad wird ein Effekt-Shader ausgefĂŒhrt, der den Schattenkoeffizienten in jedem Fragment berechnet. Da die Ergebnisse in einer anderen Rendering-Phase verwendet werden, in der die endgĂŒltige Beleuchtung erstellt wird, mĂŒssen Sie ein weiteres Framebuffer-Objekt erstellen, um das Ergebnis des Shaders zu speichern:

 unsigned int ssaoFBO; glGenFramebuffers(1, &ssaoFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); unsigned int ssaoColorBuffer; glGenTextures(1, &ssaoColorBuffer); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0); 

Da das Ergebnis des Algorithmus die einzige reelle Zahl innerhalb von [0., 1.] ist, reicht es fĂŒr die Speicherung aus, eine Textur mit der einzigen verfĂŒgbaren Komponente zu erstellen. Deshalb wird GL_RED als internes Format fĂŒr den Farbpuffer festgelegt.

Im Allgemeinen sieht der Renderprozess der SSAO-Stufe ungefĂ€hr so ​​aus:

 //  :  G- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); [...] glBindFramebuffer(GL_FRAMEBUFFER, 0); //  G-      SSAO glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, noiseTexture); shaderSSAO.use(); SendKernelSamplesToShader(); shaderSSAO.setMat4("projection", projection); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); //  :    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shaderLightingPass.use(); [...] glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); [...] RenderQuad(); 

Der shaderSSAO- Shader akzeptiert die benötigten G-Buffer-Texturen als Eingabe sowie die Rauschtextur und den Sample-Kern:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D texNoise; uniform vec3 samples[64]; uniform mat4 projection; //             //      1280x720 const vec2 noiseScale = vec2(1280.0/4.0, 720.0/4.0); void main() { [...] } 

Beachten Sie die Variable NoiseScale . Unsere kleine Textur mit Rauschen sollte ĂŒber die gesamte OberflĂ€che des Bildschirms gekachelt werden. Da die TexCoords- Texturkoordinaten jedoch innerhalb von [0., 1.] liegen, geschieht dies nicht ohne unser Eingreifen. FĂŒr diese Zwecke berechnen wir den Faktor fĂŒr Texturkoordinaten, der sich als VerhĂ€ltnis der BildschirmgrĂ¶ĂŸe zur GrĂ¶ĂŸe der Rauschtextur ergibt:

 vec3 fragPos = texture(gPosition, TexCoords).xyz; vec3 normal = texture(gNormal, TexCoords).rgb; vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 

Da wir beim Erstellen der texNoise- Rauschtextur den Wiederholungsmodus auf GL_REPEAT gesetzt haben , wird er jetzt auf der BildschirmoberflÀche viele Male wiederholt. Mit randomVec , fragPos und Normalwerten können wir eine TBN-Transformationsmatrix vom Tangenten- zum Artenraum erstellen:

 vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); 

Mit dem Gram-Schmidt-Prozess erstellen wir eine orthogonale Basis, die in jedem Fragment basierend auf dem Zufallswert randomVec zufĂ€llig geneigt ist . Ein wichtiger Punkt: Da es in diesem Fall fĂŒr uns nicht wichtig ist, dass die TBN-Matrix genau entlang der OberflĂ€che des Dreiecks ausgerichtet ist (wie im Fall der Parallaxenabbildung, ca. Per.), Benötigen wir keine vorberechneten Tangenten- und Bi-Tangenten-Daten.

Als nĂ€chstes gehen wir durch das Array des Probenkerns, ĂŒbersetzen jeden Probenvektor vom Tangentenraum in den Speziesraum und erhalten seine Summe mit der aktuellen Position des Fragments. Dann vergleichen wir den Tiefenwert der resultierenden Menge mit dem Tiefenwert, der durch Abtasten aus der entsprechenden G-Puffer-Textur erhalten wird.

WĂ€hrend es verwirrend klingt, gehen wir die Schritte durch:

 float occlusion = 0.0; for(int i = 0; i < kernelSize; ++i) { //     vec3 sample = TBN * samples[i]; //      - sample = fragPos + sample * radius; [...] } 

KernelSize und Radius sind hier Variablen, die die Eigenschaften des Effekts steuern. In diesem Fall sind sie 64 bzw. 0,5. Bei jeder Iteration ĂŒbersetzen wir den Probenkernvektor in den Speziesraum. Als nĂ€chstes addieren wir zum erhaltenen Wert der Verschiebung der Probe im Artenraum den Wert der Position des Fragments im Artenraum. In diesem Fall wird der Versatzwert mit der Radiusvariablen multipliziert, die den Radius des Kerns der SSAO-Effektprobe steuert.

Nach diesen Schritten sollten wir den resultierenden Probenvektor in den Bildschirmbereich konvertieren, damit wir aus der G-Puffer-Textur auswÀhlen können, in der die Positionen und Tiefen von Fragmenten unter Verwendung des erhaltenen projizierten Werts gespeichert werden. Da sich das Beispiel im Ansichtsfenster befindet, benötigen wir die Projektionsprojektionsmatrix:

 vec4 offset = vec4(sample, 1.0); offset = projection * offset; //     offset.xyz /= offset.w; //   offset.xyz = offset.xyz * 0.5 + 0.5; //    [0., 1.] 

Nach der Konvertierung in den Clip-Bereich fĂŒhren wir die Perspektiventeilung manuell durch, indem wir einfach die xyz- Komponenten durch die w- Komponente teilen. Der resultierende Vektor in normalisierten GerĂ€tekoordinaten ( NDC ) wird in das Werteintervall [0., 1.] ĂŒbersetzt, damit er als Texturkoordinaten verwendet werden kann:

 float sampleDepth = texture(gPosition, offset.xy).z; 

Wir verwenden die xy- Komponenten des Probenvektors , um aus der Textur die Positionen des G-Puffers auszuwĂ€hlen. Wir erhalten den Tiefenwert ( z- Komponenten), der dem Probenvektor entspricht, wenn wir ihn von der Position des Beobachters aus betrachten (dies ist das erste nicht abgeschirmte sichtbare Fragment). Wenn gleichzeitig die erhaltene Abtasttiefe grĂ¶ĂŸer als die gespeicherte Tiefe ist, erhöhen wir den Schattierungskoeffizienten:

 occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0); 

Beachten Sie den Bias- Offset, der zur ursprĂŒnglichen Fragmenttiefe hinzugefĂŒgt wird (im Beispiel auf 0,025 eingestellt). Dieser Versatz ist nicht immer erforderlich, aber das Vorhandensein einer Variablen ermöglicht es Ihnen, das Aussehen des SSAO-Effekts zu steuern und in bestimmten Situationen auch Probleme mit Welligkeiten in schattierten Bereichen zu beseitigen.

Dies ist jedoch noch nicht alles, da eine solche Implementierung zu auffĂ€lligen Artefakten fĂŒhrt. Es manifestiert sich in FĂ€llen, in denen ein Fragment betrachtet wird, das nahe dem Rand einer bestimmten OberflĂ€che liegt. In solchen Situationen erfasst der Algorithmus beim Vergleich der Tiefen unweigerlich die Tiefen von OberflĂ€chen, die sehr weit hinter der betrachteten liegen können. An diesen Stellen erhöht der Algorithmus fĂ€lschlicherweise den Grad der Abschattung erheblich, wodurch an den RĂ€ndern von Objekten wahrnehmbare dunkle Lichthöfe entstehen. Das Artefakt wird durch EinfĂŒhrung einer zusĂ€tzlichen EntfernungsprĂŒfung behandelt (ein Beispiel von John Chapman ):


Die PrĂŒfung begrenzt den Beitrag zum Schattierungskoeffizienten nur fĂŒr Tiefenwerte, die innerhalb des Radius der Probe liegen:

 float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth)); occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0) * rangeCheck; 

Wir verwenden auch die GLSL-Funktion glattstep () , die eine reibungslose Interpolation des dritten Parameters zwischen dem ersten und dem zweiten implementiert. Gleichzeitig wird 0 zurĂŒckgegeben, wenn der dritte Parameter kleiner oder gleich dem ersten ist, oder 1, wenn der dritte Parameter grĂ¶ĂŸer oder gleich dem zweiten ist. Wenn der Tiefenunterschied innerhalb des Radius liegt , wird sein Wert im Intervall [0., 1.] gemĂ€ĂŸ dieser Kurve gleichmĂ€ĂŸig geglĂ€ttet:


Wenn wir bei der ÜberprĂŒfung der Tiefe klare Grenzen verwenden wĂŒrden, wĂŒrden Artefakte in Form scharfer Grenzen an den Stellen hinzugefĂŒgt, an denen die Werte der Tiefenunterschiede außerhalb der Grenzen des Radius liegen .

Mit dem letzten Schliff normalisieren wir den Wert des Schattierungskoeffizienten anhand der GrĂ¶ĂŸe des Probenkerns und zeichnen das Ergebnis auf. Wir invertieren den Endwert auch, indem wir ihn von der Einheit subtrahieren, sodass Sie den Endwert direkt verwenden können, um die Hintergrundkomponente der Beleuchtung ohne zusĂ€tzliche Schritte zu modulieren:

 } occlusion = 1.0 - (occlusion / kernelSize); FragColor = occlusion; 

FĂŒr eine Szene mit einem uns vertrauten liegenden Nanosuit ergibt die AusfĂŒhrung des SSAO-Shaders die folgende Textur:


Wie Sie sehen können, erzeugt der Effekt der Hintergrundschattierung eine gute Illusion von Tiefe. Nur das Ausgabebild des Shaders ermöglicht es Ihnen bereits, die Details des KostĂŒms zu unterscheiden und sicherzustellen, dass es wirklich auf dem Boden liegt und nicht in einiger Entfernung davon schwebt.

Trotzdem ist der Effekt alles andere als ideal, da das durch die Textur zufĂ€lliger Rotationsvektoren eingefĂŒhrte Rauschmuster leicht erkennbar ist. Um das Ergebnis der SSAO-Berechnung zu glĂ€tten, wenden wir einen UnschĂ€rfefilter an.

Hintergrundschattierung verwischen


Nach dem Erstellen des SSAO-Ergebnisses und vor dem endgĂŒltigen Mischen der Beleuchtung muss die Textur verwischt werden, in der Daten zum Schattierungskoeffizienten gespeichert sind. Dazu haben wir einen weiteren Framebuffer:

 unsigned int ssaoBlurFBO, ssaoColorBufferBlur; glGenFramebuffers(1, &ssaoBlurFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO); glGenTextures(1, &ssaoColorBufferBlur); glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0); 

Das Kacheln einer Rauschtextur im Bildschirmbereich bietet genau definierte ZufÀlligkeitseigenschaften, die Sie beim Erstellen eines UnschÀrfefilters zu Ihrem Vorteil nutzen können:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D ssaoInput; void main() { vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0)); float result = 0.0; for (int x = -2; x < 2; ++x) { for (int y = -2; y < 2; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; result += texture(ssaoInput, TexCoords + offset).r; } } FragColor = result / (4.0 * 4.0); } 

Der Shader ĂŒbergeht einfach Texel der SSAO-Textur mit einem Versatz von -2 bis +2, was der tatsĂ€chlichen GrĂ¶ĂŸe der Rauschtextur entspricht. Der Offset entspricht der exakten GrĂ¶ĂŸe eines Texels: FĂŒr die Berechnung wird die Funktion texturSize () verwendet, die vec2 mit den Abmessungen der angegebenen Textur zurĂŒckgibt. T.O. Der Shader mittelt einfach die in der Textur gespeicherten Ergebnisse, wodurch eine schnelle und ziemlich effektive UnschĂ€rfe entsteht:


Insgesamt haben wir fĂŒr jedes Fragment auf dem Bildschirm eine Textur mit Hintergrundschattierungsdaten - alles ist bereit fĂŒr die Phase der endgĂŒltigen Bildreduzierung!

Hintergrundschattierung anwenden


Der Schritt der Anwendung des Schattierungskoeffizienten bei der endgĂŒltigen Berechnung der Beleuchtung ist ĂŒberraschend einfach: FĂŒr jedes Fragment reicht es aus, den Wert der Hintergrundkomponente der Lichtquelle einfach mit dem Schattierungskoeffizienten aus der vorbereiteten Textur zu multiplizieren. Sie können einen vorgefertigten Shader mit dem Blinn-Fong-Modell aus der Lektion ĂŒber verzögerte Schattierung nehmen und ein wenig korrigieren:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D ssao; struct Light { vec3 Position; vec3 Color; float Linear; float Quadratic; float Radius; }; uniform Light light; void main() { //    G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Diffuse = texture(gAlbedo, TexCoords).rgb; float AmbientOcclusion = texture(ssao, TexCoords).r; //   -    //   :   -  vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); vec3 lighting = ambient; //    (0, 0, 0)   - vec3 viewDir = normalize(-FragPos); //   vec3 lightDir = normalize(light.Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color; //   vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0); vec3 specular = light.Color * spec; //   float dist = length(light.Position - FragPos); float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist); diffuse *= attenuation; specular *= attenuation; lighting += diffuse + specular; FragColor = vec4(lighting, 1.0); } 

Es gibt nur zwei wesentliche Änderungen: den Übergang zu Berechnungen im Ansichtsfenster und die Multiplikation der Hintergrundbeleuchtungskomponente mit dem Wert von AmbientOcclusion . Ein Beispiel fĂŒr eine Szene mit einem einzelnen blauen Punktlicht:


Der vollstÀndige Quellcode ist hier .

Die Manifestation des SSAO-Effekts hĂ€ngt stark von Parametern wie KernelSize , Radius und Bias ab . Oft ist es fĂŒr den KĂŒnstler selbstverstĂ€ndlich, einen bestimmten Ort / eine bestimmte Szene zu bestimmen. Es gibt keine „besten“ und universellen Kombinationen von Parametern: FĂŒr einige Szenen ist ein kleiner Radius des Probenkerns gut, wĂ€hrend andere vom vergrĂ¶ĂŸerten Radius und der Anzahl der Proben profitieren. In diesem Beispiel werden 64 Beispielpunkte verwendet, was offen gesagt redundant ist. Sie können den Code jedoch jederzeit bearbeiten und sehen, was mit einer geringeren Anzahl von Stichproben passiert.

ZusĂ€tzlich zu den aufgelisteten Uniformen, die fĂŒr die Einstellung des Effekts verantwortlich sind, besteht die Möglichkeit, den Schweregrad des Hintergrundschattierungseffekts explizit zu steuern. Dazu reicht es aus, den Koeffizienten auf einen Grad anzuheben, der von einer anderen Uniform gesteuert wird:

 occlusion = 1.0 - (occlusion / kernelSize); FragColor = pow(occlusion, power); 

Ich rate Ihnen, einige Zeit mit den Einstellungen des Spiels zu verbringen, da dies ein besseres VerstĂ€ndnis der Art der Änderungen im endgĂŒltigen Bild ermöglicht.

Zusammenfassend lĂ€sst sich sagen, dass der visuelle Effekt der Anwendung von SSAO zwar eher subtil ist, in Szenen mit gut platzierter Beleuchtung jedoch zweifellos einen spĂŒrbaren Teil des Realismus ausmacht. Ein solches Werkzeug in Ihrem Arsenal zu haben, ist sicherlich wertvoll.

ZusÀtzliche Ressourcen


  1. SSAO-Tutorial : Ein ausgezeichneter Unterrichtsartikel von John Chapman, auf dessen Grundlage der Code fĂŒr diese Lektion erstellt wird.
  2. Kennen Sie Ihre SSAO-Artefakte : Ein sehr wertvoller Artikel, der nicht nur die dringendsten Probleme mit der SSAO-QualitĂ€t aufzeigt, sondern auch Möglichkeiten, sie zu lösen. Empfohlene LektĂŒre.
  3. SSAO mit Tiefenrekonstruktion: Nachtrag zur wichtigsten SSAO-Lektion von OGLDev ĂŒber eine hĂ€ufig verwendete Technik zum Wiederherstellen von Fragmentkoordinaten basierend auf der Tiefe. Die Bedeutung dieses Ansatzes beruht auf den erheblichen Speichereinsparungen, da keine Positionen im G-Puffer gespeichert werden mĂŒssen. Der Ansatz ist so universell, dass er fĂŒr SSAO gilt, sofern.

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!

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


All Articles