3D Game Shader für Anfänger: Effekte

[ Der erste Teil ]

Nachdem wir uns mit den Grundlagen befasst haben, implementieren wir in diesem Teil des Artikels Effekte wie Objektkonturen, Bloom, SSAO, Unschärfe, Schärfentiefe, Pixelung und andere.

Umrisse



Das Erstellen von Konturen um die Geometrie der Szene verleiht dem Spiel ein einzigartiges Aussehen, das Comics oder Cartoons ähnelt.

Diffuses Material


Der Contour Shader benötigt eine Eingabetextur, um die Kanten zu erkennen und zu färben. Kandidaten für eine solche eingehende Textur können eine diffuse Farbe aus Materialien, Farben aus diffusen Texturen, Scheitelpunktnormalen oder sogar Farben aus normalen Karten sein.

uniform struct { vec4 diffuse ; } p3d_Material; out vec4 fragColor; void main() { vec3 diffuseColor = p3d_Material.diffuse.rgb; fragColor = vec4(diffuseColor, 1); } 

Dies ist ein kleiner Fragment-Shader, der die diffuse Farbe eines Geometriematerials in eine Rahmenpuffertextur umwandelt. Diese diffuse Farbtextur aus dem Bildspeicher ist die Eingabetextur für den Pfad-Shader.


Dies ist die Textur der diffusen Farbe des Materials aus dem Rahmenpuffer, der die Farben anzeigt, die wir in Blender festgelegt haben. Der Kontur-Shader erkennt die Kanten in der Szene und färbt sie.

Es ist zu beachten, dass die diffuse Farbe von Materialien nicht funktioniert, wenn bestimmte Teile der Szene keine eigene diffuse Farbe des Materials haben.

Kanten erstellen



Das Erstellen von Kanten ähnelt der Verwendung von Kantenerkennungsfiltern in GIMP .

Alle Berechnungen für diese Schattierungstechnik werden in einem Fragment-Shader durchgeführt. Um Konturen für den Scheitelpunkt-Shader zu erstellen, müssen vier Scheitelpunkte des rechteckigen Netzes an die Ausgabe übergeben werden, damit sie zum Bildschirm passen.

 // ... uniform sampler2D materialDiffuseTexture; // ... vec2 texSize = textureSize(materialDiffuseTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy; // ... 

Bevor Sie beginnen, die Kanten zu erkennen, müssen Sie die eingehende Textur vorbereiten, mit der wir arbeiten werden. Da die Textur eine Bildschirmgröße hat, können wir die UV-Koordinaten berechnen, wobei wir die Koordinaten des Fragments und die Größe der eingehenden Textur kennen.

  // ... int separation = 1; // ... 

separation kann nach Ihrem Geschmack angepasst werden. Je größer der Abstand ist, desto dicker sind die Kanten oder Linien.

  // ... float threshold = 0; // ... vec4 mx = vec4(0); vec4 mn = vec4(1); int x = -1; int y = -1; for (int i = 0; i < 9; ++i) { vec4 color = texture ( materialDiffuseTexture , (texCoord + (vec2(x, y) * separation)) / texSize ); mx = max(color, mx); mn = min(color, mn); x += 1; if (x >= 2) { x = -1; y += 1; } } float alpha = ((mx.r + mx.g + mx.b) / 3) - ((mn.r + mn.g + mn.b) / 3); if (alpha > threshold) { alpha = 1; } // ... 


Die Kantenerkennungstechnik findet Änderungen in den Farben der eingehenden Textur. Wenn Sie sich auf das aktuelle Fragment konzentrieren, werden im Fenster 3x3-Fragmente die hellsten und dunkelsten Farben der neun Samples ermittelt. Dann subtrahiert sie von der Helligkeit einer Farbe die Helligkeit einer anderen und erhält ihren Unterschied.

  // ... vec3 lineRgb = vec3(0.012, 0.014, 0.022); // ... vec4 lineColor = vec4(lineRgb, alpha); // ... fragColor = lineColor; // ... 

Dieser Unterschied wird im Alphakanal der Ausgabefarbe verwendet. Wenn es keinen Unterschied gibt, wird die Kante oder Linie nicht gezeichnet. Wenn es einen Unterschied gibt, wird die Kante gezeichnet.

  // ... float threshold = 0; // ... if (alpha > threshold) { alpha = 1; } // ... 

Versuchen Sie, mit dem Schwellenwert zu experimentieren. Jetzt ist es Null. Jeder Wert ungleich Null wird zu einer Flanke. Dieser Schwellenwert kann geändert werden. Dies ist besonders nützlich für lautere eingehende Texturen mit kleinen Unterschieden. Bei einer verrauschten eingehenden Textur müssen Sie normalerweise nur für große Unterschiede Konturen erstellen.

Quellcode



