Reflektierende Schattenkarten: Teil 2 - Implementierung

Hallo Habr! Dieser Artikel enthält eine einfache Implementierung von Reflective Shadow Maps (der Algorithmus wurde in einem früheren Artikel beschrieben ). Als nächstes werde ich erklären, wie ich es gemacht habe und welche Fallstricke es gab. Einige mögliche Optimierungen werden ebenfalls berücksichtigt.

Bild
Abbildung 1: Von links nach rechts: ohne RSM, mit RSM, Unterschied

Ergebnis


In Abbildung 1 sehen Sie das mit RSM erzielte Ergebnis. Zur Erstellung dieser Bilder wurden das „Stanford Rabbit“ und drei mehrfarbige Vierecke verwendet. Im Bild links sehen Sie das Ergebnis des Renderns ohne RSM , wobei nur Punktlicht verwendet wird . Alles im Schatten ist komplett schwarz. Das Bild in der Mitte zeigt das Ergebnis mit RSM . Folgende Unterschiede sind erkennbar: Überall dort, wo es hellere Farben gibt, rosa, die den Boden und das Kaninchen überfluten, ist die Schattierung nicht vollständig schwarz. Das letzte Bild zeigt den Unterschied zwischen dem ersten und dem zweiten und damit den Beitrag von RSM . Engere Kanten und Artefakte sind im mittleren Bild sichtbar. Dies kann jedoch durch Anpassen der Größe des Kerns, der Intensität der indirekten Beleuchtung und der Anzahl der Proben behoben werden.

Implementierung


Der Algorithmus wurde auf einer eigenen Engine implementiert. Die Shader sind in HLSL geschrieben, und das Rendern ist in DirectX 11. Ich habe bereits vor dem Schreiben dieses Artikels eine verzögerte Schattierung und Schattenzuordnung für gerichtetes Licht (gerichtete Lichtquelle) eingerichtet. Zuerst habe ich RSM für gerichtetes Licht implementiert und erst nachdem ich Unterstützung für die Schattenkarte und RSM für Punktlicht hinzugefügt habe.

Schattenkartenerweiterung


Traditionell ist Shadow Maps (SM) nichts anderes als eine Tiefenkarte. Dies bedeutet, dass Sie nicht einmal einen Pixel- / Fragment-Shader benötigen, um SM zu füllen. Für RSM benötigen Sie jedoch einige zusätzliche Puffer. Sie müssen die Weltraumposition, die Weltraumnormalen und den Fluss (Lichtleistung) speichern. Dies bedeutet, dass Sie einen Pixel- / Fragment-Shader mit mehreren Renderzielen benötigen. Denken Sie daran, dass Sie für diese Technik das Keulen des Gesichts abschneiden müssen, nicht die Vorderseite.

Die Verwendung von Face-Culling- Vorderkanten ist eine weit verbreitete Methode, um Schattenartefakte zu vermeiden. Dies funktioniert jedoch nicht mit RSM .

Sie übergeben die Weltraumpositionen und Normalen an den Pixel-Shader und schreiben sie in die entsprechenden Puffer. Wenn Sie die normale Zuordnung verwenden , berechnen Sie diese auch im Pixel-Shader. Der Fluss wird dort berechnet, indem Albedomaterial mit der Farbe der Lichtquelle multipliziert wird. Für Spotlicht müssen Sie den resultierenden Wert mit dem Einfallswinkel multiplizieren. Für gerichtetes Licht wird ein nicht schattiertes Bild erhalten.

Vorbereitung für die Lichtberechnung


Es gibt ein paar Dinge, die Sie für die Hauptpassage tun müssen. Sie müssen alle im Schattenpass verwendeten Puffer als Texturen binden. Sie benötigen auch Zufallszahlen. Der offizielle Artikel besagt, dass Sie diese Zahlen vorberechnen und in einem Puffer speichern müssen, um die Anzahl der Operationen im RSM- Abtastdurchlauf zu verringern. Da der Algorithmus sehr leistungsintensiv ist, stimme ich dem offiziellen Artikel voll und ganz zu. Es wird dort auch empfohlen, die zeitliche Kohärenz einzuhalten (für alle indirekten Beleuchtungsberechnungen das gleiche Stichprobenmuster verwenden). Dies verhindert ein Flackern, wenn jeder Frame einen anderen Schatten verwendet.

Sie benötigen für jede Stichprobe zwei zufällige Gleitkommazahlen im Bereich [0, 1]. Diese Zufallszahlen werden verwendet, um die Koordinaten der Stichprobe zu bestimmen. Sie benötigen außerdem dieselbe Matrix, mit der Sie Positionen vom Weltraum (Weltraum) in den Schattenraum (Lichtquellenraum) konvertieren. Sie benötigen solche Parameter auch für die Abtastung, die eine schwarze Farbe ergeben, wenn Sie über die Grenzen der Textur hinaus abtasten.

