SSLR: Screen Space Local Reflections in AAA-Spielen

  • Tutorial
Bild

Hallo Freund! Dieses Mal werde ich erneut auf das Thema Grafik in AAA- Spielen eingehen . Ich habe bereits die Methode herausgefunden HDRR (nicht mit HDRI verwechselt werden) hier und ein wenig reden über Farbkorrektur. Heute werde ich Ihnen sagen, was SSLR (auch bekannt als SSPR, SSR) ist: Screen Space Local Reflections . Wen kümmert es - unter Katze.

Einführung in das verzögerte Rendern


Zunächst möchte ich ein Konzept wie " Verzögertes Rendern" einführen (nicht zu verwechseln mit " Verzögertes Schattieren" , da sich letzteres auf die Beleuchtung bezieht). Was ist die Essenz des verzögerten Renderns ? Fakt ist, dass alle Effekte (wie Beleuchtung, Global Shading, Reflections, DOF ) von der Geometrie getrennt werden können und diese Effekte als eine besondere Art der Nachbearbeitung realisieren. Was braucht es zum Beispiel, um DOF ( Depth Of Field , Unschärfe über große Entfernungen) auf unsere Szene anzuwenden ? Haben Sie die Szene selbst ( Farbkarte ) und haben Sie Informationen über die Position des Texels (mit anderen Worten, wie viele Pixel von der Kamera entfernt sind). Weiter - alles ist einfach. Anwenden Blur auf die FarbkarteWobei der Unschärferadius von der Pixeltiefe abhängt (aus der Tiefenkarte ). Und wenn Sie sich das Ergebnis ansehen - je weiter das Objekt entfernt ist, desto unschärfer wird es. Was macht die Technik des verzögerten Renderns ? Sie erstellt den sogenannten GBuffer , der normalerweise drei Texturen enthält ( RenderTarget ):

  • Ordnen Sie die Farbe (Informationen über die diffuse Komponente oder eine Pixelfarbe)
    Bild
  • Normale Karte (Pixelnormale Informationen)
    Bild
  • Tiefenkarte (Information über die Position des "Pixels", hier wird nur die Tiefe gespeichert)
    Bild


Bei der Color Map , Normal Map scheint alles klar zu sein, das sind gewöhnliche Surface.Color- Texturen: vielleicht mit der Ausnahme, dass der normale Vektor innerhalb von [-1, 1] liegen kann (es wird eine einfache Packung des Vektors in das Format [0, 1] verwendet) )

Aber die Situation mit der Tiefenkarte wird immer unverständlicher. Wie speichert die Tiefenkarte Informationen über die Position eines Pixels und sogar eine Zahl? Ganz einfach gesagt, die Transformation des Primitiven:

float4 vertexWVP = mul(vertex, World*View*Projection);


Gibt uns die Bildschirmkoordinaten:

float2 UV = vertexWVP.xy;


Und ein paar Informationen darüber, wie weit das Pixel von der Kamera entfernt ist:

float depth = vertexWVP.z / vertexWVP.w;


Basierend darauf brauchen wir kein UV , weil Wenn Sie ein normales Quad im Vollbildmodus zeichnen, ist es bereits bekannt. Daher lohnt es sich, in der Tiefenkarte nicht die Position des Pixels, sondern nur die Tiefe zu speichern.

In Zukunft können wir die Pixelposition auf sehr einfache Weise rekonstruieren:

float3 GetPosition(float2 UV, float depth)
{
	float4 position = 1.0f; 
	position.x = UV.x * 2.0f - 1.0f; 
	position.y = -(UV.y * 2.0f - 1.0f); 
	position.z = depth; 
	//Transform Position from Homogenous Space to World Space 
	position = mul(position, InverseViewProjection); 
	position /= position.w;
	return position.xyz;
}


Lassen Sie mich daran erinnern, dass Sie zum Erstellen von GBuffer eine Technik wie MRT ( Multiple Render Targets ) benötigen , die das Modell auf einmal in mehrere Render Targets zeichnet (und jede RT unterschiedliche Informationen enthält). Eine der Regeln von der MRT - die Dimension aller Render Ziel sollte sein , die gleiche . Im Fall von Color Map , Normal Map - Surface.Color : 32-Bit- RT , wobei es für jeden ARGB- Kanal 8 Bits gibt, d.h. 256 Abstufungen von 0 bis 1.

Dank dieses Ansatzes können wir komplexe Effekte auf jede Geometrie anwenden, z. B. den beliebtesten Screen Space-Effekt: SSAO (Screen Space Ambient Occlusion). Dieser Algorithmus analysiert die Tiefe und die normalen Puffer, wobei der Grad der Schattierung gezählt wird. Der gesamte Algorithmus , den ich nicht beschreiben, wurde er beschrieben Habré, außer zu sagen , dass die Aufgabe des Algorithmus auf die Tiefenkarte reduziert Tracing: Wir haben eine Reihe von Zufallsvektoren aus dem Lese gerichtet „Pixel“ , und wir müssen die Anzahl der Kreuzungen mit Geometrie finden.