Nebel



Nebel (oder Nebel, wie er in Blender genannt wird) fügt der Szene atmosphärischen Dunst hinzu und erzeugt mysteriöse, erweichte, hervorstehende Teile. Die hervorstehenden Teile erscheinen, wenn eine gewisse Geometrie plötzlich in die Pyramide der Kamerasichtbarkeit fällt.

 // ... uniform struct p3d_FogParameters { vec4 color ; float start ; float end ; } p3d_Fog; // ... 

Panda3D verfügt über eine praktische Datenstruktur, die alle Nebelparameter enthält. Sie können sie jedoch manuell auf Ihren Shader übertragen.

  // ... float fogIntensity = clamp ( ( p3d_Fog.end - vertexPosition.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; // ... 

Im Codebeispiel wird ein lineares Modell verwendet, um die Helligkeit des Nebels zu berechnen, wenn Sie sich von der Kamera entfernen. Stattdessen können Sie das Exponentialmodell verwenden. Die Helligkeit des Nebels ist vor oder zu Beginn des Nebels Null. Wenn sich die Scheitelpunktposition dem Ende des Nebels fogIntensity nähert sich fogIntensity Einheit. Für alle Eckpunkte nach dem Ende des Nebels ist die fogIntensity von oben auf 1 begrenzt.

  // ... fragColor = mix ( outputColor , p3d_Fog.color , fogIntensity ); // ... 

Basierend auf der Helligkeit des Nebels mischen wir die Farbe des Nebels mit der Ausgabefarbe. Wenn sich fogIntensity Einheit nähert, wird es immer weniger outputColor und immer mehr outputColor . Wenn fogIntensity die Einheit erreicht, bleibt nur die Farbe des Nebels übrig.

Nebel auf den Konturen




 // ... uniform sampler2D positionTexture; // ... vec4 position = texture(positionTexture, texCoord / texSize); float fogIntensity = clamp ( ( p3d_Fog.end - position.y) / ( p3d_Fog.end - p3d_Fog.start) , 0 , 1 ); fogIntensity = 1 - fogIntensity; vec4 lineWithFogColor = mix ( lineColor , p3d_Fog.color , fogIntensity ); fragColor = vec4(lineWithFogColor.rgb, alpha); // ... 

Der Path Shader bringt Nebel auf die Farben der Kanten, um ein ganzheitlicheres Bild zu erhalten. Wenn er dies nicht tun würde, würde die Geometrie der Konturen durch Nebel verdeckt, was seltsam aussehen würde. Er erzeugt jedoch immer noch Konturen an den äußersten Kanten der Geometrie der Bühne mit der Mühle, da die Kanten über die Geometrie hinausgehen - bis dahin, wo es keine Scheitelpunktpositionen gibt.

positionTexture ist eine Bildpuffertextur, die die Positionen der Scheitelpunkte des Ansichtsbereichs enthält. Sie werden dies erfahren, wenn wir den SSAO-Shader implementieren.

Quellcode



Blüte



Das Hinzufügen von Blüte zur Szene kann eine überzeugende Illusion des Beleuchtungsmodells erzeugen. Licht emittierende Objekte werden überzeugender und Lichtreflexionen erhalten eine zusätzliche Strahlung.

  //... float separation = 3; int samples = 15; float threshold = 0.5; float amount = 1; // ... 

Sie können diese Einstellungen nach Ihren Wünschen anpassen. Die Trennung vergrößert die Unschärfe. Proben bestimmen die Stärke der Unschärfe. Der Schwellenwert bestimmt, was von diesem Effekt betroffen ist und was nicht. Menge steuert die Menge der Blütenausgabe.

  // ... int size = samples; int size2 = size * size; int x = 0; int y = 0; // ... float value = 0; vec4 result = vec4(0); vec4 color = vec4(0); // ... for (int i = 0; i < size2; ++i) { // ... } // ... 

Diese Technik beginnt mit dem Übergeben von samples Fenstergröße an samples die relativ zum aktuellen Fragment zentriert sind. Es sieht aus wie ein Fenster zum Erstellen von Pfaden.

  // ... color = texture ( bloomTexture , ( gl_FragCoord.xy + vec2(x * separation, y * separation) ) / texSize ); value = ((0.3 * color.r) + (0.59 * color.g) + (0.11 * color.b)); if (value < threshold) { color = vec4(0); } result += color; // ... 

Dieser Code ermittelt die Farbe aus der eingehenden Textur und wandelt die Werte für Rot, Grün und Blau in einen Graustufenwert um. Wenn der Wert in Graustufen unter dem Schwellenwert liegt, wird diese Farbe verworfen und schwarz.

Beim Durchlaufen aller Proben im Fenster werden alle ihre Werte im result akkumuliert.

  // ... result = result / size2; // ... 

Nachdem er die Probensammlung abgeschlossen hat, dividiert er die Summe der Farbmuster durch die Anzahl der entnommenen Proben. Das Ergebnis ist die mittlere Farbe des Fragments selbst und seiner Nachbarn. Auf diese Weise erhalten wir für jedes Fragment ein verschwommenes Bild. Diese Art von Unschärfe wird als Box-Unschärfe bezeichnet.


Hier sehen Sie den Prozess der Ausführung des Bloom-Algorithmus.

Quellcode



Screen Space Ambient Occlusion (SSAO)



SSAO ist einer der Effekte, von denen Sie nicht wissen, dass sie existieren, aber sobald Sie wissen, dass Sie ohne sie nicht mehr leben können. Er kann eine mittelmäßige Szene in eine erstaunliche verwandeln! In statischen Szenen kann Umgebungsokklusion in die Textur eingebrannt werden, aber für dynamischere Szenen benötigen wir einen Shader. SSAO ist eine der ausgefeilteren Schattierungstechniken, aber sobald Sie es herausgefunden haben, werden Sie ein Master-Shader.

Beachten Sie, dass der Begriff „Bildschirmbereich“ im Titel nicht ganz korrekt ist, da nicht alle Berechnungen im Bildschirmbereich ausgeführt werden.

Eingehende Daten


Der SSAO-Shader benötigt die folgende Eingabe.

  • Vektoren von Scheitelpunktpositionen im Betrachtungsraum.
  • Normale Vektoren zu den Eckpunkten im Betrachtungsraum.
  • Abtastvektoren im Tangentenraum.
  • Rauschvektoren im Tangentenraum.
  • Die Projektionsmatrix auf dem Kameraobjektiv.

Position



Es ist nicht erforderlich, Scheitelpunktpositionen in der Bildpuffertextur zu speichern. Wir können sie aus dem Kameratiefenpuffer neu erstellen. Ich schreibe einen Leitfaden für Anfänger, daher werden wir diese Optimierung nicht verwenden und sofort zur Sache kommen. In Ihrer Implementierung können Sie den Tiefenpuffer problemlos verwenden.

 PT(Texture) depthTexture = new Texture("depthTexture"); depthTexture->set_format(Texture::Format::F_depth_component32); PT(GraphicsOutput) depthBuffer = graphicsOutput->make_texture_buffer("depthBuffer", 0, 0, depthTexture); depthBuffer->set_clear_color(LVecBase4f(0, 0, 0, 0)); NodePath depthCameraNP = window->make_camera(); DCAST(Camera, depthCameraNP.node())->set_lens(window->get_camera(0)->get_lens()); PT(DisplayRegion) depthBufferRegion = depthBuffer->make_display_region(0, 1, 0, 1); depthBufferRegion->set_camera(depthCameraNP); 

Wenn Sie sich für die Verwendung des Tiefenpuffers entscheiden, können Sie ihn wie folgt in Panda3D konfigurieren.

 in vec4 vertexPosition; out vec4 fragColor; void main() { fragColor = vertexPosition; } 

Hier ist ein einfacher Shader zum Rendern von Scheitelpunktpositionen im Anzeigebereich in eine Bildpuffertextur. Eine schwierigere Aufgabe besteht darin, die Textur des Bildpuffers so einzustellen, dass die von ihm erhaltenen Komponenten des Fragmentvektors nicht auf das Intervall [0, 1] sind und dass jedes eine ausreichend hohe Genauigkeit (eine ausreichend große Anzahl von Bits) aufweist. Wenn beispielsweise eine interpolierte Scheitelpunktposition <-139.444444566, 0.00000034343, 2.5> , können Sie sie nicht als <0.0, 0.0, 1.0> in der Textur speichern.

  // ... FrameBufferProperties fbp = FrameBufferProperties::get_default(); // ... fbp.set_rgba_bits(32, 32, 32, 32); fbp.set_rgb_color(true); fbp.set_float_color(true); // ... 

Hier ist ein Beispielcode, der eine Bildpuffertextur zum Speichern von Scheitelpunktpositionen vorbereitet. Er benötigt 32 Bit für Rot, Grün, Blau und Alpha, daher deaktiviert er die Einschränkung von Werten um das Intervall [0, 1] . Der Aufruf von set_rgba_bits(32, 32, 32, 32) legt das set_rgba_bits(32, 32, 32, 32) und deaktiviert die Einschränkung.

  glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGB32F , 1200 , 900 , 0 , GL_RGB , GL_FLOAT , nullptr ); 

Hier ist ein ähnlicher Aufruf von OpenGL. GL_RGB32F setzt die Bits und deaktiviert die Einschränkung.

