Der erste Teil der Übersetzung ist
hier . In diesem Teil werden wir über die Auswirkungen von Schärfe, durchschnittlicher Helligkeit, Mondphasen und atmosphärischen Phänomenen während des Regens sprechen.
Teil 6. Schärfen
In diesem Teil werden wir uns einen weiteren Nachbearbeitungseffekt von The Witcher 3 - Sharpen genauer ansehen.
Durch das Schärfen wird das Ausgabebild etwas schärfer. Dieser Effekt ist uns aus Photoshop und anderen Grafikeditoren bekannt.
In The Witcher 3 hat das Schärfen zwei Möglichkeiten: niedrig und hoch. Ich werde unten auf den Unterschied zwischen ihnen eingehen, aber jetzt schauen wir uns die Screenshots an:
Option „Niedrig“ - bis zuOption “Niedrig” - danachHohe Option - bis zuOption "Hoch" - nachWenn Sie sich detailliertere (interaktive) Vergleiche ansehen möchten, lesen Sie den
Abschnitt im Nvidia The Witcher 3-Leistungshandbuch . Wie Sie sehen, macht sich der Effekt besonders bei Gras und Laub bemerkbar.
In diesem Teil des Beitrags werden wir den Rahmen von Anfang an studieren: Ich habe ihn absichtlich gewählt, weil wir hier das Relief (lange Zeichenentfernung) und die Kuppel des Himmels sehen.
In Bezug auf die Eingabe erfordert das Schärfen einen Farbpuffer
t0 (LDR nach Tonkorrektur und Linseneffekten) und einen Tiefenpuffer
t1 .
Lassen Sie uns den Assembler-Code für den Pixel-Shader untersuchen:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb3[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_input_ps_siv v0.xy, position
dcl_output o0.xyzw
dcl_temps 7
0: ftoi r0.xy, v0.xyxx
1: mov r0.zw, l(0, 0, 0, 0)
2: ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t1.xyzw
3: mad r0.x, r0.x, cb12[22].x, cb12[22].y
4: mad r0.y, r0.x, cb12[21].x, cb12[21].y
5: max r0.y, r0.y, l(0.000100)
6: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
7: mad_sat r0.y, r0.y, cb3[1].z, cb3[1].w
8: add r0.z, -cb3[1].x, cb3[1].y
9: mad r0.y, r0.y, r0.z, cb3[1].x
10: add r0.y, r0.y, l(1.000000)
11: ge r0.x, r0.x, l(1.000000)
12: movc r0.x, r0.x, l(0), l(1.000000)
13: mul r0.z, r0.x, r0.y
14: round_z r1.xy, v0.xyxx
15: add r1.xy, r1.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)
16: div r1.xy, r1.xyxx, cb3[0].zwzz
17: sample_l(texture2d)(float,float,float,float) r2.xyz, r1.xyxx, t0.xyzw, s0, l(0)
18: lt r0.z, l(0), r0.z
19: if_nz r0.z
20: div r3.xy, l(0.500000, 0.500000, 0.000000, 0.000000), cb3[0].zwzz
21: add r0.zw, r1.xxxy, -r3.xxxy
22: sample_l(texture2d)(float,float,float,float) r4.xyz, r0.zwzz, t0.xyzw, s0, l(0)
23: mov r3.zw, -r3.xxxy
24: add r5.xyzw, r1.xyxy, r3.zyxw
25: sample_l(texture2d)(float,float,float,float) r6.xyz, r5.xyxx, t0.xyzw, s0, l(0)
26: add r4.xyz, r4.xyzx, r6.xyzx
27: sample_l(texture2d)(float,float,float,float) r5.xyz, r5.zwzz, t0.xyzw, s0, l(0)
28: add r4.xyz, r4.xyzx, r5.xyzx
29: add r0.zw, r1.xxxy, r3.xxxy
30: sample_l(texture2d)(float,float,float,float) r1.xyz, r0.zwzz, t0.xyzw, s0, l(0)
31: add r1.xyz, r1.xyzx, r4.xyzx
32: mul r3.xyz, r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000)
33: mad r1.xyz, -r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000), r2.xyzx
34: max r0.z, abs(r1.z), abs(r1.y)
35: max r0.z, r0.z, abs(r1.x)
36: mad_sat r0.z, r0.z, cb3[2].x, cb3[2].y
37: mad r0.x, r0.y, r0.x, l(-1.000000)
38: mad r0.x, r0.z, r0.x, l(1.000000)
39: dp3 r0.y, l(0.212600, 0.715200, 0.072200, 0.000000), r2.xyzx
40: dp3 r0.z, l(0.212600, 0.715200, 0.072200, 0.000000), r3.xyzx
41: max r0.w, r0.y, l(0.000100)
42: div r1.xyz, r2.xyzx, r0.wwww
43: add r0.y, -r0.z, r0.y
44: mad r0.x, r0.x, r0.y, r0.z
45: max r0.x, r0.x, l(0)
46: mul r2.xyz, r0.xxxx, r1.xyzx
47: endif
48: mov o0.xyz, r2.xyzx
49: mov o0.w, l(1.000000)
50: ret
50 Zeilen Assembler-Code scheinen eine machbare Aufgabe zu sein. Kommen wir zur Lösung.
Wertschöpfung schärfen
Der erste Schritt ist das Laden des Tiefenpuffers (Zeile 1). Es ist erwähnenswert, dass der „The Witcher 3“ eine umgekehrte Tiefe verwendet (1,0 - nah, 0,0 - fern). Wie Sie vielleicht wissen, ist die Hardwaretiefe nichtlinear gebunden (
Einzelheiten finden Sie in
diesem Artikel ).
Die Zeilen 3-6 bieten eine sehr interessante Möglichkeit, diese Hardwaretiefe [1.0 - 0.0] mit [Nah-Fern] -Werten zu verknüpfen (wir setzen sie in der MatrixPerspectiveFov-Phase). Betrachten Sie die Werte aus dem konstanten Puffer:
Mit dem Wert "close" von 0,2 und dem Wert "far" 5000 können wir die Werte von cb12_v21.xy wie folgt berechnen:
cb12_v21.y = 1.0 / near
cb12_v21.x = - (1.0 / near) + (1.0 / near) * (near / far)
Dieser Code ist in TW3-Shadern weit verbreitet, daher denke ich, dass er nur eine Funktion ist.
Nach Erhalt der „Tiefe der Sichtbarkeitspyramide“ verwendet Zeile 7 die Skalierung / Verzerrung, um den Interpolationskoeffizienten zu erstellen (hier verwenden wir
Sättigung , um die Werte auf das Intervall [0-1] zu beschränken).
cb3_v1.xy und cb3_v2.xy - Dies ist die Helligkeit des Schärfeeffekts bei kurzen und langen Entfernungen. Nennen wir sie sharpenNear und sharpenFar. Und dies ist der einzige Unterschied zwischen den Optionen "Niedrig" und "Hoch" dieses Effekts in The Witcher 3.
Jetzt ist es Zeit, das resultierende Verhältnis zu verwenden. Die Zeilen 8-9 führen nur
lerp(sharpenNear, sharpenFar, interpolationCoeff)
. Wofür ist das? Dank dessen erhalten wir unterschiedliche Helligkeit in der Nähe von Geralt und von ihm weg. Schauen Sie mal rein:
Vielleicht ist dies kaum wahrnehmbar, aber hier haben wir basierend auf dem Abstand die Schärfehelligkeit neben dem Player (2.177151) interpoliert und die Effekthelligkeit ist sehr weit (1.91303). Nach dieser Berechnung addieren wir 1,0 zur Helligkeit (Zeile 10). Warum wird das benötigt? Angenommen, die oben gezeigte Operation lerp ergab 0,0. Nach dem Hinzufügen von 1.0 erhalten wir natürlich 1.0, und dies ist ein Wert, der das Pixel beim Schärfen nicht beeinflusst. Lesen Sie weiter unten mehr darüber.
Beim Schärfen möchten wir den Himmel nicht beeinflussen. Dies kann durch Hinzufügen einer einfachen bedingten Prüfung erreicht werden:
// sharpen
float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;
In The Witcher 3 beträgt der Wert der Pixeltiefe des Himmels 1,0, daher verwenden wir ihn, um eine Art „Binärfilter“ zu erhalten (eine interessante Tatsache: In diesem Fall funktioniert der
Schritt nicht richtig).
Jetzt können wir die interpolierte Helligkeit mit einem „Himmelsfilter“ multiplizieren:
Diese Multiplikation wird in Zeile 13 durchgeführt.
Shader-Codebeispiel:
// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;
Pixel Sampling Center
SV_Position hat einen Aspekt, der hier wichtig sein wird:
einen halben Pixelversatz . Es stellt sich heraus, dass dieses Pixel in der oberen linken Ecke (0, 0) keine Koordinaten (0, 0) in Bezug auf SV_Position.xy hat, sondern (0,5, 0,5). Wow!
Hier wollen wir ein Beispiel in der Mitte des Pixels nehmen, schauen wir uns also die Zeilen 14-16 an. Sie können sie in HLSL schreiben:
// .
// "" SV_Position.xy.
float2 uvCenter = trunc( Input.Position.xy );
// ,
uvCenter += float2(0.5, 0.5);
uvCenter /= g_Viewport.xy
Und später probieren wir die Eingabefarbtextur von texcoords „uvCenter“ aus. Keine Sorge, das Ergebnis der Stichprobe ist das gleiche wie bei der „normalen“ Methode (SV_Position.xy / ViewportSize.xy).
Schärfen oder nicht schärfen
Die Entscheidung, ob Sharpen verwendet werden soll, hängt von fSharpenAmount ab.
//
float3 colorCenter = TexColorBuffer.SampleLevel( samplerLinearClamp, uvCenter, 0 ).rgb;
//
float3 finalColor = colorCenter;
if ( fSharpenAmount > 0 )
{
// sharpening...
}
return float4( finalColor, 1 );
Schärfen
Es ist Zeit, einen Blick auf die Innenseiten des Algorithmus selbst zu werfen.
Im Wesentlichen werden die folgenden Aktionen ausgeführt:
- Abtastung der vierfachen Eingabefarbtextur an den Ecken des Pixels,
- fügt Stichproben hinzu und berechnet den Durchschnittswert,
- berechnet die Differenz zwischen "Mitte" und "Eckdurchschnitt",
- findet die maximale absolute Komponente der Differenz,
- korrigiert max. abs. Komponente unter Verwendung von Skala + Bias-Werten,
- bestimmt die Stärke des Effekts mit max. abs. Komponente
- berechnet den Helligkeitswert (Luma) für "centerColor" und "durchschnittlichColor",
- teilt colorCenter in seine Luma,
- berechnet einen neuen, interpolierten Luma-Wert basierend auf der Stärke des Effekts,
- Multipliziert colorCenter mit dem neuen Luma-Wert.
Viel Arbeit, und es war schwierig für mich, es herauszufinden, weil ich noch nie mit Schärfefiltern experimentiert hatte.
Beginnen wir mit dem Stichprobenmuster. Wie Sie im Assembler-Code sehen können, werden vier Texturlesevorgänge durchgeführt.
Dies wird am besten anhand eines Beispiels eines Pixelbilds gezeigt (das Können des Künstlers ist
ein Experte ):
Alle Lesevorgänge im Shader verwenden bilineares Sampling (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).
Der Versatz von der Mitte zu jedem der Winkel beträgt (± 0,5, ± 0,5), abhängig vom Winkel.
Sehen Sie, wie dies auf HLSL implementiert werden kann? Mal sehen:
float2 uvCorner;
float2 uvOffset = float2( 0.5, 0.5 ) / g_Viewport.xy; // remember about division!
float3 colorCorners = 0;
//
// -0,5, -0.5
uvCorner = uvCenter - uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// +0.5, -0.5
uvCorner = uvCenter + float2(uvOffset.x, -uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// -0.5, +0.5
uvCorner = uvCenter + float2(-uvOffset.x, uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// +0.5, +0.5
uvCorner = uvCenter + uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
Jetzt sind alle vier Beispiele in der Variablen „colorCorners“ zusammengefasst. Befolgen wir diese Schritte:
//
float3 averageColorCorners = colorCorners / 4.0;
//
float3 diffColor = colorCenter - averageColorCorners;
// . . RGB-
float fDiffColorMaxComponent = max( abs(diffColor.x), max( abs(diffColor.y), abs(diffColor.z) ) );
//
float fDiffColorMaxComponentScaled = saturate( fDiffColorMaxComponent * sharpenLumScale + sharpenLumBias );
// .
// "1.0" - fSharpenIntensity 1.0.
float fPixelSharpenAmount = lerp(1.0, fSharpenAmount, fDiffColorMaxComponentScaled);
// "" .
float lumaCenter = dot( LUMINANCE_RGB, finalColor );
float lumaCornersAverage = dot( LUMINANCE_RGB, averageColorCorners );
// "centerColor"
float3 fColorBalanced = colorCenter / max( lumaCenter, 1e-4 );
//
float fPixelLuminance = lerp(lumaCornersAverage, lumaCenter, fPixelSharpenAmount);
//
finalColor = fColorBalanced * max(fPixelLuminance, 0.0);
}
return float4(finalColor, 1.0);
Die Kantenerkennung erfolgt durch Berechnung von max. abs. Differenzkomponente. Kluger Schachzug! Schauen Sie sich die Visualisierung an:
Visualisierung der maximalen absoluten Komponente der Differenz.Großartig. Der fertige HLSL-Shader ist
hier verfügbar. Entschuldigung für die ziemlich schlechte Formatierung. Sie können mein
HLSLexplorer- Programm verwenden und mit dem Code experimentieren.
Ich kann glücklich sagen, dass der obige Code den gleichen Assembler-Code wie im Spiel erzeugt!
Zusammenfassend: Der Schärfeshader Witcher 3 ist sehr gut geschrieben (beachten Sie, dass fPixelSharpenAmount größer als 1,0 ist! Dies ist interessant ...). Darüber hinaus ist die Helligkeit von Nah- / Fernobjekten die Hauptmethode zum Ändern der Helligkeit des Effekts. In diesem Spiel sind sie keine Konstanten; Ich habe einige Beispiele für Werte gesammelt:
Skellige:
| sharpenNear | sharpenFar | sharpenDistanceScale | sharpenDistanceBias | sharpenLumScale | sharpenLumBias |
---|
niedrig |
hoch | 2.0 | 1.8 | 0,025
| -0,25
| -13,333333
| 1,33333 |
Kaer Morhen:
| sharpenNear
| sharpenFar
| sharpenDistanceScale
| sharpenDistanceBias
| sharpenLumScale
| sharpenLumBias
|
---|
niedrig
| 0,57751
| 0,31303
| 0,06665
| -0,33256
| -1,0
| 2.0
|
hoch
| 2.17751
| 1,91303
| 0,06665
| -0,33256
| -1,0
| 2.0 |
Teil 7. Durchschnittliche Helligkeit
Die Berechnung der durchschnittlichen Helligkeit des aktuellen Frames ist in fast jedem modernen Videospiel möglich. Dieser Wert wird häufig später für den Effekt der Augenanpassung und Tonwertkorrektur verwendet (siehe im
vorherigen Teil des Beitrags). In einfachen Lösungen wird die Helligkeitsberechnung beispielsweise für die Textur 512
2 verwendet , dann die Berechnung ihrer Mip-Pegel und deren Anwendung. Dies funktioniert normalerweise, schränkt jedoch die Möglichkeiten stark ein. Bei komplexeren Lösungen werden Computer-Shader verwendet, die beispielsweise eine
parallele Reduktion durchführen .
Lassen Sie uns in The Witcher 3 herausfinden, wie das CD Projekt Red-Team dieses Problem gelöst hat. Im vorherigen Teil habe ich bereits die Tonwertkorrektur und Anpassung des Auges untersucht, sodass das einzige verbleibende Puzzleteil die durchschnittliche Helligkeit war.
Die durchschnittliche Helligkeitsberechnung von The Witcher 3 besteht zunächst aus zwei Durchgängen. Aus Gründen der Klarheit habe ich beschlossen, sie in separate Teile zu zerlegen, und zunächst betrachten wir den ersten Durchgang - „Helligkeitsverteilung“ (Berechnung des Helligkeitshistogramms).
Helligkeitsverteilung
Diese beiden Durchgänge sind in jedem Frame-Analysator ziemlich leicht zu finden. Dies sind die
Versandaufrufe in der richtigen Reihenfolge, bevor die Augenanpassung durchgeführt wird:
Schauen wir uns die Eingabe für diesen Durchgang an. Er braucht zwei Texturen:
1) HDR-Farbpuffer, dessen Skalierung auf 1/4 x 1/4 reduziert ist (z. B. von 1920 x 1080 auf 480 x 270),
2) Vollbild-Tiefenpuffer
1/4 x 1/4 HDR-Farbpuffer. Beachten Sie den kniffligen Trick - dieser Puffer ist Teil eines größeren Puffers. Das Wiederverwenden von Puffern ist eine gute Praxis.Vollbild-TiefenpufferWarum den Farbpuffer verkleinern? Ich denke, es geht nur um Leistung.
Die Ausgabe dieses Durchlaufs ist ein strukturierter Puffer. 256 Elemente zu je 4 Bytes.
Shader haben hier keine Debugging-Informationen. Nehmen wir also an, es handelt sich nur um einen Puffer mit vorzeichenlosen int-Werten.
Wichtig: Der erste Schritt bei der Berechnung der durchschnittlichen Helligkeit ruft
ClearUnorderedAccessViewUint auf, um alle Elemente des strukturierten Puffers auf Null zurückzusetzen.
Lassen Sie uns den Assembler-Code des Computer-Shaders untersuchen (dies ist der erste Computer-Shader in unserer gesamten Analyse!)
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[3], immediateIndexed
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_uav_structured u0, 4
dcl_input vThreadGroupID.x
dcl_input vThreadIDInGroup.x
dcl_temps 6
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: store_structured g0.x, vThreadIDInGroup.x, l(0), l(0)
1: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
2: store_structured g0.x, r0.x, l(0), l(0)
3: store_structured g0.x, r0.y, l(0), l(0)
4: store_structured g0.x, r0.z, l(0), l(0)
5: sync_g_t
6: ftoi r1.x, cb0[2].z
7: mov r2.y, vThreadGroupID.x
8: mov r2.zw, l(0, 0, 0, 0)
9: mov r3.zw, l(0, 0, 0, 0)
10: mov r4.yw, l(0, 0, 0, 0)
11: mov r1.y, l(0)
12: loop
13: utof r1.z, r1.y
14: ge r1.z, r1.z, cb0[0].x
15: breakc_nz r1.z
16: iadd r2.x, r1.y, vThreadIDInGroup.x
17: utof r1.z, r2.x
18: lt r1.z, r1.z, cb0[0].x
19: if_nz r1.z
20: ld_indexable(texture2d)(float,float,float,float) r5.xyz, r2.xyzw, t0.xyzw
21: dp3 r1.z, r5.xyzx, l(0.212600, 0.715200, 0.072200, 0.000000)
22: imul null, r3.xy, r1.xxxx, r2.xyxx
23: ld_indexable(texture2d)(float,float,float,float) r1.w, r3.xyzw, t1.yzwx
24: eq r1.w, r1.w, cb0[2].w
25: and r1.w, r1.w, cb0[2].y
26: add r2.x, -r1.z, cb0[2].x
27: mad r1.z, r1.w, r2.x, r1.z
28: add r1.z, r1.z, l(1.000000)
29: log r1.z, r1.z
30: mul r1.z, r1.z, l(88.722839)
31: ftou r1.z, r1.z
32: umin r4.x, r1.z, l(255)
33: atomic_iadd g0, r4.xyxx, l(1)
34: endif
35: iadd r1.y, r1.y, l(64)
36: endloop
37: sync_g_t
38: ld_structured r1.x, vThreadIDInGroup.x, l(0), g0.xxxx
39: mov r4.z, vThreadIDInGroup.x
40: atomic_iadd u0, r4.zwzz, r1.x
41: ld_structured r1.x, r0.x, l(0), g0.xxxx
42: mov r0.w, l(0)
43: atomic_iadd u0, r0.xwxx, r1.x
44: ld_structured r0.x, r0.y, l(0), g0.xxxx
45: atomic_iadd u0, r0.ywyy, r0.x
46: ld_structured r0.x, r0.z, l(0), g0.xxxx
47: atomic_iadd u0, r0.zwzz, r0.x
48: ret
Und ein konstanter Puffer:
Wir wissen bereits, dass der erste Eingang ein HDR-Farbpuffer ist. Bei FullHD beträgt die Auflösung 480 x 270. Schauen wir uns den Versandaufruf an.
Versand (270, 1, 1) - Dies bedeutet, dass 270 Thread-Gruppen ausgeführt werden. Einfach ausgedrückt, führen wir eine Gruppe von Threads pro Zeile des Farbpuffers aus.
Jede Thread-Gruppe führt eine Zeile HDR-Farbpuffer ausNachdem wir diesen Kontext haben, versuchen wir herauszufinden, was der Shader tut.
Jede Thread-Gruppe hat 64 Threads in X-Richtung (dcl_thread_group 64, 1, 1) sowie gemeinsam genutzten Speicher, 256 Elemente mit jeweils 4 Bytes (dcl_tgsm_structured g0, 4, 256).
Beachten Sie, dass wir im Shader
SV_GroupThreadID (vThreadIDInGroup.x) [0-63] und
SV_GroupID (vThreadGroupID.x) [0-269] verwenden.
1) Wir beginnen damit, allen Elementen des gemeinsam genutzten Speichers Nullwerte zuzuweisen. Da der Gesamtspeicher 256 Elemente und 64 Threads pro Gruppe enthält, kann dies bequem mit einer einfachen Schleife durchgeführt werden:
// - .
// 64 , 4 .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = 0;
}
2) Danach setzen wir die Barriere mit
GroupMemoryBarrierWithGroupSync (sync_g_t). Wir tun dies, um sicherzustellen, dass alle Threads im gemeinsam genutzten Speicher der Gruppen auf Null zurückgesetzt werden, bevor Sie mit dem nächsten Schritt fortfahren.
3) Jetzt führen wir eine Schleife aus, die grob wie folgt geschrieben werden kann:
// cb0_v0.x - . 1920x1080 1920/4 = 480;
float ViewportSizeX = cb0_v0.x;
[loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )
{
...
Dies ist eine einfache for-Schleife mit einem Inkrement von 64 (haben Sie bereits verstanden, warum?).
Der nächste Schritt besteht darin, die Position des geladenen Pixels zu berechnen.
Lass uns darüber nachdenken.
Für die Y-Koordinate können wir SV_GroupID.x verwenden, da wir 270 Thread-Gruppen gestartet haben.
Für die X-Koordinate können wir ... den aktuellen Gruppenfluss nutzen! Lass es uns versuchen.
Da jede Gruppe 64 Threads enthält, werden bei einer solchen Lösung alle Pixel umgangen.
Betrachten Sie die Thread-Gruppe (0, 0, 0).
- Der Stream (0, 0, 0) verarbeitet die Pixel (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0), (384, 0), (448,0).
- Der Stream (1, 0, 0) verarbeitet die Pixel (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0), (449, 0) ...
- Der Stream (63, 0, 0) verarbeitet die Pixel (63, 0), (127, 0), (191, 0), (255, 0), (319, 0), (383, 0), (447, 0)
Somit werden alle Pixel verarbeitet.
Wir müssen auch sicherstellen, dass wir keine Pixel von außerhalb des Farbpuffers laden:
// X. Y GroupID.
uint CurrentPixelPositionX = PositionX + threadID;
uint CurrentPixelPositionY = groupID;
if ( CurrentPixelPositionX < ViewportSizeX )
{
// HDR- .
// HDR- , .
uint2 colorPos = uint2(CurrentPixelPositionX, CurrentPixelPositionY);
float3 color = texture0.Load( int3(colorPos, 0) ).rgb;
float luma = dot(color, LUMA_RGB);
Sehen Sie? Es ist ziemlich einfach!
Ich habe auch die Helligkeit berechnet (Zeile 21 des Assembler-Codes).
Großartig, wir haben die Helligkeit bereits aus einem Farbpixel berechnet. Der nächste Schritt ist das Laden (keine Probe!) Des entsprechenden Tiefenwerts.
Aber hier haben wir ein Problem, weil wir den Puffer der Tiefen voller Auflösung verbunden haben. Was tun?
Dies ist überraschend einfach - multiplizieren Sie colorPos einfach mit einer Konstanten (cb0_v2.z). Wir haben den HDR-Farbpuffer viermal verkleinert. daher ist der Wert 4!
const int iDepthTextureScale = (int) cb0_v2.z;
uint2 depthPos = iDepthTextureScale * colorPos;
float depth = texture1.Load( int3(depthPos, 0) ).x;
So weit so gut! Aber ... wir müssen die Zeilen 24-25 erreichen ...
24: eq r2.x, r2.x, cb0[2].w
25: and r2.x, r2.x, cb0[2].y
Also Zuerst haben wir einen
Vergleich der Gleitkomma-
Gleichheit , ihr Ergebnis ist in r2.x geschrieben, und gleich danach geht ... was? Bitweise
und ?? Wirklich? Für einen Gleitkommawert? Was zur Hölle???
Das Problem 'eq + and'Lassen Sie mich nur sagen, dass es für mich der schwierigste Teil des Shaders war. Ich habe sogar seltsame Asint / Asfloat-Kombinationen ausprobiert ...
Und wenn Sie einen etwas anderen Ansatz verwenden? Lassen Sie uns einfach den üblichen Float-Float-Vergleich in HLSL durchführen.
float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y);
return test;
}
Und hier ist die Ausgabe im Assembler-Code:
0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, l(0x3f800000)
2: ret
Interessant, oder? Ich hatte nicht erwartet, "und" hier zu sehen.
0x3f800000 ist nur 1.0f ... Es ist logisch, weil wir sonst 1.0 und 0.0 erhalten, wenn der Vergleich erfolgreich ist.
Aber was ist, wenn wir 1.0 durch einen anderen Wert ersetzen? Zum Beispiel so:
float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y) ? cb0_v0.z : 0.0;
return test;
}
Wir erhalten folgendes Ergebnis:
0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, cb0[0].z
2: ret
Ha! Es hat funktioniert. Dies ist nur die Magie des HLSL-Compilers. Hinweis: Wenn Sie 0.0 durch etwas anderes ersetzen, erhalten Sie nur movc.
Kehren wir zum Computational Shader zurück. Der nächste Schritt besteht darin, zu überprüfen, ob die Tiefe gleich cb0_v2.w ist. Es ist immer gleich 0.0 - mit anderen Worten, wir prüfen, ob sich ein Pixel in einer fernen Ebene (am Himmel) befindet. Wenn ja, dann weisen wir diesem Koeffizienten einen Wert von ungefähr 0,5 zu (ich habe mehrere Frames überprüft).
Dieser berechnete Koeffizient wird verwendet, um zwischen der Helligkeit der Farbe und der Helligkeit des „Himmels“ zu interpolieren (cb0_v2.x-Wert, der häufig ungefähr gleich 0,0 ist). Ich gehe davon aus, dass dies notwendig ist, um die Bedeutung des Himmels für die Berechnung der durchschnittlichen Helligkeit zu kontrollieren. Normalerweise wird die Wichtigkeit reduziert. Sehr clevere Idee.
// , ( ). , ,
// .
float value = (depth == cb0_v2.w) ? cb0_v2.y : 0.0;
// 'value' 0.0, lerp 'luma'. 'value'
// ( 0.50), luma . (cb0_v2.x 0.0).
float lumaOk = lerp( luma, cb0_v2.x, value );
Da wir lumaOk haben, besteht der nächste Schritt darin, seinen natürlichen Logarithmus zu berechnen, um eine gute Verteilung zu erstellen. Aber warten Sie, sagen wir, lumaOk ist 0.0. Wir wissen, dass der Wert von log (0) undefiniert ist, also addieren wir 1.0, weil log (1) = 0.0 ist.
Danach skalieren wir den berechneten Logarithmus auf 128, um ihn in 256 Zellen zu verteilen. Sehr schlau!
Und von hier aus wird dieser Wert 88.722839 genommen. Dies ist ein
128 * (2)
.
Auf diese Weise berechnet HLSL Logarithmen.
Es gibt nur eine Funktion im HLSL-Assembler-Code, die Logarithmen berechnet:
log , und sie hat eine Basis von 2.
// , lumaOk 0.0.
// log(0) undefined
// log(1) = 0.
//
lumaOk = log(lumaOk + 1.0);
// 128
lumaOk *= 128;
Schließlich berechnen wir den Index der Zelle aus der logarithmisch verteilten Helligkeit und addieren 1 zur entsprechenden Zelle im gemeinsam genutzten Speicher.
// . Uint, 256 ,
// , .
uint uLuma = (uint) lumaOk;
uLuma = min(uLuma, 255);
// 1 .
InterlockedAdd( shared_data[uLuma], 1 );
Im nächsten Schritt wird erneut eine Barriere festgelegt, um sicherzustellen, dass alle Pixel in der Zeile verarbeitet wurden.
Der letzte Schritt besteht darin, dem strukturierten Puffer Werte aus dem gemeinsam genutzten Speicher hinzuzufügen. Dies geschieht auf die gleiche Weise über eine einfache Schleife:
// ,
GroupMemoryBarrierWithGroupSync();
// .
[unroll] for (uint idx = 0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
uint data = shared_data[offset];
InterlockedAdd( g_buffer[offset], data );
}
Nachdem alle 64 Threads in der Thread-Gruppe die gemeinsamen Daten ausgefüllt haben, fügt jeder Thread dem Ausgabepuffer 4 Werte hinzu.
Betrachten Sie den Ausgabepuffer. Lass uns darüber nachdenken. Die Summe aller Werte im Puffer entspricht der Gesamtzahl der Pixel! (bei 480 × 270 = 129.600). Das heißt, wir wissen, wie viele Pixel einen bestimmten Helligkeitswert haben.
Wenn Sie sich mit Computer-Shadern (wie ich) schlecht auskennen, ist dies zunächst möglicherweise nicht klar. Lesen Sie den Beitrag daher noch einige Male, nehmen Sie Papier und einen Bleistift und versuchen Sie, die Konzepte zu verstehen, auf denen diese Technik basiert.
Das ist alles! So berechnet The Witcher 3 ein Helligkeitshistogramm. Persönlich habe ich beim Schreiben dieses Teils viel gelernt. Herzlichen Glückwunsch an die Jungs von CD Projekt Red für ihre hervorragende Arbeit!
Wenn Sie an einem vollständigen HLSL-Shader interessiert sind, finden Sie ihn
hier . Ich bemühe mich immer, den Assembler-Code so nah wie möglich am Spiel zu haben und bin vollkommen froh, dass es mir wieder gelungen ist!
Berechnung der durchschnittlichen Helligkeit
Dies ist der zweite Teil der Analyse von Berechnungen mittlerer Helligkeit in „The Witcher 3: Wild Hunt“.
Bevor wir uns mit einem anderen Computational Shader messen, wiederholen wir kurz, was im letzten Teil passiert ist: Wir haben mit einem HDR-Farbpuffer mit einer Skalierung auf 1 / 4x1 / 4 gearbeitet. Nach dem ersten Durchgang erhielten wir ein Helligkeitshistogramm (strukturierter Puffer mit 256 vorzeichenlosen ganzzahligen Werten). Wir haben den Logarithmus für die Helligkeit jedes Pixels berechnet, ihn auf 256 Zellen verteilt und den entsprechenden Wert des strukturierten Puffers um 1 pro Pixel erhöht. Aus diesem Grund entspricht die Gesamtsumme aller Werte in diesen 256 Zellen der Anzahl der Pixel.
Ein Beispiel für die Ausgabe des ersten Durchgangs. Es gibt 256 Elemente.Unser Vollbildpuffer hat beispielsweise eine Größe von 1920 x 1080. Nach dem Verkleinern wurde beim ersten Durchgang ein Puffer von 480 x 270 verwendet. Die Summe aller 256 Werte im Puffer entspricht 480 * 270 = 129 600.
Nach dieser kurzen Einführung sind wir bereit, mit dem nächsten Schritt fortzufahren: dem Rechnen.
Diesmal wird nur eine Thread-Gruppe verwendet (Dispatch (1, 1, 1)).
Schauen wir uns den Assembler-Code des Computational Shader an:
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_uav_structured u0, 4
dcl_uav_typed_texture2d (float,float,float,float) u1
dcl_input vThreadIDInGroup.x
dcl_temps 4
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, vThreadIDInGroup.x, l(0), u0.xxxx
1: store_structured g0.x, vThreadIDInGroup.x, l(0), r0.x
2: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
3: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, r0.x, l(0), u0.xxxx
4: store_structured g0.x, r0.x, l(0), r0.w
5: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.y, l(0), u0.xxxx
6: store_structured g0.x, r0.y, l(0), r0.x
7: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.z, l(0), u0.xxxx
8: store_structured g0.x, r0.z, l(0), r0.x
9: sync_g_t
10: if_z vThreadIDInGroup.x
11: mul r0.x, cb0[0].y, cb0[0].x
12: ftou r0.x, r0.x
13: utof r0.y, r0.x
14: mul r0.yz, r0.yyyy, cb0[0].zzwz
15: ftoi r0.yz, r0.yyzy
16: iadd r0.x, r0.x, l(-1)
17: imax r0.y, r0.y, l(0)
18: imin r0.y, r0.x, r0.y
19: imax r0.z, r0.y, r0.z
20: imin r0.x, r0.x, r0.z
21: mov r1.z, l(-1)
22: mov r2.xyz, l(0, 0, 0, 0)
23: loop
24: breakc_nz r2.x
25: ld_structured r0.z, r2.z, l(0), g0.xxxx
26: iadd r3.x, r0.z, r2.y
27: ilt r0.z, r0.y, r3.x
28: iadd r3.y, r2.z, l(1)
29: mov r1.xy, r2.yzyy
30: mov r3.z, r2.x
31: movc r2.xyz, r0.zzzz, r1.zxyz, r3.zxyz
32: endloop
33: mov r0.w, l(-1)
34: mov r1.yz, r2.yyzy
35: mov r1.xw, l(0, 0, 0, 0)
36: loop
37: breakc_nz r1.x
38: ld_structured r2.x, r1.z, l(0), g0.xxxx
39: iadd r1.y, r1.y, r2.x
40: utof r2.x, r2.x
41: utof r2.w, r1.z
42: add r2.w, r2.w, l(0.500000)
43: mul r2.w, r2.w, l(0.011271)
44: exp r2.w, r2.w
45: add r2.w, r2.w, l(-1.000000)
46: mad r3.z, r2.x, r2.w, r1.w
47: ilt r2.x, r0.x, r1.y
48: iadd r2.w, -r2.y, r1.y
49: itof r2.w, r2.w
50: div r0.z, r3.z, r2.w
51: iadd r3.y, r1.z, l(1)
52: mov r0.y, r1.z
53: mov r3.w, r1.x
54: movc r1.xzw, r2.xxxx, r0.wwyz, r3.wwyz
55: endloop
56: store_uav_typed u1.xyzw, l(0, 0, 0, 0), r1.wwww
57: endif
58: ret
Es gibt einen konstanten Puffer:
Werfen Sie einen kurzen Blick auf den Assembler-Code: Zwei UAVs sind angehängt (u0: Eingabepuffer aus dem ersten Teil und u1: Ausgabetextur im Format 1x1 R32_FLOAT). Wir sehen auch, dass es 64 Threads pro Gruppe und 256 Elemente des gemeinsam genutzten 4-Byte-Gruppenspeichers gibt.
Wir beginnen damit, den gemeinsam genutzten Speicher mit Daten aus dem Eingabepuffer zu füllen. Wir haben 64 Threads, daher müssen Sie fast das Gleiche tun wie zuvor.
Um absolut sicher zu sein, dass alle Daten zur weiteren Verarbeitung geladen wurden, setzen wir danach eine Barriere.
// - .
// 64 , 4
// .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = g_buffer[offset];
}
// , ,
// .
GroupMemoryBarrierWithGroupSync();
Alle Berechnungen werden nur in einem Thread ausgeführt, alle anderen werden einfach zum Laden von Werten aus dem Puffer in den gemeinsam genutzten Speicher verwendet.
Der "Computer" -Stream hat einen Index von 0. Warum? Theoretisch können wir jeden Stream aus dem Intervall [0-63] verwenden, aber dank eines Vergleichs mit 0 können wir zusätzliche Ganzzahl-Ganzzahl-Vergleiche (
dhq- Anweisungen) vermeiden.
Der Algorithmus basiert auf der Angabe des Pixelintervalls, das bei der Operation berücksichtigt wird.
In Zeile 11 multiplizieren wir width * height, um die Gesamtzahl der Pixel zu erhalten, und multiplizieren sie mit zwei Zahlen aus dem Intervall [0.0f-1.0f], wobei der Beginn und das Ende des Intervalls angegeben werden. Weitere Einschränkungen werden verwendet, um sicherzustellen, dass
0 <= Start <= End <= totalPixels - 1
:
// 0.
[branch] if (threadID == 0)
{
//
uint totalPixels = cb0_v0.x * cb0_v0.y;
// (, , ),
// .
int pixelsToConsiderStart = totalPixels * cb0_v0.z;
int pixelsToConsiderEnd = totalPixels * cb0_v0.w;
int pixelsMinusOne = totalPixels - 1;
pixelsToConsiderStart = clamp( pixelsToConsiderStart, 0, pixelsMinusOne );
pixelsToConsiderEnd = clamp( pixelsToConsiderEnd, pixelsToConsiderStart, pixelsMinusOne );
Wie Sie sehen können, gibt es unten zwei Zyklen. Das Problem mit ihnen (oder mit ihrem Assembler-Code) ist, dass es seltsame bedingte Übergänge an den Enden von Schleifen gibt. Es war sehr schwierig für mich, sie nachzubilden. Schauen Sie sich auch Zeile 21 an. Warum gibt es "-1"? Ich werde es unten etwas erklären.
Die Aufgabe des ersten Zyklus besteht darin,
pixelsToConsiderStart zu
löschen und den Index der Pufferzelle
anzugeben, in der
pixelsToConsiderStart +1 vorhanden ist (sowie die Anzahl aller Pixel in den vorherigen Zellen).
Angenommen ,
pixelsToConsiderStart entspricht ungefähr 30.000, und im Puffer befinden sich 37.000 Pixel in der Zelle „Null“ (dies geschieht nachts im Spiel). Daher möchten wir die Analyse der Helligkeit mit ungefähr dem Pixel 30001 beginnen, das in der Zelle "Null" vorhanden ist. In diesem Fall verlassen wir sofort die Schleife und erhalten den Startindex '0' und null verworfene Pixel.
Schauen Sie sich den HLSL-Code an:
//
int numProcessedPixels = 0;
// [0-255]
int lumaValue = 0;
//
bool bExitLoop = false;
// - "pixelsToConsiderStart" .
// lumaValue, .
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];
// , lumaValue
int tempSum = numProcessedPixels + numPixels;
// , pixelsToConsiderStart, .
// , lumaValue.
// , pixelsToConsiderStart - "" , , .
[flatten]
if (tempSum > pixelsToConsiderStart)
{
bExitLoop = true;
}
else
{
numProcessedPixels = tempSum;
lumaValue++;
}
}
Die mysteriöse Zahl "-1" aus Zeile 21 des Assembler-Codes ist mit der Booleschen Bedingung für die Schleifenausführung verbunden (ich habe dies fast zufällig entdeckt).
Nachdem wir die Anzahl der Pixel von
lumaValue- Zellen und
lumaValue selbst erhalten haben, können wir mit dem zweiten Zyklus
fortfahren .
Die Aufgabe des zweiten Zyklus besteht darin, den Einfluss von Pixeln und der durchschnittlichen Helligkeit zu berechnen.
Wir beginnen mit
lumaValue, das in der ersten Schleife berechnet wurde.
float finalAvgLuminance = 0.0f;
//
uint numProcessedPixelStart = numProcessedPixels;
// - .
// , , lumaValue.
// [0-255], , , ,
// pixelsToConsiderEnd.
// .
bExitLoop = false;
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];
//
numProcessedPixels += numPixels;
// , [0-255] (uint)
uint encodedLumaUint = lumaValue;
//
float numberOfPixelsWithCurrentLuma = numPixels;
// , [0-255] (float)
float encodedLumaFloat = encodedLumaUint;
Zu diesem Zeitpunkt erhielten wir den im Intervall [0.0f-255.f] codierten Helligkeitswert.
Der Dekodierungsprozess ist recht einfach - Sie müssen die Berechnung der Kodierungsstufe umkehren.
Eine kurze Wiederholung des Codierungsprozesses:
float luma = dot( hdrPixelColor, float3(0.2126, 0.7152, 0.0722) );
...
float outLuma;
// log(0) undef, log(1) = 0
outLuma = luma + 1.0;
//
outLuma = log( outLuma );
// 128, log(1) * 128 = 0, log(2,71828) * 128 = 128, log(7,38905) * 128 = 256
outLuma = outLuma * 128
// uint
uint outLumaUint = min( (uint) outLuma, 255);
Um die Helligkeit zu dekodieren, kehren wir den Kodierungsprozess beispielsweise wie folgt um:
// 0.5f ( , )
float fDecodedLuma = encodedLumaFloat + 0.5;
// :
// 128
fDecodedLuma /= 128.0;
// exp(x), log(x)
fDecodedLuma = exp(fDecodedLuma);
// 1.0
fDecodedLuma -= 1.0;
Dann berechnen wir die Verteilung, indem wir die Anzahl der Pixel mit einer bestimmten Helligkeit mit der decodierten Helligkeit multiplizieren und sie summieren, bis wir zur Verarbeitung von
pixelsToConsiderEnd- Pixeln gelangen.
Danach teilen wir den Gesamteffekt auf die Anzahl der analysierten Pixel.Hier ist der Rest der Schleife (und der Shader): Der vollständige Shader ist hier verfügbar . Es ist vollständig kompatibel mit meinem HLSLexplorer- Programm , ohne das ich die durchschnittliche Helligkeitsberechnung in The Witcher 3 (und allen anderen Effekten auch!) Nicht effektiv nachbilden könnte . Abschließend noch ein paar Gedanken. Bei der Berechnung der durchschnittlichen Helligkeit war dieser Shader schwer wiederherzustellen. Die Hauptgründe: 1) Seltsame "ausstehende" Überprüfungen der Ausführung des Zyklus, es dauerte viel länger als ich bisher dachte. 2) Probleme beim Debuggen dieses Computer-Shaders in RenderDoc (Version 1.2).//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;
// () .
float tempTotalContribution = fCurrentLumaContribution + finalAvgLuminance;
[flatten]
if (numProcessedPixels > pixelsToConsiderEnd )
{
//
bExitLoop = true;
// , .
//
int diff = numProcessedPixels - numProcessedPixelStart;
//
finalAvgLuminance = tempTotalContribution / float(diff);
}
else
{
// lumaValue
finalAvgLuminance = tempTotalContribution;
lumaValue++;
}
}
//
g_avgLuminance[uint2(0,0)] = finalAvgLuminance;
Die Operationen "ld_structured_indexable" werden nicht vollständig unterstützt, obwohl das Ergebnis des Lesens aus Index 0 den korrekten Wert ergibt, alle anderen geben Nullen zurück, weshalb die Zyklen unbegrenzt fortgesetzt werden.Obwohl ich nicht den gleichen Assembler-Code wie im Original erzielen konnte (Unterschiede siehe Screenshot unten), konnte ich mit RenderDoc diesen Shader in die Pipeline einfügen - und die Ergebnisse waren dieselben!Das Ergebnis der Schlacht. Links ist mein Shader, rechts ist der ursprüngliche Assembler-Code.Teil 8. Der Mond und seine Phasen
Im achten Teil des Artikels untersuche ich den Mond-Shader aus The Witcher 3 (und genauer gesagt aus der Blood and Wine-Erweiterung).Der Mond ist ein wichtiges Element des Nachthimmels, und es kann ziemlich schwierig sein, ihn glaubwürdig zu machen, aber für mich war es ein echtes Vergnügen, nachts in TW3 zu laufen.Schau dir diese Szene an!Bevor wir uns mit dem Pixel-Shader befassen, möchte ich einige Worte zu den Nuancen des Renderns sagen. Aus geometrischer Sicht ist der Mond nur eine Kugel (siehe unten) mit Texturkoordinaten, Normal- und Tangentenvektoren. Der Vertex-Shader berechnet die Position im Weltraum sowie die normalisierten Vektoren von Normalen, tangential und tangential zu zwei Punkten (unter Verwendung eines Vektorprodukts), multipliziert mit der Weltmatrix.Um sicherzustellen , dass der Mond liegt ganz auf den entfernten Ebene, Felder und MinDepth MaxDepth Struktur D3D11_VIEWPORT den Wert 0,0 (den gleichen Trick, der für den Himmel Kuppel verwendet wurde) zugewiesen. Der Mond wird unmittelbar nach dem Himmel gerendert.Die Kugel, mit der der Mondgezeichnet wurde . Nun, ich denke, alles, was Sie tun können. Werfen wir einen Blick auf den Pixel-Shader: Der Hauptgrund, warum ich mich für einen Shader von Blood and Wine entschieden habe, ist einfach - er ist kürzer. Zuerst berechnen wir den Versatz, um die Textur abzutasten. cb0 [0] .w wird als Versatz entlang der X-Achse verwendet. Mit diesem einfachen Trick können wir die Drehung des Mondes um seine Achse simulieren.ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[267], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_input_ps linear v1.w
dcl_input_ps linear v2.xyzw
dcl_input_ps linear v3.xy
dcl_input_ps linear v4.xy
dcl_output o0.xyzw
dcl_temps 3
0: mov r0.x, -cb0[0].w
1: mov r0.y, l(0)
2: add r0.xy, r0.xyxx, v2.xyxx
3: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, t0.xyzw, s0
4: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
5: log r0.w, r0.w
6: mul r0.w, r0.w, l(2.200000)
7: exp r0.w, r0.w
8: add r0.xyz, r0.xyzx, r0.xyzx
9: dp3 r1.x, r0.xyzx, r0.xyzx
10: rsq r1.x, r1.x
11: mul r0.xyz, r0.xyzx, r1.xxxx
12: mul r1.xy, r0.yyyy, v3.xyxx
13: mad r0.xy, v4.xyxx, r0.xxxx, r1.xyxx
14: mad r0.xy, v2.zwzz, r0.zzzz, r0.xyxx
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
17: sincos r1.x, r2.x, r0.z
18: mov r2.y, r1.x
19: dp2_sat r0.x, r0.xyxx, r2.xyxx
20: mul r0.xyz, r0.xxxx, cb12[266].xyzx
21: mul r0.xyz, r0.xyzx, r0.wwww
22: mul r0.xyz, r0.xyzx, cb2[2].xyzx
23: add_sat r0.w, -v1.w, l(1.000000)
24: mul r0.w, r0.w, cb2[2].w
25: mul o0.xyz, r0.wwww, r0.xyzx
26: mov o0.w, l(0)
27: ret