Beleuchtung passieren


Nun ist der schwer zu verstehende Teil. Ich empfehle, dass Sie die indirekte Beleuchtung berechnen, nachdem Sie die direkte Beleuchtung für eine bestimmte Lichtquelle berechnet haben. Dies liegt daran, dass Sie ein Vollbild-Quad für gerichtetes Licht benötigen. Für Punkt- und Punktlicht möchten Sie jedoch normalerweise Netze einer bestimmten Form mit Keulung verwenden , um weniger Pixel zu füllen.

Im folgenden Code wird die indirekte Beleuchtung für das Pixel berechnet. Als nächstes werde ich erklären, was dort passiert.

float3 DoReflectiveShadowMapping(float3 P, bool divideByW, float3 N) { float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); } 

Das erste Argument für die Funktion ist P , dh die Weltraumposition (im Weltraum) für ein bestimmtes Pixel. DivideByW wird für die voraussichtliche Division verwendet, die erforderlich ist, um den korrekten Z- Wert zu erhalten. N ist der Weltraum normal.

 float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; 

In diesem Teil des Codes wird die Position des Lichtraums (relativ zur Lichtquelle) berechnet, die indirekte Beleuchtungsvariable initialisiert, in der die aus jeder Probe berechneten Werte summiert werden, und die rMax- Variable wird aus der Beleuchtungsgleichung im offiziellen Artikel festgelegt , deren Wert ich im nächsten Abschnitt erläutern werde.

 for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; 

Hier starten wir den Zyklus und bereiten unsere Variablen für die Gleichung vor. Zu Optimierungszwecken enthalten die von mir berechneten Zufallsstichproben bereits Koordinatenversätze, dh um die UV-Koordinaten zu erhalten, muss ich nur rMax * rnd zu den Lichtraumkoordinaten hinzufügen. Wenn die resultierenden UV-Strahlen außerhalb des Bereichs [0,1] liegen, sollten die Proben schwarz sein. Das ist logisch, da sie über den Beleuchtungsbereich hinausgehen.

  float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); 

Dies ist der Teil, in dem die indirekte Beleuchtungsgleichung berechnet ( Abbildung 2 ) und auch gemäß dem Abstand von der Lichtraumkoordinate zur Probe gewogen wird. Die Gleichung sieht einschüchternd aus und der Code hilft nicht, alles zu verstehen, daher werde ich dies genauer erläutern.

Die Variable Φ (phi) ist der Lichtfluss , der die Strahlungsintensität ist. Der vorherige Artikel beschreibt den Fluss detaillierter.

Flussskalen mit zwei skalaren Kunstwerken. Die erste liegt zwischen der Normalen der Lichtquelle (Texel) und der Richtung von der Lichtquelle zur aktuellen Position. Die zweite liegt zwischen der Stromnormalen und dem Richtungsvektor von der aktuellen Position zur Position der Lichtquelle (Texel). Um keinen negativen Beitrag zur Beleuchtung zu leisten (es stellt sich heraus, dass das Pixel nicht beleuchtet ist), sind Skalarprodukte auf den Bereich [0, ∞] beschränkt. In dieser Gleichung wird die Normalisierung vermutlich aus Leistungsgründen am Ende durchgeführt. Es ist ebenso akzeptabel, Richtungsvektoren zu normalisieren, bevor skalare Produkte durchgeführt werden.

Bild
Abbildung 2: Gleichung der Beleuchtungsstärke eines Punktes mit Position x und normaler n- gerichteter Pixellichtquelle p

Das Ergebnis dieses Durchlaufs kann mit einem Backbuffer (direkte Beleuchtung) gemischt werden. Das Ergebnis ist wie in Abbildung 1 dargestellt .

Fallstricke


Bei der Implementierung dieses Algorithmus sind einige Probleme aufgetreten. Ich werde über diese Probleme sprechen, damit Sie nicht auf denselben Rechen treten.

Falscher Sampler


Ich habe viel Zeit damit verbracht herauszufinden, warum sich meine indirekte Beleuchtung wiederholt. Die Texturen von Crytek Sponza sind versteckt, daher benötigen Sie einen verpackten Sampler. Aber für RSM ist es nicht sehr geeignet.

Opengl
OpenGL setzt RSM- Texturen auf GL_CLAMP_TO_BORDER

Benutzerdefinierte Werte