Wenn der Farbpuffer ein festes Komma hat, sind die Komponenten der Anfangs- und Endwerte sowie der Mischungsindizes vor der Berechnung der Mischungsgleichung für die vorzeichenlosen normalisierten und vorzeichenbehafteten normalisierten Farbpuffer auf [0, 1] bzw. [–1, 1] begrenzt. Wenn der Farbpuffer einen Gleitkomma hat, ist die Einschränkung nicht erfüllt.

Quelle


Hier sehen Sie die Positionen der Eckpunkte; Die y-Achse ist oben.

Denken Sie daran, dass Panda3D die z-Achse als nach oben zeigenden Vektor definiert, während in OpenGL die y-Achse nach oben zeigt. Der Positions-Shader zeigt die Positionen von Scheitelpunkten mit einem z nach oben an, da in Panda3D
Der gl-coordinate-system default ist konfiguriert.

Normal



Für die korrekte Ausrichtung der im SSAO-Shader erhaltenen Samples benötigen wir die Normalen zu den Eckpunkten. Der Beispielcode generiert mehrere über die Hemisphäre verteilte Abtastvektoren. Sie können jedoch die Kugel verwenden und das Problem der Notwendigkeit von Normalen vollständig lösen.

 in vec3 vertexNormal; out vec4 fragColor; void main() { vec3 normal = normalize(vertexNormal); fragColor = vec4(normal, 1); } 

Wie der Positions-Shader ist der normale Shader sehr einfach. Denken Sie daran, die Normalen auf die Eckpunkte zu normalisieren, und denken Sie daran, dass sie sich im Betrachtungsraum befinden.


Die Normalen zu den Eckpunkten werden hier gezeigt; Die y-Achse ist oben.

Denken Sie daran, dass Panda3D die z-Achse als Aufwärtsvektor und OpenGL als y-Achse betrachtet. Der normale Shader zeigt Scheitelpunktpositionen mit der z-Achse nach oben an, da in Panda3D die gl-coordinate-system default konfiguriert ist.

Proben


Um den Umgebungsokklusionswert für ein einzelnes Fragment zu bestimmen, müssen wir die Umgebung abtasten.

  // ... for (int i = 0; i < numberOfSamples; ++i) { LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) ).normalized(); float rand = randomFloats(generator); sample[0] *= rand; sample[1] *= rand; sample[2] *= rand; float scale = (float) i / (float) numberOfSamples; scale = lerp(0.1, 1.0, scale * scale); sample[0] *= scale; sample[1] *= scale; sample[2] *= scale; ssaoSamples.push_back(sample); } // ... 

Der Beispielcode generiert 64 Zufallsstichproben, die auf einer Halbkugel verteilt sind. Diese ssaoSamples werden an den SSAO-Shader übergeben.

  LVecBase3f sample = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 ).normalized(); 

Wenn Sie Ihre Stichproben auf eine Kugel verteilen möchten, ändern Sie das Intervall der Zufallskomponente z so, dass es sich von minus eins zu eins ändert.

Der Lärm


  // ... for (int i = 0; i < 16; ++i) { LVecBase3f noise = LVecBase3f ( randomFloats(generator) * 2.0 - 1.0 , randomFloats(generator) * 2.0 - 1.0 , 0.0 ); ssaoNoise.push_back(noise); } // ... 

Um den abgetasteten Bereich gut abzudecken, müssen wir Rauschvektoren erzeugen. Diese Rauschvektoren können Proben um die Oberseite der Oberfläche drehen.

Umgebungsokklusion



SSAO erfüllt seine Aufgabe, indem es den Betrachtungsraum um das Fragment herum abtastet. Je mehr Proben sich unter der Oberfläche befinden, desto dunkler ist die Farbe des Fragments. Diese Proben befinden sich im Fragment und zeigen in der allgemeinen Richtung der Normalen zum Scheitelpunkt an. Jedes Sample wird verwendet, um nach einer Position in der Textur der Bildpufferposition zu suchen. Die zurückgegebene Position wird mit der Probe verglichen. Wenn die Probe weiter von der Kamera entfernt ist als die Position, wird die Probe in Richtung des Fragments verschlossen.


Hier sehen Sie einen Raum über der Oberfläche, der zur Okklusion abgetastet wurde.

  // ... float radius = 1.1; float bias = 0.026; float lowerRange = -2; float upperRange = 2; // ... 

Wie einige andere Techniken verfügt der SSAO-Shader über mehrere Steuerparameter, die geändert werden können, um das gewünschte Erscheinungsbild zu erzielen. Die Vorspannung wird zum Abstand von der Probe zur Kamera addiert. Dieser Parameter kann zur Bekämpfung von Flecken verwendet werden. Der Radius vergrößert oder verkleinert den Abdeckungsbereich des Probenraums. LowerRange und UpperRange ändern den Standardbereich der Faktormetrik von [0, 1] auf einen beliebigen Wert, den Sie auswählen. Durch Erhöhen der Reichweite können Sie den Kontrast erhöhen.

  // ... vec4 position = texture(positionTexture, texCoord); vec3 normal = texture(normalTexture, texCoord).xyz; int noiseX = int(gl_FragCoord.x - 0.5) % 4; int noiseY = int(gl_FragCoord.y - 0.5) % 4; vec3 random = noise[noiseX + (noiseY * 4)]; // ... 

Wir erhalten den Positions-, Normal- und Zufallsvektor zur weiteren Verwendung. Denken Sie daran, dass im Codebeispiel 16 Zufallsvektoren erstellt wurden. Ein zufälliger Vektor wird basierend auf der Bildschirmposition der aktuellen Fragmente ausgewählt.

  // ... vec3 tangent = normalize(random - normal * dot(random, normal)); vec3 binormal = cross(normal, tangent); mat3 tbn = mat3(tangent, binormal, normal); // ... 

Unter Verwendung eines Zufallsvektors und eines Normalenvektors sammeln wir die Matrix aus Tangens, Binormalem und Normalem. Wir benötigen diese Matrix, um die Probenvektoren vom Tangentenraum in den Vermessungsraum zu transformieren.

  // ... float occlusion = NUM_SAMPLES; for (int i = 0; i < NUM_SAMPLES; ++i) { // ... } // ... 

Mit einer Matrix kann der Shader alle Samples in der Schleife durchlaufen und die Anzahl der ungeöffneten abziehen.

  // ... vec3 sample = tbn * samples[i]; sample = position.xyz + sample * radius; // ... 

Platzieren Sie die Probe mithilfe der Matrix neben der Scheitelpunkt- / Fragmentposition und skalieren Sie sie um den Radius.

  // ... vec4 offset = vec4(sample, 1.0); offset = lensProjection * offset; offset.xyz /= offset.w; offset.xyz = offset.xyz * 0.5 + 0.5; // ... 

Anhand der Position der Probe im Betrachtungsraum transformieren wir sie vom Betrachtungsraum in den Beschneidungsraum und dann in den UV-Raum.

 -1 * 0.5 + 0.5 = 0 1 * 0.5 + 0.5 = 1 

Vergessen Sie nicht, dass die Komponenten des Beschneidungsraums im Bereich von minus eins bis eins liegen und die UV-Koordinaten im Bereich von null bis eins liegen. Um die Koordinaten des Beschneidungsraums in UV-Koordinaten umzuwandeln, multiplizieren Sie sie mit einer Sekunde und addieren Sie eine Sekunde.

  // ... vec4 offsetPosition = texture(positionTexture, offset.xy); float occluded = 0; if (sample.y + bias <= offsetPosition.y) { occluded = 0; } else { occluded = 1; } // ... 

Unter Verwendung der UV-Versatzkoordinaten, die durch Projizieren der 3D-Probe auf die 2D-Positionstextur erhalten wurden, finden wir den entsprechenden Positionsvektor. Dies führt uns vom Betrachtungsraum zum Beschneidungsraum zum UV-Raum und dann zurück zum Betrachtungsraum. Der Shader führt diese Schleife aus, um festzustellen, ob sich hinter der Probe, an der Probenposition oder vor der Probe eine Geometrie befindet. Befindet sich die Probe vor oder in einer Geometrie, wird diese Probe in Bezug auf das überlappende Fragment einfach nicht berücksichtigt. Befindet sich die Probe hinter einer Geometrie, wird diese Probe relativ zum überlappenden Fragment berücksichtigt.

  // ... float intensity = smoothstep ( 0.0 , 1.0 , radius / abs(position.y - offsetPosition.y) ); occluded *= intensity; occlusion -= occluded; // ... 

Fügen Sie dieser abgetasteten Position nun Gewicht hinzu, basierend darauf, wie weit sie innerhalb oder außerhalb des Radius liegt. Subtrahieren Sie dann diese Probe von der Okklusionsmetrik, da davon ausgegangen wird, dass sich alle Proben vor der Schleife überlappten.

  // ... occlusion /= NUM_SAMPLES; // ... fragColor = vec4(vec3(occlusion), position.a); // ... 

Teilen Sie die Anzahl der überlappenden durch die Anzahl der Proben, um den Okklusionsindikator vom Intervall [0, NUM_SAMPLES] in das Intervall [0, 1] [0, NUM_SAMPLES] . Null bedeutet vollständige Okklusion, Einheiten bedeuten keine Okklusion. Weisen Sie nun der Fragmentfarbe die Okklusionsmetrik zu, und fertig.

Bitte beachten Sie, dass dem Beispielkanal im Beispielcode der Alpha-Wert der Positionstextur aus dem Bildpuffer zugewiesen wird, um überlappende Hintergründe zu vermeiden.