Beispiele für Werte aus dem konstanten Puffer:Eine Textur (1024 x 512) wird als Eingabe angehängt. Die normale Karte ist in den RGB-Kanälen und die Farbe der Mondoberfläche im Alphakanal codiert. Clever!Der Alpha-Kanal einer Textur ist die Farbe der Mondoberfläche.Textur-RGB-Kanäle sind eine normale Karte.Nachdem wir die richtigen Texturkoordinaten erhalten haben, probieren wir die RGBA-Kanäle aus. Wir müssen die normale Karte auspacken und eine Gammakorrektur der Oberflächenfarbe durchführen. Derzeit kann ein HLSL-Shader folgendermaßen geschrieben werden: Der nächste Schritt besteht darin, eine normale Bindung durchzuführen, jedoch nur in XY-Komponenten. (In The Witcher 3 ist die Z-Achse oben und der gesamte Z-Kanal der Textur ist 1,0). Wir können es so machen: Jetzt ist es Zeit für meinen Lieblingsteil dieses Shaders. Schauen Sie sich noch einmal die Zeilen 15-16 an: Was ist das für ein mysteriöses 0.033864? Zunächst scheint es keinen Sinn zu machen, aber wenn wir den umgekehrten Wert berechnen, erhalten wir ungefähr 29,53, was der Dauer des synodischen Monats entsprichtfloat4 MoonPS(in InputStruct IN) : SV_Target0
{
// Texcoords
float2 uvOffsets = float2(-cb0_v0.w, 0.0);
// texcoords
float2 uv = IN.param2.xy + uvOffsets;
//
float4 sampledTexture = texture0.Sample( sampler0, uv);
// - -
float moonColorTex = pow(sampledTexture.a, 2.2 );
// [0,1] [-1,1].
// : sampledTexture.xyz * 2.0 - 1.0
float3 sampledNormal = normalize((sampledTexture.xyz - 0.5) * 2);
//
float3 Tangent = IN.param4.xyz;
float3 Normal = float3(IN.param2.zw, IN.param3.w);
float3 Bitangent = IN.param3.xyz;
// TBN
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
// XY
// TBN float3x2: 3 , 2
float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
in Tagen! Das ist es, was ich auf Details aufmerksam mache!Wir können zuverlässig davon ausgehen, dass cb0 [0] .y die Anzahl der Tage ist, die während des Spiels vergangen sind. Hier wird eine zusätzliche Abweichung verwendet, die als Versatz entlang der x-Achse der Textur verwendet wird.Nachdem wir diesen Koeffizienten erhalten haben, multiplizieren wir ihn mit 2 * Pi.Dann berechnen wir mit sincos einen weiteren 2d-Vektor.Durch Berechnung des Skalarprodukts zwischen dem Normalenvektor und dem "Mond" -Vektor wird eine Mondphase simuliert. Schauen Sie sich die Screenshots mit verschiedenen Mondphasen an:// .
// days/29.53 + bias.
float phase = cb0_v0.y * (1.0 / SYNODIC_MONTH_LENGTH) + cb0_v0.w;
// 2*PI. , 29.53
// sin/cos.
phase *= TWOPI;
// .
float outSin = 0.0;
float outCos = 0.0;
sincos(phase, outSin, outCos);
//
float lunarPhase = saturate( dot(vNormal, float2(outCos, outSin)) );
Der letzte Schritt besteht darin, eine Reihe von Multiplikationsoperationen durchzuführen, um die endgültige Farbe zu berechnen. Sie verstehen wahrscheinlich nicht, warum dieser Shader einen Alpha-Wert von 0,0 an die Ausgabe sendet. Dies liegt daran, dass der Mond mit aktivierter Überblendung gerendert wird:// .
// cb12_v266.xyz , .
// (1.54, 2.82, 4.13)
float3 moonSurfaceGlowColor = cb12_v266.xyz;
float3 moonColor = lunarPhase * moonSurfaceGlowColor;
moonColor = moonColorTex * moonColor;
// cb_v2.xyz - , , , (1.0, 1.0, 1.0)
moonColor *= cb2_v2.xyz;
// , , . - .
// , ,
// .
float paramHorizon = saturate(1.0 - IN.param1.w);
paramHorizon *= cb2_v2.w;
moonColor *= paramHorizon;
//
return float4(moonColor, 0.0);
Mit diesem Ansatz können Sie die Hintergrundfarbe (Himmelfarbe) abrufen, wenn dieser Shader Schwarz zurückgibt.Wenn Sie an einem vollständigen Shader interessiert sind, können Sie ihn hier herunterladen . Es hat große konstante Puffer und sollte bereits für die Injektion in RenderDoc anstelle des ursprünglichen Shaders bereit sein (benennen Sie einfach "MoonPS" in "EditedShaderPS" um).Und das letzte: Ich wollte die Ergebnisse mit Ihnen teilen:Links ist mein Shader, rechts ist der ursprüngliche Shader aus dem Spiel.Der Unterschied ist minimal und hat keinen Einfluss auf die Ergebnisse.Wie Sie sehen können, war dieser Shader ziemlich einfach neu zu erstellen.Teil 9. G-Puffer
In diesem Teil werde ich einige Details des Gbuffers in The Witcher 3 enthüllen.Wir gehen davon aus, dass Sie die Grundlagen der verzögerten Schattierung kennen.Kurze Wiederholung: Die Idee des Verschiebens besteht nicht darin, die gesamte fertige Beleuchtung und Beschattung auf einmal zu berechnen, sondern die Berechnungen in zwei Stufen zu unterteilen.Im ersten (Geometrie-Durchgang) füllen wir den GBuffer mit Oberflächendaten (Position, Normalen, Spiegelfarbe usw. ...) und im zweiten (Beleuchtungs-Durchgang) kombinieren wir alles und berechnen die Beleuchtung.Die verzögerte Schattierung ist ein sehr beliebter Ansatz, da Sie mit einem einzigen Vollbilddurchlauf Techniken wie die verzögerte Schattierung von Kacheln berechnen können , wodurch die Leistung erheblich verbessert wird.Einfach ausgedrückt ist GBuffer eine Reihe von Texturen mit Geometrieeigenschaften. Es ist sehr wichtig, die richtige Struktur dafür zu schaffen. Als Beispiel aus dem wirklichen Leben können Sie die Crysis 3-Rendering-Technologie studieren .Schauen wir uns nach dieser kurzen Einführung einen Beispielrahmen aus The Witcher 3 an: Blood and Wine:Eines der vielen Hotels in ToussentBasic GBuffer besteht aus drei Vollbild-Renderzielen im Format DXGI_FORMAT_R8G8B8A8_UNORM und einem Tiefen- + Schablonenpuffer im Format DXGI_FORMAT_D24_UNORM_S8_UINT.Hier sind ihre Screenshots:Renderziel 0 - RGB-Kanäle, OberflächenfarbeRenderziel 0 - Alpha-Kanal. Ehrlich gesagt habe ich keine Ahnung, was diese Informationen sind.Renderziel 1 - RGB-Kanäle. Hier werden die Normalenvektoren im Intervall [0-1] aufgezeichnet.Renderziel 1 - Alpha-Kanal. Sieht aus wie Reflektivität!Renderziel 2 - RGB-Kanäle. Sieht aus wie Spiegelfarbe!In dieser Szene ist der Alphakanal schwarz (wird aber später verwendet).Puffertiefen. Beachten Sie, dass hier die invertierte Tiefe verwendet wird.Schablonenpuffer zum Markieren eines bestimmtenPixeltyps (z. B. Haut, Vegetation usw.) Dies ist nicht der gesamte GB-Puffer. Der Beleuchtungspass verwendet auch Beleuchtungssonden und andere Puffer, aber ich werde sie in diesem Artikel nicht diskutieren.Bevor ich zum "Hauptteil" des Beitrags übergehe, werde ich allgemeine Bemerkungen machen:Allgemeine Beobachtungen
1) Der einzige zu reinigende Puffer ist der Tiefen- / Schablonenpuffer.Wenn Sie die oben genannten Texturen in einem guten Frame-Analysator analysieren, werden Sie ein wenig überrascht sein, da sie den Aufruf „Löschen“ mit Ausnahme von Tiefe / Schablone nicht verwenden.Das heißt, in Wirklichkeit sieht RenderTarget1 so aus (beachten Sie die „verschwommenen“ Pixel auf der anderen Ebene):Dies ist eine einfache und intelligente Optimierung.Eine wichtige Lektion: Sie müssen Ressourcen für ClearRenderTargetView- Aufrufe ausgeben. Verwenden Sie sie daher nur bei Bedarf.2) Inverted Tiefe - es ist kühlin vielen Artikeln bereits geschrieben über die Genauigkeit des Tiefenpuffers mit Gleitkomma. Hexer 3 verwendet umgekehrt-z. Dies ist die natürliche Wahl für ein solches Open-World-Spiel mit langen Rendering-Entfernungen.Der Wechsel zu DirectX wird nicht schwierig sein:a) Wir löschen den Tiefenpuffer, indem wir "0" und nicht "1" schreiben.Beim traditionellen Ansatz wurde der Fernwert „1“ verwendet, um den Tiefenpuffer zu löschen. Nach dem Tiefenwechsel wurde der neue "entfernte" Wert 0, sodass Sie alles ändern müssen.b) Vertauschen Sie bei der Berechnung der Projektionsmatrix die nahen und fernen Grenzen.c) Ändern Sie die Tiefenprüfung von „weniger“ auf „mehr“.Bei OpenGL muss etwas mehr Arbeit geleistet werden (siehe die oben genannten Artikel), aber es lohnt sich.3) Wir behalten unsere Position in der Welt nicht bei.Ja, alles ist so einfach. Im Durchgang der Beleuchtung schaffen wir aus den Tiefen eine Position in der Welt.Pixel Shader
In diesem Teil wollte ich genau den Pixel-Shader zeigen, der GBuffer Oberflächendaten liefert.Jetzt wissen wir also bereits, wie man Farben, Normalen und Spiegelbilder speichert.Natürlich ist nicht alles so einfach, wie Sie vielleicht denken.Das Problem mit dem Pixel-Shader ist, dass er viele Optionen hat. Sie unterscheiden sich in der Anzahl der auf sie übertragenen Texturen und der Anzahl der aus dem konstanten Puffer verwendeten Parameter (wahrscheinlich aus dem das Material beschreibenden konstanten Puffer).Für die Analyse habe ich mich für dieses schöne Fass entschieden:Unser heldenhaftes Fass!Bitte begrüßen Sie die Texturen:Wir haben also Albedo, eine normale Karte und eine spiegelnde Farbe. Ziemlich normaler Fall.Bevor wir beginnen, ein paar Worte zur Geometrieeingabe:Geometrie wird mit Positions-, Texkoordinaten-, Normal- und Tangentenpuffern übertragen.Der Vertex-Shader gibt mindestens Texcoords, normalisierte Tangenten- / Normal- / Tangentenvektoren, an zwei Punkte aus, die zuvor mit der Weltmatrix multipliziert wurden. Bei komplexeren Materialien (z. B. mit zwei diffusen Karten oder zwei normalen Karten) kann der Vertex-Shader andere Daten ausgeben, aber ich wollte hier ein einfaches Beispiel zeigen.Pixel-Shader im Assembler-Code: Ein Shader besteht aus mehreren Schritten. Ich werde jeden Hauptteil dieses Shaders separat beschreiben.ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[3], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t2
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 3
0: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, v1.xyxx, t1.xyzw, s0
1: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t0.xyzw, s0
2: add r1.w, r1.y, r1.x
3: add r1.w, r1.z, r1.w
4: mul r2.x, r1.w, l(0.333300)
5: add r2.y, l(-1.000000), cb4[1].x
6: mul r2.y, r2.y, l(0.500000)
7: mov_sat r2.z, r2.y
8: mad r1.w, r1.w, l(-0.666600), l(1.000000)
9: mad r1.w, r2.z, r1.w, r2.x
10: mul r2.xzw, r1.xxyz, cb4[0].xxyz
11: mul_sat r2.xzw, r2.xxzw, l(1.500000, 0.000000, 1.500000, 1.500000)
12: mul_sat r1.w, abs(r2.y), r1.w
13: add r2.xyz, -r1.xyzx, r2.xzwx
14: mad r1.xyz, r1.wwww, r2.xyzx, r1.xyzx
15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
21: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r1.xyz, v3.xyzx, r0.xxxx, r1.xyzx
27: mad r0.xyz, v2.xyzx, r0.zzzz, r1.xyzx
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
46: dp3 r0.w, r0.xyzx, r0.xyzx
47: rsq r0.w, r0.w
48: mul r0.xyz, r0.wwww, r0.xyzx
49: max r0.w, abs(r0.y), abs(r0.x)
50: max r0.w, r0.w, abs(r0.z)
51: lt r1.xy, abs(r0.zyzz), r0.wwww
52: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
53: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
54: lt r1.z, r1.y, r1.x
55: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
56: div r1.z, r1.y, r1.x
57: div r0.xyz, r0.xyzx, r0.wwww
58: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
59: mul r0.xyz, r0.wwww, r0.xyzx
60: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
61: mov o0.w, cb4[2].x
62: mov o2.w, l(0)
63: ret
Aber zuerst wie gewohnt - ein Screenshot mit den Werten aus dem konstanten Puffer:Albedo
Wir werden mit komplexen Dingen beginnen. Es ist nicht nur "OutputColor.rgb = Texture.Sample (uv) .rgb".Nach dem Abtasten der RGB- Farbtextur (Zeile 1) werden die nächsten 14 Zeilen als "Sättigungsreduzierungspuffer" bezeichnet. Lassen Sie mich Ihnen den HLSL-Code zeigen: Bei den meisten Objekten gibt dieser Code nur die ursprüngliche Farbe aus der Textur zurück. Dies wird durch die entsprechenden Werte des „Materialpuffers“ erreicht. cb4_v1.x hat den Wert 1.0, der eine Maske von 0.0 und die Eingabefarbe aus der Lerp- Anweisung zurückgibt . Es gibt jedoch einige Ausnahmen. Der größte Entsättigungsfaktor, den ich gefunden habe, ist 4.0 (er ist nie kleiner als 1.0) und desaturatedColorfloat3 albedoColorFilter( in float3 color, in float desaturationFactor, in float3 desaturationValue )
{
float sumColorComponents = color.r + color.g + color.b;
float averageColorComponentValue = 0.3333 * sumColorComponents;
float oneMinusAverageColorComponentValue = 1.0 - averageColorComponentValue;
float factor = 0.5 * (desaturationFactor - 1.0);
float avgColorComponent = lerp(averageColorComponentValue, oneMinusAverageColorComponentValue, saturate(factor));
float3 desaturatedColor = saturate(color * desaturationValue * 1.5);
float mask = saturate( avgColorComponent * abs(factor) );
float3 finalColor = lerp( color, desaturatedColor, mask );
return finalColor;
}
Kommt auf das Material an. Es kann so etwas wie (0,2, 0,3, 0,4) sein; Es gibt keine strengen Regeln. Natürlich, ich konnte nicht helfen , aber weiß , das in seinem eigenen DX11-Rahmen, und hier sind die Ergebnisse, wo alle der Werte desaturatedColor gleich float3 (0,25, 0,3, 0,45)Entsättigungsfaktor = 1,0 (hat keine Wirkung)Entsättigungsfaktor = 2,0Entsättigungsfaktor = 3,0desaturationFactor = 4.0Ich bin sicher, dass dies nur eine Anwendung von Materialparametern ist, aber nicht am Ende des Albedo-Teils durchgeführt wird.Die Zeilen 15 bis 20 fügen den letzten Schliff hinzu: v0.z ist die Ausgabe des Vertex-Shaders und sie sind Null. Vergessen Sie es nicht, denn v0.z wird später einige Male verwendet. Es scheint, als wäre es eine Art Koeffizient, und der gesamte Code sieht aus wie eine kleine Dimmalbedo, aber da v0.z 0 ist, bleibt die Farbe unverändert. HLSL: In Bezug auf RT0.a wird es, wie wir sehen können, aus dem Materialkonstantenpuffer entnommen, aber da der Shader keine Debugging-Informationen hat, ist es schwer zu sagen, was es ist. Vielleicht Transluzenz? Wir sind mit dem ersten Renderziel fertig!15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
/* ALBEDO */
// (?)
float3 albedoColor = albedoColorFilter( colorTex, cb4_v1.x, cb4_v0.rgb );
float albedoMaxComponent = getMaxComponent( albedoColor );
// ,
// "paramZ" 0
float paramZ = Input.out0.z; // , 0
// , 0.70 0.85
// lerp, .
float param = (albedoMaxComponent > 0.22) ? 0.70 : 0.85;
float mulParam = lerp(1, param, paramZ);
//
pout.RT0.rgb = albedoColor * mulParam;
pout.RT0.a = cb4_v2.x;
Normal
Beginnen wir mit dem Auspacken der normalen Karte und binden dann wie üblich die Normalen: Bisher nichts Überraschendes. Schauen Sie sich die Zeilen 28-33 an: Wir können sie grob wie folgt schreiben: Nicht sicher, ob das Schreiben korrekt ist. Wenn Sie wissen, was diese mathematische Operation ist, lassen Sie es mich wissen. Wir sehen, dass der Pixel-Shader SV_IsFrontFace verwendet./* */
float3 sampledNormal = ((normalTex.xyz - 0.5) * 2);
// TBN
float3 Tangent = Input.TangentW.xyz;
float3 Normal = Input.NormalW.xyz;
float3 Bitangent;
Bitangent.x = Input.out0.w;
Bitangent.yz = Input.out1.zw;
// ; , , normal-tbn
// 'mad' 'mov'
Bitangent = saturate(Bitangent);
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
float3 normal = mul( sampledNormal, TBN );
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
[branch] if (bIsFrontFace <= 0)
{
float cosTheta = dot(Input.NormalW, normal);
float3 invNormal = cosTheta * Input.NormalW;
normal = normal - 2*invNormal;
}
Was ist das?
Die Dokumentation hilft mir (ich wollte "msdn" schreiben, aber ...):Legt fest, ob das Dreieck auf die Kamera schaut. Für Linien und Punkte gilt IsFrontFace. Eine Ausnahme bilden die aus Dreiecken gezogenen Linien (Drahtgittermodus), die IsFrontFace ähnlich wie das Rasteren eines Dreiecks im durchgezogenen Modus festlegen. Das Schreiben darauf kann von einem Geometrie-Shader und das Lesen von einem Pixel-Shader erfolgen.
Ich wollte es selbst ausprobieren. Tatsächlich macht sich der Effekt nur im Drahtgittermodus bemerkbar. Ich glaube, dieser Code wird für die korrekte Berechnung von Normalen (und damit der Beleuchtung) im Drahtgittermodus benötigt.Hier ein Vergleich: Sowohl die Farben des Rahmens der fertigen Szene mit diesem Trick ein / aus als auch die Textur der gbuffer [0-1] -Normalen mit dem Trick ein / aus:Szenenfarbe ohne SpielereiFarbszene mit StuntNormal [0-1] kein TrickNormal [0-1] mit einem TrickHaben Sie bemerkt, dass jedes Renderziel in GBuffer das Format R8G8B8A8_UNORM hat? Dies bedeutet, dass es 256 mögliche Werte pro Komponente gibt. Reicht das aus, um Normalen zu speichern?Das Speichern hochwertiger Normalen mit genügend Bytes in Gbuffer ist ein bekanntes Problem, aber zum Glück gibt es viele verschiedene Materialien , aus denen man lernen kann .Vielleicht wissen einige von Ihnen bereits, welche Technik hier verwendet wird. Ich muss sagen, dass im gesamten Durchgang der Geometrie eine zusätzliche Textur an Steckplatz 13 angebracht ist ...:Ha!
Der Witcher 3 verwendet eine Technik namens " Best Fit Normals ". Hier werde ich es nicht im Detail erklären (siehe Präsentation). Es wurde zwischen 2009 und 2010 von Crytek erfunden. Da CryEngine Open Source hat, ist BFN auch Open Source .BFN verleiht der Textur der Normalen ein "körniges" Aussehen.Nachdem wir die Normalen mit BFN skaliert haben, codieren wir sie vom Intervall [-1; 1] bis [0, 1] neu.Spiegel
Beginnen wir mit Zeile 34 und probieren Sie die Spiegelstruktur aus: Wie Sie sehen, gibt es einen Dimmfilter, den wir von Albedo kennen: Wir berechnen die Komponente mit max. Wert, und berechnen Sie dann die „abgedunkelte“ Farbe und interpolieren Sie sie mit der ursprünglichen Spiegelfarbe, wobei Sie den Parameter aus dem Vertex-Shader nehmen ... der 0 ist. Bei der Ausgabe erhalten wir also die Farbe aus der Textur. HLSL:34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
/* SPECULAR */
float3 specularTex = texture2.Sample( samplerAnisoWrap, Texcoords ).rgb;
// , Albedo. . ,
// - "".
// paramZ 0,
// .
float specularMaxComponent = getMaxComponent( specularTex );
float3 specB = (specularMaxComponent > 0.2) ? specularTex : float3(0.12, 0.12, 0.12);
float3 finalSpec = lerp(specularTex, specB, paramZ);
pout.RT2.xyz = finalSpec;
Reflexionsvermögen
Ich habe keine Ahnung, ob dieser Name für diesen Parameter geeignet ist, da ich nicht weiß, wie er den Durchgang der Beleuchtung beeinflusst. Tatsache ist, dass der Alpha-Kanal der normalen Eingabekarte zusätzliche Daten enthält:Alpha-Kanal-Textur "normale Karte".Assembler-Code: Begrüßen Sie unseren alten Freund - v0.z! Seine Bedeutung ist ähnlich wie Albedo und Spiegel:41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
/* REFLECTIVITY */
float reflectivity = normalTex.a;
float reflectivity2 = (reflectivity < 0.33) ? (reflectivity * 0.95) : 0.33;
float finalReflectivity = lerp(reflectivity, reflectivity2, paramZ);
pout.RT1.a = finalReflectivity;
Großartig!
Dies ist das Ende der Analyse der ersten Version des Pixel-Shaders.Hier ist ein Vergleich meines Shaders (links) mit dem Original (rechts):Diese Unterschiede wirken sich nicht auf die Berechnungen aus, sodass meine Arbeit hier abgeschlossen ist.Pixel Shader: Albedo + Normal Option
Ich beschloss, eine weitere Option anzuzeigen, jetzt nur mit Albedo und normalen Karten, ohne spiegelnde Textur. Der Assembler-Code ist etwas länger: Der Unterschied zwischen dieser und den vorherigen Optionen ist wie folgt: a) Zeilen 1, 19 : Der Interpolationsparameter v0.z wird mit cb4 [0] .x aus dem konstanten Puffer multipliziert, aber dieses Produkt wird nur für die Interpolationsalbedo in Zeile 19 verwendet. Für andere Ausgaben wird der "normale" Wert von v0.z verwendet. b) Zeilen 54-55 : o2.w wird nun unter der Bedingung gesetzt, dass (cb4 [7] .x> 0.0) dieses Muster „eine Art Vergleich - UND“ bereits aus der Berechnung des Helligkeitshistogramms erkennt. Es kann so geschrieben werden: c) Zeilen 34-42 : eine völlig andere Spiegelberechnung.ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 4
0: mul r0.x, v0.z, cb4[0].x
1: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, v1.xyxx, t1.xyzw, s0
2: sample_indexable(texture2d)(float,float,float,float) r0.yzw, v1.xyxx, t0.wxyz, s0
3: add r2.x, r0.z, r0.y
4: add r2.x, r0.w, r2.x
5: add r2.z, l(-1.000000), cb4[2].x
6: mul r2.yz, r2.xxzx, l(0.000000, 0.333300, 0.500000, 0.000000)
7: mov_sat r2.w, r2.z
8: mad r2.x, r2.x, l(-0.666600), l(1.000000)
9: mad r2.x, r2.w, r2.x, r2.y
10: mul r3.xyz, r0.yzwy, cb4[1].xyzx
11: mul_sat r3.xyz, r3.xyzx, l(1.500000, 1.500000, 1.500000, 0.000000)
12: mul_sat r2.x, abs(r2.z), r2.x
13: add r2.yzw, -r0.yyzw, r3.xxyz
14: mad r0.yzw, r2.xxxx, r2.yyzw, r0.yyzw
15: max r2.x, r0.w, r0.z
16: max r2.x, r0.y, r2.x
17: lt r2.x, l(0.220000), r2.x
18: movc r2.x, r2.x, l(-0.300000), l(-0.150000)
19: mad r0.x, r0.x, r2.x, l(1.000000)
20: mul o0.xyz, r0.xxxx, r0.yzwy
21: add r0.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r0.xyw, v3.xyxz, r0.xxxx, r1.xyxz
27: mad r0.xyz, v2.xyzx, r0.zzzz, r0.xywx
28: uge r0.w, l(0), v4.x
29: if_nz r0.w
30: dp3 r0.w, v2.xyzx, r0.xyzx
31: mul r1.xyz, r0.wwww, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
43: max r0.w, r1.z, r1.y
44: max r0.w, r0.w, r1.x
45: lt r0.w, l(0.200000), r0.w
46: movc r2.xyz, r0.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
47: add r2.xyz, -r1.xyzx, r2.xyzx
48: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
49: lt r0.w, r1.w, l(0.330000)
50: mul r1.x, r1.w, l(0.950000)
51: movc r0.w, r0.w, r1.x, l(0.330000)
52: add r0.w, -r1.w, r0.w
53: mad o1.w, v0.z, r0.w, r1.w
54: lt r0.w, l(0), cb4[7].x
55: and o2.w, r0.w, l(0.064706)
56: dp3 r0.w, r0.xyzx, r0.xyzx
57: rsq r0.w, r0.w
58: mul r0.xyz, r0.wwww, r0.xyzx
59: max r0.w, abs(r0.y), abs(r0.x)
60: max r0.w, r0.w, abs(r0.z)
61: lt r1.xy, abs(r0.zyzz), r0.wwww
62: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
63: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
64: lt r1.z, r1.y, r1.x
65: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
66: div r1.z, r1.y, r1.x
67: div r0.xyz, r0.xyzx, r0.wwww
68: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
69: mul r0.xyz, r0.wwww, r0.xyzx
70: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
71: mov o0.w, cb4[6].x
72: ret
pout.RT2.w = (cb4_v7.x > 0.0) ? (16.5/255.0) : 0.0;
Hier gibt es keine spiegelnde Textur. Schauen wir uns den Assembler-Code an, der für diesen Teil verantwortlich ist: Beachten Sie, dass wir ihn hier verwendet haben (1 - reflektierte Fähigkeit). Glücklicherweise ist das Schreiben in HLSL recht einfach: Ich möchte hinzufügen, dass in dieser Version der konstante Puffer mit Materialdaten etwas größer ist. Hier werden diese zusätzlichen Werte verwendet, um die Spiegelfarbe zu emulieren. Der Rest des Shaders ist der gleiche wie in der vorherigen Version. 72 Zeilen Assembler-Code sind zu viel, um in WinMerge angezeigt zu werden. Nehmen Sie also mein Wort: Mein Code war fast der gleiche wie im Original. Oder Sie können meinen HLSLexplorer herunterladen und sich selbst davon überzeugen !34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
float oneMinusReflectivity = 1.0 - normalTex.a;
float3 specularTex = pow(cb4_v3.rgb, 2.2);
oneMinusReflectivity = oneMinusReflectivity * cb4_v4.x + cb4_v5.x;
specularTex = saturate(specularTex * oneMinusReflectivity);
specularTex = pow(specularTex, 1.0/2.2);
// ...
float specularMaxComponent = getMaxComponent( specularTex );
...
Zusammenfassend
... und wenn Sie es hier lesen, dann möchten Sie wahrscheinlich etwas tiefer gehen.Was im wirklichen Leben einfach erscheint, ist oft nicht der Fall, und die Datenübertragung zu gbuffer The Witcher 3 war keine Ausnahme. Ich habe Ihnen nur die einfachsten Versionen der dafür verantwortlichen Pixel-Shader gezeigt und auch allgemeine Beobachtungen gemacht, die sich auf die verzögerte Schattierung im Allgemeinen beziehen.Für die meisten Patienten gibt es zwei Optionen für Pixel-Shader im Pastebin:Option 1 - mit spiegelnder TexturOption 2 - ohne spiegelnde TexturTeil 10. Regenvorhänge in der Ferne
In diesem Teil werden wir einen wunderbaren atmosphärischen Effekt betrachten, den ich wirklich mag - entfernte Regen- / Lichtvorhänge in der Nähe des Horizonts. Im Spiel sind sie auf den Skellig-Inseln am einfachsten zu treffen.Ich persönlich mag dieses atmosphärische Phänomen sehr und war gespannt, wie die Grafikprogrammierer von CD Projekt Red es implementiert haben. Lass es uns herausfinden!Hier sind zwei Screenshots vor und nach dem Anbringen der Regenvorhänge:Zu den RegenvorhängenNach den Vorhängen des RegensGeometrie
Zunächst konzentrieren wir uns auf die Geometrie. Die Idee ist, einen kleinen Zylinder zu verwenden:Ein Zylinder im lokalen RaumAus Sicht seiner Position im lokalen Raum ist er ziemlich klein - seine Position liegt im Bereich (0,0 - 1,0).Die Eingangsschaltung für diesen Draw-Aufruf sieht folgendermaßen aus ...Folgendes ist für uns hier wichtig: Texcoords und Instance_Transform.Texcoords werden ganz einfach gewickelt: U der oberen und unteren Basis liegen im Intervall [0.02777 - 1.02734]. V an der unteren Basis ist 1,0 und an der oberen - 0,0. Wie Sie sehen, können Sie dieses Netz ganz einfach auch prozedural erstellen.Nachdem wir diesen kleinen Zylinder im lokalen Raum erhalten haben, multiplizieren wir ihn mit der Weltmatrix, die für jede Instanz des Eingabeelements INSTANCE_TRANSFORM bereitgestellt wird. Lassen Sie uns die Werte dieser Matrix überprüfen:Sieht ziemlich beängstigend aus, oder? Aber keine Sorge, wir werden diese Matrix analysieren und sehen, was sie verbirgt! Die Ergebnisse sind sehr interessant: Es ist wichtig, die Kameraposition in diesem bestimmten Bild zu kennen: (-116.5338, 234.8695, 2.09) Wie Sie sehen können, haben wir den Zylinder so skaliert, dass er im Weltraum ziemlich groß ist (in TW3 ist die Z-Achse nach oben gerichtet) und relativ zur Kameraposition verschoben und drehte sich um. So sieht der Zylinder nach der Konvertierung mit dem Vertex-Shader aus:XMMATRIX mat( -227.7472, 159.8043, 374.0736, -116.4951,
-194.7577, -173.3836, -494.4982, 238.6908,
-14.16466, -185.4743, 784.564, -1.45565,
0.0, 0.0, 0.0, 1.0 );
mat = XMMatrixTranspose( mat );
XMVECTOR vScale;
XMVECTOR vRotateQuat;
XMVECTOR vTranslation;
XMMatrixDecompose( &vScale, &vRotateQuat, &vTranslation, mat );
// ...
XMMATRIX matRotate = XMMatrixRotationQuaternion( vRotateQuat );
vRotateQuat: (0.0924987569, -0.314900011, 0.883411944, -0.334462732)
vScale: (299.999969, 300.000000, 1000.00012)
vTranslation: (-116.495102, 238.690796, -1.45564997)
Zylinder nach Umbau durch Vertex Shader. Sehen Sie, wie es sich relativ zur Sichtbarkeitspyramide befindet.Vertex-Shader
Eingabegeometrie und Vertex-Shader sind streng voneinander abhängig.Schauen wir uns den Assembler-Code für den Vertex-Shader genauer an: Neben der einfachen Übergabe von Texcoords (Zeile 0) und Instance_LOD_Params (Zeile 8) werden zwei weitere Elemente für die Ausgabe benötigt: SV_Position (dies ist offensichtlich) und Height (Komponente .z) der Position in der Welt. Denken Sie daran, dass der lokale Raum im Bereich [0-1] liegt? Kurz vor dem Anwenden der Weltmatrix verwendet der Vertex-Shader Skalierung und Abweichung, um die lokale Position zu ändern. Kluger Schachzug! In diesem Fall ist scale = float3 (4, 4, 2) und Bias = float3 (-2, -2, -1). < Das Muster, das zwischen den Zeilen 9 und 28 erkennbar ist, ist die Multiplikation zweier Zeilenhauptmatrizen. Schauen wir uns nur den fertigen Vertex-Shader in HLSL an:vs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb1[7], immediateIndexed
dcl_constantbuffer cb2[6], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xy
dcl_input v4.xyzw
dcl_input v5.xyzw
dcl_input v6.xyzw
dcl_input v7.xyzw
dcl_output o0.xyz
dcl_output o1.xyzw
dcl_output_siv o2.xyzw, position
dcl_temps 2
0: mov o0.xy, v1.xyxx
1: mul r0.xyzw, v5.xyzw, cb1[6].yyyy
2: mad r0.xyzw, v4.xyzw, cb1[6].xxxx, r0.xyzw
3: mad r0.xyzw, v6.xyzw, cb1[6].zzzz, r0.xyzw
4: mad r0.xyzw, cb1[6].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
5: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx
6: mov r1.w, l(1.000000)
7: dp4 o0.z, r1.xyzw, r0.xyzw
8: mov o1.xyzw, v7.xyzw
9: mul r0.xyzw, v5.xyzw, cb1[0].yyyy
10: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw
11: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw
12: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
13: dp4 o2.x, r1.xyzw, r0.xyzw
14: mul r0.xyzw, v5.xyzw, cb1[1].yyyy
15: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw
16: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw
17: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
18: dp4 o2.y, r1.xyzw, r0.xyzw
19: mul r0.xyzw, v5.xyzw, cb1[2].yyyy
20: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw
21: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw
22: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
23: dp4 o2.z, r1.xyzw, r0.xyzw
24: mul r0.xyzw, v5.xyzw, cb1[3].yyyy
25: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw
26: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw
27: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
28: dp4 o2.w, r1.xyzw, r0.xyzw
29: ret
cbuffer cbPerFrame : register (b1)
{
row_major float4x4 g_viewProjMatrix;
row_major float4x4 g_rainShaftsViewProjMatrix;
}
cbuffer cbPerObject : register (b2)
{
float4x4 g_mtxWorld;
float4 g_modelScale;
float4 g_modelBias;
}
struct VS_INPUT
{
float3 PositionW : POSITION;
float2 Texcoord : TEXCOORD;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float4 InstanceTransform0 : INSTANCE_TRANSFORM0;
float4 InstanceTransform1 : INSTANCE_TRANSFORM1;
float4 InstanceTransform2 : INSTANCE_TRANSFORM2;
float4 InstanceLODParams : INSTANCE_LOD_PARAMS;
};
struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;
float4 LODParams : LODParams;
float4 PositionH : SV_Position;
};
VS_OUTPUT RainShaftsVS( VS_INPUT Input )
{
VS_OUTPUT Output = (VS_OUTPUT)0;
//
Output.TexcoordAndZ.xy = Input.Texcoord;
Output.LODParams = Input.InstanceLODParams;
//
float3 meshScale = g_modelScale.xyz; // float3( 4, 4, 2 );
float3 meshBias = g_modelBias.xyz; // float3( -2, -2, -1 );
float3 PositionL = Input.PositionW * meshScale + meshBias;
// instanceWorld float4s:
float4x4 matInstanceWorld = float4x4(Input.InstanceTransform0, Input.InstanceTransform1,
Input.InstanceTransform2 , float4(0, 0, 0, 1) );
// (.z)
float4x4 matWorldInstanceLod = mul( g_rainShaftsViewProjMatrix, matInstanceWorld );
Output.TexcoordAndZ.z = mul( float4(PositionL, 1.0), transpose(matWorldInstanceLod) ).z;
// SV_Posiiton
float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );
Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );
return Output;
}
Vergleich meines Shaders (links) und des Originals (rechts):Unterschiede wirken sich nicht auf Berechnungen aus. Ich habe meinen Shader in den Rahmen gespritzt und alles war noch in Ordnung!Pixel Shader
Endlich!
Zu Beginn zeige ich Ihnen die Eingabe: Hier werdenzwei Texturen verwendet: die Rauschtextur und der Tiefenpuffer:Werte aus konstanten Puffern:Und der Pixel-Shader-Assembler-Code: Wow! Ziemlich viel, aber in der Tat ist nicht alles so schlecht. Was ist hier los? Zunächst berechnen wir animierte UVs anhand der verstrichenen Zeit aus cbuffer (cb0 [0] .x) und scale / offset. Diese Texcoords werden zum Abtasten aus der Rauschtextur verwendet (Zeile 2). Nachdem wir den Rauschwert von der Textur erhalten haben, interpolieren wir zwischen den Min / Max-Werten (normalerweise 0 und 1). Dann multiplizieren wir zum Beispiel mit der Koordinate der Textur V (denken Sie daran, dass die Koordinate V von 1 nach 0 geht?) - Zeile 5. So haben wir die „Helligkeitsmaske“ berechnet - es sieht so aus:ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[8], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s15, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t15
dcl_input_ps linear v0.xyz
dcl_input_ps linear v1.w
dcl_input_ps_siv v2.xy, position
dcl_output o0.xyzw
dcl_temps 1
0: mul r0.xy, cb0[0].xxxx, cb4[5].xyxx
1: mad r0.xy, v0.xyxx, cb4[4].xyxx, r0.xyxx
2: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t0.xyzw, s0
3: add r0.y, -cb4[2].x, cb4[3].x
4: mad_sat r0.x, r0.x, r0.y, cb4[2].x
5: mul r0.x, r0.x, v0.y
6: mul r0.x, r0.x, v1.w
7: mul r0.x, r0.x, cb4[1].x
8: mul r0.yz, v2.xxyx, cb0[1].zzwz
9: sample_l(texture2d)(float,float,float,float) r0.y, r0.yzyy, t15.yxzw, s15, l(0)
10: mad r0.y, r0.y, cb12[22].x, cb12[22].y
11: mad r0.y, r0.y, cb12[21].x, cb12[21].y
12: max r0.y, r0.y, l(0.000100)
13: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
14: add r0.y, r0.y, -v0.z
15: mul_sat r0.y, r0.y, cb4[6].x
16: mul_sat r0.x, r0.y, r0.x
17: mad r0.y, cb0[7].y, r0.x, -r0.x
18: mad r0.x, cb4[7].x, r0.y, r0.x
19: mul r0.xyz, r0.xxxx, cb4[0].xyzx
20: log r0.xyz, r0.xyzx
21: mul r0.xyz, r0.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
22: exp r0.xyz, r0.xyzx
23: mul r0.xyz, r0.xyzx, cb2[2].xyzx
24: mul o0.xyz, r0.xyzx, cb2[2].wwww
25: mov o0.w, l(0)
26: ret