Um den Workflow zu verbessern, ist es wichtig, einige Variablen per Knopfdruck ändern zu können. Zum Beispiel die Intensität der indirekten Beleuchtung und der Abtastbereich ( rMax ). Diese Parameter müssen für jede Lichtquelle angepasst werden. Wenn Sie einen großen Abtastbereich haben, erhalten Sie von überall eine indirekte Beleuchtung, was für große Szenen nützlich ist. Für eine lokalere indirekte Beleuchtung benötigen Sie eine kleinere Reichweite. Abbildung 3 zeigt die globale und lokale indirekte Beleuchtung.

Bild
Abbildung 3: Demonstration der rMax- Abhängigkeit.

Separate Passage


Zuerst dachte ich, ich könnte indirekte Beleuchtung in einem Shader machen, in dem ich direkte Beleuchtung betrachte. Bei gerichtetem Licht funktioniert dies, da Sie immer noch ein Vollbild-Quad zeichnen. Für Punkt- und Punktlicht müssen Sie jedoch die Berechnung der indirekten Beleuchtung optimieren. Daher habe ich die indirekte Beleuchtung als separaten Durchgang betrachtet, der erforderlich ist, wenn Sie auch eine Bildschirmrauminterpolation durchführen möchten.

Cache


Dieser Algorithmus ist mit dem Cache überhaupt nicht vertraut. Es führt Stichproben an zufälligen Punkten in mehreren Texturen durch. Die Anzahl der Proben ohne Optimierungen ist ebenfalls unannehmbar groß. Mit einer Auflösung von 1280 * 720 und der Anzahl der RSM 400-Abtastwerte erstellen Sie 1.105.920.000 Abtastwerte für jede Lichtquelle.

Dafür und dagegen


Ich werde die Vor- und Nachteile dieses indirekten Beleuchtungsberechnungsalgorithmus auflisten.
FürGegen
Einfach zu verstehender AlgorithmusÜberhaupt keine Freunde mit Cache
Lässt sich gut in den verzögerten Renderer integrierenVariable Einstellung erforderlich
Kann in anderen Algorithmen ( LPV ) verwendet werdenErzwungene Wahl zwischen lokaler und globaler indirekter Beleuchtung

Optimierungen


Ich habe mehrere Versuche unternommen, die Geschwindigkeit dieses Algorithmus zu erhöhen. Wie im offiziellen Artikel beschrieben , können Sie die Bildschirmrauminterpolation implementieren. Ich habe das gemacht und ein bisschen schneller gerendert. Im Folgenden werde ich einige Optimierungen beschreiben und einen Vergleich (in Bildern pro Sekunde) zwischen den folgenden Implementierungen anhand einer Szene mit 3 Wänden und einem Kaninchen durchführen: ohne RSM , naive Implementierung von RSM , interpoliert von RSM .

Z-Check


Einer der Gründe, warum mein RSM ineffizient funktionierte, war, dass ich auch die indirekte Beleuchtung für Pixel berechnet habe, die Teil der Skybox waren. Skybox braucht es definitiv nicht.

CPU-Zufallsstichprobe


Die vorläufige Berechnung von Samples führt nicht nur zu einer größeren zeitlichen Kohärenz, sondern erspart Ihnen auch die Neuberechnung dieser Samples im Shader.

Bildschirmrauminterpolation


In einem offiziellen Artikel wird vorgeschlagen, ein Renderziel mit niedriger Auflösung zur Berechnung der indirekten Beleuchtung zu verwenden. Bei Szenen mit vielen glatten Normalen und geraden Wänden können Beleuchtungsinformationen leicht zwischen Punkten mit niedrigerer Auflösung interpoliert werden. Ich werde die Interpolation nicht im Detail beschreiben, so dass dieser Artikel etwas kürzer ist.

Fazit


Nachfolgend sind die Ergebnisse für eine andere Anzahl von Proben aufgeführt. Ich habe einige Kommentare zu diesen Ergebnissen:

  • Logischerweise bleibt der FPS für eine andere Anzahl von Abtastwerten bei etwa 700, wenn keine RSM- Berechnung durchgeführt wird.
  • Die Interpolation verursacht einen gewissen Overhead und ist bei einer kleinen Anzahl von Abtastwerten nicht sehr nützlich.
  • Selbst mit 100 Samples sah das endgültige Bild ziemlich gut aus. Dies kann auf eine Interpolation zurückzuführen sein, die die indirekte Beleuchtung „verwischt“.

ProbenanzahlFPS für kein RSMFPS für naives RSMFPS für interpoliertes RSM
100~ 700152264
200~ 70089179
300~ 70062138
400~ 70044116

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


All Articles