Unschärfe



Die Textur des SSAO-Bildpuffers ist etwas verrauscht, daher sollten Sie ihn zum Glätten verwischen.

  // ... for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( ssaoTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ).rgb; xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

Der SSAO-Unschärfeshader ist eine normale Box-Unschärfe. Wie der Bloom-Shader zeichnet er ein Fenster über die eingehende Textur und mittelt jedes Fragment mit den Werten seiner Nachbarn.

Beachten Sie, dass parameters.x ein Trennungsparameter ist.

Umgebungsfarbe


  // ... vec2 ssaoBlurTexSize = textureSize(ssaoBlurTexture, 0).xy; vec2 ssaoBlurTexCoord = gl_FragCoord.xy / ssaoBlurTexSize; float ssao = texture(ssaoBlurTexture, ssaoBlurTexCoord).r; vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex * ssao; // ... 

Die letzte Herausforderung für SSAO liegt erneut in den Beleuchtungsberechnungen. Hier sehen wir, wie die Okklusion im Textur-SSAO-Texturpuffer gefunden und in die Berechnung des Umgebungslichts einbezogen wird.

Quellcode



Schärfentiefe



Die Schärfentiefe ist auch ein solcher Effekt, da man gelernt hat, ohne was man nicht leben kann. Aus künstlerischer Sicht können Sie damit die Aufmerksamkeit des Betrachters auf ein bestimmtes Objekt lenken. Im allgemeinen Fall trägt die Schärfentiefe auf Kosten eines kleinen Aufwands zu einem großen Teil des Realismus bei.

Im Fokus


Der erste Schritt besteht darin, die Szene vollständig scharf zu stellen. Rendern Sie es in die Frame Buffer-Textur. Dies ist einer der Eingabewerte für den Tiefenschärfepuffer.

Unscharf


  // ... vec4 result = vec4(0); for (int i = 0; i < size2; ++i) { x = size - xCount; y = yCount - size; result += texture ( blurTexture , texCoord + vec2(x * parameters.x, y * parameters.x) ); xCount -= 1; if (xCount < countMin) { xCount = countMax; yCount -= 1; } } result = result / size2; // ... 

Der zweite Schritt besteht darin, die Szene so zu verwischen, als wäre sie völlig unscharf. Wie bei Bloom und SSAO können Sie Box Blur verwenden. Rendern Sie diese defokussierte Szene in die Bildpuffertextur. Dies ist ein weiterer Eingabewert für die Tiefenschärfe.

Beachten Sie, dass parameters.x ein Trennungsparameter ist.

Verwirrung




  // ... float focalLengthSharpness = 100; float blurRate = 6; // ... 

Sie können diese Optionen nach Ihrem Geschmack anpassen. focalLengthSharpness beeinflusst, wie defokussiert die Szene auf Brennweite ist. Je kleiner die focalLengthSharpness , desto defokussierter ist die Szene auf die Brennweite. blurRate beeinflusst die Geschwindigkeit der Unschärfe der Szene, wenn Sie sich von der Brennweite entfernen. Je kleiner die blurRate , blurRate weniger unscharf wird die Szene, wenn Sie sich vom Fokuspunkt entfernen.

  // ... vec4 focusColor = texture(focusTexture, texCoord); vec4 outOfFocusColor = texture(outOfFocusTexture, texCoord); // ... 

Wir brauchen Farben im Fokus und in einem defokussierten Bild.

  // ... vec4 position = texture(positionTexture, texCoord); // ... 

Möglicherweise benötigen wir auch die Position des Scheitelpunkts im Betrachtungsraum. Sie können die Positionstextur aus dem für SSAO verwendeten Bildpuffer erneut anwenden.

  // ... float blur = clamp ( pow ( blurRate , abs(position.y - focalLength.x) ) / focalLengthSharpness , 0 , 1 ); // ... fragColor = mix(focusColor, outOfFocusColor, blur); // ... 

Und hier findet die Verwirrung statt. Je näher bluran einem, desto mehr wird es verwenden outOfFocusColor. Ein Wert von Null blurbedeutet, dass dieses Fragment vollständig fokussiert ist. Mit blur >= 1diesem Fragment wird vollständig defokussiert.

Quellcode



Posterisierung



Bei der Posterisierung oder Farbabtastung wird die Anzahl der eindeutigen Farben in einem Bild reduziert. Mit diesem Shader können Sie dem Spiel einen Comic- oder Retro-Look verleihen. Wenn Sie es mit einem Umriss kombinieren, erhalten Sie einen echten Cartoon-Stil.

  // ... float levels = 8; // ... 

Sie können mit diesem Parameter experimentieren. Je größer es ist, desto mehr Blumen bleiben übrig.

  // ... vec4 texColor = texture(posterizeTexture, texCoord); // ... 