Beispieleffekt (links ohne SSAO, rechts mit SSAO):
Bild


Gerade Deferred Shading ist der Platz auf dem Bildschirm Effekt. Das heißt Für jede Lichtquelle auf dem Bildschirm (ohne Optimierungen) zeichnen wir im Additiv- Modus im sogenannten RenderTarget : Light Map ein Quad . Und wenn wir die Weltposition des „Pixels“ kennen, seine normale Position der Lichtquelle, können wir die Beleuchtung dieses Pixels berechnen.

Beispiel für eine verzögerte Schattierung (die Beleuchtung wird nach dem Rendern der Geometrie verzögert):

Bild


Vorteile und Probleme von Screen Space-Effekten

Das wichtigste Plus der Bildschirmraumeffekte ist die Unabhängigkeit der Komplexität des Effekts von der Geometrie.

Das wichtigste Minus ist die Lokalität aller Effekte. Tatsache ist, dass wir ständig auf Information Lost stoßen , was in vielen Fällen stark von der Überprüfung abhängt , da die SSE von benachbarten Texeltiefen abhängt, die von jeder Geometrie generiert werden können.

Gut, es ist erwähnenswert, dass Screen Space-Effekte vollständig auf der GPU ausgeführt und nachbearbeitet werden.

Endlich SSLR


Nach all der Theorie haben wir einen solchen Effekt kommen, wie der Platz auf dem Bildschirm die Local Reflections : lokale Reflexion im Bildschirmraum.

Zunächst beschäftigen wir uns mit der perspektivischen Projektion:

Bild


Horizontale und vertikale Blickwinkel ist gegeben FOV (typischerweise 45 Grad, ziehe ich es bis 60 Grad), weil sie in der virtuellen Kamera unterschiedlich sind Das Seitenverhältnis (Seitenverhältnis) wird ebenfalls berücksichtigt .

Das Projektionsfenster (in dem wir mit UV- Raumdaten arbeiten) ist das, was wir sehen, und dann projizieren wir unsere Szene.
Die vordere und hintere Schnittebene, Nahebene bzw. Fernebene , werden in derselben Projektion wie die Parameter festgelegt. Bei verzögertem Rendern ist der Far Plane- Wert zu groß , weil Die Genauigkeit des Tiefenpuffers wird dramatisch sinken: alles hängt von der Szene ab.

Wenn wir nun die Projektionsmatrix und die Position auf dem Projektionsfenster (sowie die Tiefe) für jedes Pixel kennen, berechnen wir seine Position wie folgt:

float3 GetPosition(float2 UV, float depth)
{
	float4 position = 1.0f; 
	position.x = UV.x * 2.0f - 1.0f; 
	position.y = -(UV.y * 2.0f - 1.0f); 
	position.z = depth; 
	position = mul(position, InverseViewProjection); 
	position /= position.w;
	return position.xyz;
}


Nachdem wir den Blickvektor für dieses Pixel finden müssen:

float3 viewDir = normalize(texelPosition - CameraPosition);

Die CameraPosition ist die Kameraposition.
Und finde die Reflexion dieses Vektors von der Normalen im aktuellen Pixel:

float3 reflectDir = normalize(reflect(viewDir, texelNormal));

Als nächstes wird die Aufgabe auf das Verfolgen der Tiefenkarte reduziert. Das heißt Wir müssen den Schnittpunkt des reflektierten Vektors mit einer Geometrie finden. Es ist klar, dass die Ablaufverfolgung durch Iterationen erfolgt. Und wir sind in ihnen sehr begrenzt. Weil Jede Tiefenkartenauswahl ist die Zeit wert. In meiner Version nehmen wir eine anfängliche Annäherung L und ändern sie dynamisch basierend auf dem Abstand zwischen unserem Texel und der Position, die wir „wiederhergestellt“ haben:

float3 currentRay = 0;
float3 nuv = 0;
float L = LFactor;
for(int i = 0; i < 10; i++)
{
    currentRay = texelPosition + reflectDir * L;
    nuv = GetUV(currentRay); // проецирование позиции на экран
    float n = GetDepth(nuv.xy); // чтение глубины из DepthMap по UV
    float3 newPosition = GetPosition2(nuv.xy, n);
    L = length(texelPosition - newPosition);
}


Hilfsfunktionen, Übertragung eines Weltpunktes auf den Bildschirmraum:

float3 GetUV(float3 position)
{
	 float4 pVP = mul(float4(position, 1.0f), ViewProjection);
	 pVP.xy = float2(0.5f, 0.5f) + float2(0.5f, -0.5f) * pVP.xy / pVP.w;
	 return float3(pVP.xy, pVP.z / pVP.w);
}