Beachten Sie, dass entfernte Objekte (ein Leuchtturm, Berge ...) verschwunden sind. Dies geschah, weil der Zylinder den Tiefentest besteht - der Zylinder befindet sich nicht in der Fernebene und wird auf diese Objekte gezeichnet:TiefentestWir möchten simulieren, dass der Regenvorhang weiter entfernt ist (aber nicht unbedingt auf der anderen Ebene). Dazu berechnen wir eine andere Maske, die "Maske entfernter Objekte".Es wird nach folgender Formel berechnet:farObjectsMask = saturate( (FrustumDepth - CylinderWorldSpaceHeight) * 0.001 );
(0,001 wird aus dem Puffer entnommen), wodurch wir die gewünschte Maske erhalten:(In dem Teil über den Schärfeeffekt habe ich bereits oberflächlich erklärt, wie die Tiefe der Sichtbarkeitspyramide aus dem Tiefenpuffer extrahiert wird.)Persönlich scheint es mir, dass dieser Effekt kostengünstiger realisiert werden könnte, ohne die Höhe im Weltraum zu berechnen, indem beispielsweise die Tiefe der Sichtbarkeitspyramide mit einer kleineren Zahl multipliziert wird 0,0004.Wenn beide Masken multipliziert werden, erhält man die letzte:Nachdem wir diese endgültige Maske erhalten haben (Zeile 16), führen wir eine weitere Interpolation durch, die (zumindest im getesteten Fall) fast nichts bewirkt, multiplizieren dann die endgültige Maske mit der Farbe der Vorhänge (Zeile 19) und führen eine Gammakorrektur durch (Zeilen 20) -22) und endgültige Multiplikationen (23-24).Am Ende geben wir eine Farbe mit einem Alpha-Wert von Null zurück. Dies liegt daran, dass das Mischen in diesem Durchgang aktiviert ist:FinalColor = SourceColor * 1.0 + (1.0 - SourceAlpha) * DestColor
Wenn Sie nichtgenau verstehen, wie das Mischen funktioniert, finden Sie hier eine kurze Erklärung: SourceColor ist die RGB-Ausgabe des Pixel-Shaders und DestColor ist die aktuelle RGB-Farbe des Pixels im Renderziel . Da SourceAlpha immer auf 0,0 gleich, reduziert sich die obige Gleichung zu: FinalColor = SourceColor + DestColor
.Einfach ausgedrückt, hier führen wir ein additives Mischen durch. Wenn der Pixel-Shader zurückkehrt (0, 0, 0), bleibt die Farbe gleich.Hier ist der fertige HLSL-Code - ich denke, dass es nach der Erklärung viel einfacher zu verstehen sein wird: Ich kann glücklich sagen, dass mein Pixel-Shader den gleichen Assembler-Code wie im Original erstellt. Ich hoffe dir hat der Artikel gefallen. Danke fürs Lesen!struct VS_OUTPUT
{
float3 TexcoordAndWorldspaceHeight : Texcoord0;
float4 LODParams : LODParams; // float4(1,1,1,1)
float4 PositionH : SV_Position;
};
float getFrustumDepth( in float depth )
{
// from [1-0] to [0-1]
float d = depth * cb12_v22.x + cb12_v22.y;
// special coefficents
d = d * cb12_v21.x + cb12_v21.y;
// return frustum depth
return 1.0 / max(d, 1e-4);
}
float4 EditedShaderPS( in VS_OUTPUT Input ) : SV_Target0
{
// * Input from Vertex Shader
float2 InputUV = Input.TexcoordAndWorldspaceHeight.xy;
float WorldHeight = Input.TexcoordAndWorldspaceHeight.z;
float LODParam = Input.LODParams.w;
// * Inputs
float elapsedTime = cb0_v0.x;
float2 uvAnimation = cb4_v5.xy;
float2 uvScale = cb4_v4.xy;
float minValue = cb4_v2.x; // 0.0
float maxValue = cb4_v3.x; // 1.0
float3 shaftsColor = cb4_v0.rgb; // RGB( 147, 162, 173 )
float3 finalColorFilter = cb2_v2.rgb; // float3( 1.175, 1.296, 1.342 );
float finalEffectIntensity = cb2_v2.w;
float2 invViewportSize = cb0_v1.zw;
float depthScale = cb4_v6.x; // 0.001
// sample noise
float2 uvOffsets = elapsedTime * uvAnimation;
float2 uv = InputUV * uvScale + uvOffsets;
float disturb = texture0.Sample( sampler0, uv ).x;
// * Intensity mask
float intensity = saturate( lerp(minValue, maxValue, disturb) );
intensity *= InputUV.y; // transition from (0, 1)
intensity *= LODParam; // usually 1.0
intensity *= cb4_v1.x; // 1.0
// Sample depth
float2 ScreenUV = Input.PositionH.xy * invViewportSize;
float hardwareDepth = texture15.SampleLevel( sampler15, ScreenUV, 0 ).x;
float frustumDepth = getFrustumDepth( hardwareDepth );
// * Calculate mask covering distant objects behind cylinder.
// Seems that the input really is world-space height (.z component, see vertex shader)
float depth = frustumDepth - WorldHeight;
float distantObjectsMask = saturate( depth * depthScale );
// * calculate final mask
float finalEffectMask = saturate( intensity * distantObjectsMask );
// cb0_v7.y and cb4_v7.x are set to 1.0 so I didn't bother with naming them :)
float paramX = finalEffectMask;
float paramY = cb0_v7.y * finalEffectMask;
float effectAmount = lerp(paramX, paramY, cb4_v7.x);
// color of shafts comes from contant buffer
float3 effectColor = effectAmount * shaftsColor;
// gamma correction
effectColor = pow(effectColor, 2.2);
// final multiplications
effectColor *= finalColorFilter;
effectColor *= finalEffectIntensity;
// return with zero alpha 'cause the blending used here is:
// SourceColor * 1.0 + (1.0 - SrcAlpha) * DestColor
return float4( effectColor, 0.0 );
}