Wir brauchen die eingehende Farbe.

  // ... vec3 grey = vec3((texColor.r + texColor.g + texColor.b) / 3.0); vec3 grey1 = grey; grey = floor(grey * levels) / levels; texColor.rgb += (grey - grey1); // ... 

Ich habe eine solche Methode der Posterisierung nicht gesehen. Nachdem ich es überprüft hatte, stellte ich fest, dass es im Vergleich zu herkömmlichen Methoden schönere Ergebnisse liefert. Um die Farbpalette zu verkleinern, konvertieren Sie zuerst die Farbe in einen Graustufenwert. Wir diskretisieren die Farbe, indem wir sie an eine der Ebenen binden. Wir berechnen die Differenz zwischen dem diskretisierten Wert in Graustufen und dem nicht diskretisierten Wert in Graustufen. Fügen Sie diesen Unterschied zur Eingabefarbe hinzu. Dieser Unterschied ist der Betrag, um den die Farbe zunehmen / abnehmen muss, um einen diskretisierten Wert in Graustufen zu erreichen.

  // ... fragColor = texColor; // ... 

Vergessen Sie nicht, den Wert der Eingabefarbe der Farbe des Fragments zuzuweisen.

Cel Schattierung



Die Posterisierung kann einem Bild das Aussehen einer Cel-Schattierung verleihen, da bei der Cel-Schattierung diffuse und diffuse Farben in diskrete Schattierungen diskretisiert werden. Wir möchten nur feste, diffuse Farben ohne feine Details der normalen Karte und einen kleinen Wert verwenden levels.

Quellcode



Pixelisierung



Die Pixelisierung eines 3D-Spiels kann ihm ein interessantes Aussehen verleihen oder Zeit sparen, die für die manuelle Erstellung der gesamten Pixelgrafik aufgewendet worden wäre. Kombinieren Sie es mit Posterisierung, um einen echten Retro-Look zu kreieren.

  // ... int pixelSize = 5; // ... 

Sie können die Pixelgröße selbst anpassen. Je größer es ist, desto rauer wird das Bild.



  // ... float x = int(gl_FragCoord.x) % pixelSize; float y = int(gl_FragCoord.y) % pixelSize; x = floor(pixelSize / 2.0) - x; y = floor(pixelSize / 2.0) - y; x = gl_FragCoord.x + x; y = gl_FragCoord.y + y; // ... 

Diese Technik bringt jedes Fragment in der Mitte seines nächsten nicht überlappenden Fensters in Pixelgröße an. Diese Fenster werden über der eingehenden Textur ausgerichtet. Fragmente in der Mitte des Fensters bestimmen die Farbe anderer Fragmente in ihrem Fenster.

  // ... fragColor = texture(pixelizeTexture, vec2(x, y) / texSize); // ... 

Nachdem wir die Koordinate des gewünschten Fragments bestimmt haben, nehmen Sie seine Farbe aus der eingehenden Textur und weisen Sie sie der Fragmentfarbe zu.

Quellcode



Schärfen



Der Schärfeeffekt (Schärfen) erhöht den Kontrast an den Bildrändern. Es ist praktisch, wenn sich die Grafik als zu weich herausstellt.

  // ... float amount = 0.8; // ... 

Durch Ändern des Werts können wir die Größe der Schärfe des Ergebnisses steuern. Wenn der Wert Null ist, ändert sich das Bild nicht. Bei negativen Werten sieht das Bild merkwürdig aus.

  // ... float neighbor = amount * -1; float center = amount * 4 + 1; // ... 

Benachbarte Fragmente werden mit multipliziert amount * -1. Das aktuelle Fragment wird mit multipliziert amount * 4 + 1.

  // ... vec3 color = texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 1) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x - 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y + 0) / texSize).rgb * center + texture(sharpenTexture, vec2(gl_FragCoord.x + 1, gl_FragCoord.y + 0) / texSize).rgb * neighbor + texture(sharpenTexture, vec2(gl_FragCoord.x + 0, gl_FragCoord.y - 1) / texSize).rgb * neighbor ; // ... 

Benachbarte Fragmente befinden sich oben, unten, links und rechts. Nach dem Multiplizieren der Nachbarn und des aktuellen Fragments mit ihren Werten wird das Ergebnis addiert.

  // ... fragColor = vec4(color, texture(sharpenTexture, texCoord).a); // ... 

Diese Menge ist die endgültige Farbe des Fragments.

Quellcode



Filmkorn



Das Filmkorn (in kleinen Dosen und nicht wie im Beispiel) kann Realismus hinzufügen, der unsichtbar ist, bis dieser Effekt beseitigt ist. Normalerweise sind dies die Unvollkommenheiten, die das digital erzeugte Bild überzeugender machen.