Nach Abschluss der Iterationen haben wir die Position „Schnittpunkt mit reflektierter Geometrie“. Und unser Nuv-Wert ist die Projektion dieser Kreuzung auf den Bildschirm, d.h. nuv.xy sind die UV- Koordinaten in unserem Bildschirmraum und nuv.z ist die wiederhergestellte Tiefe (d. h. abs (GetDepth (nuv.xy) -nuv.z) muss sehr klein sein) .

Am Ende der Iterationen zeigt L den Abstand des reflektierten Pixels. Der letzte Schritt ist das Hinzufügen einer Reflektion zur Farbkarte :

float3 cnuv = GetColor(nuv.xy).rgb;
return float4(cnuv, 1);


Verdünnen Sie die Theorie mit Abbildungen, das Originalbild (der Inhalt der Color Map von GBuffer):


Nach dem Kompilieren des Shaders (Reflection) erhalten wir folgendes Bild (Color Map aus GBuffer + SSLR-Shader-Ergebnis):



Nicht viel . Und hier ist noch einmal daran zu erinnern, dass Space-Screen- Effekte einen soliden Informationsverlust darstellen (Beispiele sind in roten Rahmen hervorgehoben).

Tatsache ist, dass wenn der Reflexionsvektor über den Space-Screen hinausgeht , die Informationen über die Color- Map unzugänglich werden und wir eine Begrenzung unserer UV- Strahlung sehen .

Um dieses Problem teilweise zu beheben, kann ein zusätzlicher Koeffizient eingeführt werden, der den „Reflexionsbereich“ widerspiegelt. Und weiter über diesen Koeffizienten werden wir die Reflexion verschleiern, das Problem ist teilweise gelöst:

L = saturate(L * LDelmiter);
float error *= (1 - L);


Ergebnis, Reflexion multipliziert mit Fehler (Versuch, das SSLR-Artefakt zu entfernen - Information verloren):



Schon besser, aber wir bemerken ein anderes Problem, was passiert, wenn der Vektor in Richtung der Kamera reflektiert wird? Spann ‚und UV wird nicht auftreten, aber trotz der Dringlichkeit der UV (x> 0, y> 0, x <1, y <1), wird es falsch sein:



Dieses Problem kann auch teilweise gelöst werden, wenn Sie die Winkel der zulässigen Reflexionen einschränken. Ein Chip mit Winkeln aus dem Fresnel-Effekt ist dafür ideal :

float fresnel = dot(viewDir, texelNormal);

Wir modifizieren die Formel ein wenig:

float fresnel = 0.0 + 2.8 * pow(1+dot(viewDir, texelNormal), 2);

Fresnel-Werte unter Berücksichtigung der normalen Zuordnung (Werte der Fresnel-Variablen für den SSLR-Algorithmus):



Die Bereiche, die in der „Kamera“ reflektiert werden, sind schwarz und werden von uns nicht berücksichtigt (stattdessen können Sie eine Überblendung in eine kubische Textur vornehmen).

Reflexion multipliziert mit Fehler und Fresnel (Versuch, die meisten SSLR-Artefakte zu entfernen):



Übrigens sollte der Wert von Fresnel durch einen Parameter begrenzt werden, weil Aufgrund der „Rauheit“ der Normalen ist der Wert eine Größenordnung größer als eins (oder eine andere Begrenzerzahl).

Und die letzte Stufe des heutigen Artikels ist das Verwischen von Reflexionen, weil perfekte reflexion nur am spiegel. Der Grad der Unschärfe kann als 1-Fehler angesehen werden (je weiter das reflektierte Pixel entfernt ist, desto unschärfer ist es). Dies ist eine Art Unschärfegewicht und kann im Alpha-Kanal der RT- Reflexionen gespeichert werden .

Ergebnis (endgültiges Bild mit entfernten Artefakten und verschwommenen Reflexionen):



Fazit


Darüber hinaus sollten einige Informationen zum Reflexionsvermögen hinzugefügt werden: Wie deutlich ist die Reflexion, wie stark kann die Oberfläche im Allgemeinen reflektieren, wenn SSLR nicht funktioniert? Fügen Sie eine statische Reflexion einer kubischen Textur hinzu.

Natürlich sind Space-Screen- Effekte nicht fair und Entwickler versuchen, Artefakte zu verbergen. In Echtzeit ist dies jedoch nicht möglich (bei komplexer Geometrie). Und ohne solche Effekte sieht das Spiel irgendwie falsch aus. Ich habe die allgemeine SSLR- Methode beschrieben : Ich habe die Glanzlichter aus dem Shader hervorgehoben. Leider kann ich den Code nicht anhängen, weil Es gibt zu viele Abhängigkeiten im Projekt.

Erfolgreiche Entwicklungen! ;)

Jetzt auch beliebt: