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.
 Abbildung 1: Von links nach rechts: ohne RSM, mit RSM, Unterschied
Abbildung 1: Von links nach rechts: ohne RSM, mit RSM, UnterschiedErgebnis
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.
 Abbildung 2: Gleichung der Beleuchtungsstärke eines Punktes mit Position x und normaler n- gerichteter Pixellichtquelle p
Abbildung 2: Gleichung der Beleuchtungsstärke eines Punktes mit Position x und normaler n- gerichteter Pixellichtquelle pDas 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.
OpenglOpenGL 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.
 Abbildung 3: Demonstration der rMax- Abhängigkeit.
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ür | Gegen | 
| Einfach zu verstehender Algorithmus | Überhaupt keine Freunde mit Cache | 
| Lässt sich gut in den verzögerten Renderer integrieren | Variable Einstellung erforderlich | 
| Kann in anderen Algorithmen ( LPV ) verwendet werden | Erzwungene 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“.
| Probenanzahl | FPS für kein RSM | FPS für naives RSM | FPS für interpoliertes RSM | 
| 100 | ~ 700 | 152 | 264 | 
| 200 | ~ 700 | 89 | 179 | 
| 300 | ~ 700 | 62 | 138 | 
| 400 | ~ 700 | 44 | 116 |