Beachten Sie, dass die Filmkörnung normalerweise der letzte Effekt ist, der auf das Bild angewendet wird, bevor es angezeigt wird.

Wert


  // ... float amount = 0.1; // ... 

amountsteuert die Sichtbarkeit der Filmkörnung. Je höher der Wert, desto mehr „Schnee“ im Bild.

Zufällige Helligkeit


 // ... uniform float osg_FrameTime; //... float toRadians = 3.14 / 180; //... float randomIntensity = fract ( 10000 * sin ( ( gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime ) * toRadians ) ); // ... 

Dieser Code berechnet die zufällige Helligkeit, die zum Anpassen des Werts erforderlich ist.

 Time Since F1 = 00 01 02 03 04 05 06 07 08 09 10 Frame Number = F1 F3 F4 F5 F6 osg_FrameTime = 00 02 04 07 08 

Wert osg_FrameTime von Panda3D zur Verfügung gestellt. Eine Frame-Zeit ist ein Zeitstempel mit Informationen darüber, wie viele Sekunden seit dem ersten Frame vergangen sind. Der Beispielcode verwendet es, um die Körnung des Films zu animieren, die osg_FrameTimein jedem Bild unterschiedlich ist.

  // ... ( gl_FragCoord.x + gl_FragCoord.y * 8009 // Large number here. // ... 

Für statische Körnung müssen die Filme durch eine osg_FrameTimegroße Anzahl ersetzt werden. Um zu vermeiden, dass Muster angezeigt werden, können Sie verschiedene Zahlen ausprobieren.



  // ... * sin ( ( gl_FragCoord.x + gl_FragCoord.y * someNumber // ... 

Um Punkte oder Flecken von Filmkörnung zu erzeugen, werden beide Koordinaten sowie x und y verwendet. Wenn Sie x verwenden, werden nur vertikale Linien angezeigt. Wenn Sie y verwenden, werden nur horizontale Linien angezeigt.

Im Code wird eine Koordinate mit einer anderen multipliziert, um die diagonale Symmetrie zu zerstören.


Natürlich können Sie den Koordinatenmultiplikator entfernen und einen völlig akzeptablen Regeneffekt erzielen.

Beachten Sie, dass Sie die Ausgabe sinmit multiplizieren müssen, um den Regeneffekt zu animieren osg_FrameTime.

Experimentieren Sie mit den x- und y-Koordinaten, um die Richtung des Regens zu ändern. Lassen Sie für eine Dusche nach unten nur die x-Koordinate.

 input = (gl_FragCoord.x + gl_FragCoord.y * osg_FrameTime) * toRadians frame(10000 * sin(input)) = fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = 

sinwird als Hash-Funktion verwendet. Fragmentkoordinaten werden mit Ausgabewerten gehasht sin. Dank dessen erscheint eine praktische Eigenschaft - unabhängig von den Eingabedaten (groß oder klein) liegt das Ausgabeintervall im Bereich von minus eins bis eins.

 fract(10000 * sin(6.977777777777778)) = fract(10000 * 0.6400723818964882) = fract(6400.723818964882) = 0.723818964882 

sinin Kombination mit fractauch als Pseudozufallszahlengenerator verwendet.

 >>> [floor(fract(4 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 0, 1, 2, 2, 3, 4, 4, 5, 6] >>> [floor(fract(10000 * sin(x * toRadians)) * 10) for x in range(0, 10)] [0, 4, 8, 0, 2, 1, 7, 0, 0, 5] 

Schauen Sie sich zuerst die erste Zahlenreihe und dann die zweite an. Jede Zeile ist deterministisch, aber das Muster ist in der zweiten weniger auffällig als in der zweiten. Daher wird fract(10000 * sin(...))das Muster trotz der Tatsache, dass die Ausgabe deterministisch ist, viel schwächer erkannt.


Hier sehen wir, wie der Faktor sinzuerst 1, dann 10, dann 100 und dann 1000 ist.

Wenn der Multiplikator der Ausgabewerte zunimmt, wird das sinMuster weniger wahrnehmbar. Aus diesem Grund wird der Code sinmit 10.000 multipliziert.

Fragmentfarbe


  // ... vec2 texSize = textureSize(filmGrainTexture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; vec4 color = texture(filmGrainTexture, texCoord); // ... 

Konvertieren Sie die Koordinaten des Fragments in UV-Koordinaten. Mit diesen UV-Koordinaten suchen wir nach der Texturfarbe für das aktuelle Fragment.

  // ... amount *= randomIntensity; color.rgb += amount; // ... 

Ändern Sie den Wert in eine zufällige Helligkeit und fügen Sie ihn der Farbe hinzu.

  // ... fragColor = color; // ... 

Stellen Sie die Farbe des Fragments ein und fertig.

Quellcode



Danksagung


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


All Articles