Reverse Engineering Rendering "The Witcher 3"

Ursprünglicher Autor: Mateusz Nagórka
  • Übersetzung
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 Abschnitt betrachten wir einen weiteren Nachbearbeitungseffekt von The Witcher 3 - Sharpen.

Durch das Schärfen wird das Ausgabebild etwas klarer. Dieser Effekt ist uns aus Photoshop und anderen Grafikeditoren bekannt.

In The Witcher 3 hat das Schärfen zwei Optionen: niedrig und hoch. Ich werde den Unterschied zwischen den beiden weiter unten erläutern, aber jetzt sehen wir uns die Screenshots an:

Bild

Option "Niedrig" - bis

Bild

Option "Niedrig" - danach


Option "Hoch" - zu


Die Option "Hoch" - nachher

Wenn Sie detailliertere (interaktive) Vergleiche anzeigen möchten, lesen Sie den Abschnitt im Handbuch zu Nvidia The Witcher 3 . 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 die Erleichterung (die große Entfernung des Zeichnens) und die Kuppel des Himmels sehen.


Aus Sicht der Eingangsdaten erfordert das Schärfen einen Farbpuffer t0 (LDR nach Tonkorrektur und Linsenfackeln) und einen Tiefenpuffer t1 . Sehen

wir uns den Assembler-Code des Pixel-Shader an: 50 Zeilen Assembler-Code sehen aus wie eine machbare Aufgabe. Gehen wir zu seiner Lösung über.

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




Wertschöpfung schärfen


Der erste Schritt ist das Laden ( Laden ) des Tiefenpuffers (Zeile 1). Es ist erwähnenswert, dass in The Witcher 3 eine umgekehrte Tiefe verwendet wird (1,0 - nah, 0,0 - weit). Wie Sie vielleicht wissen, ist die Hardwaretiefe auf nichtlineare Weise gebunden (weitere Informationen finden Sie in diesem Artikel ).

Die Zeilen 3 bis 6 bieten eine sehr interessante Möglichkeit, diese Hardwaretiefe [1,0 - 0,0] an die Werte [nah-fern] zu binden (wir haben sie in der MatrixPerspectiveFov-Stufe festgelegt). Beachten Sie die Werte aus den Pufferpuffern:


Mit einem "close" -Wert von 0,2 und einem "far" -Wert von 5000 können wir die Werte von cb12_v21.xy wie folgt berechnen: Dieses Codefragment wird häufig in TW3-Shadern gefunden, daher denke ich, dass dies nur eine Funktion ist. Nachdem Sie die Pyramide für die Tiefe der Sichtbarkeit erhalten haben, verwendet Zeile 7 eine Skalierung / Verzerrung, um einen Interpolationskoeffizienten zu erstellen (hier verwenden wir saturate , um die Werte auf [0-1] zu begrenzen).

cb12_v21.y = 1.0 / near
cb12_v21.x = - (1.0 / near) + (1.0 / near) * (near / far)







cb3_v1.xy und cb3_v2.xy ist die Helligkeit des Schärfeffekts in nahen und weiten Entfernungen. Nennen wir sie "SharpenNear" und "SharpenFar". Dies ist der einzige Unterschied zwischen den Optionen „Niedrig“ und „Hoch“ dieses Effekts in The Witcher 3.

Nun ist es an der Zeit, den resultierenden Koeffizienten zu verwenden. Die Zeilen 8-9 laufen gerade lerp(sharpenNear, sharpenFar, interpolationCoeff). Wofür ist es? Dadurch erhalten wir in der Nähe von Geralt und von ihm weg eine andere Helligkeit. Schauen Sie doch mal rein:


Vielleicht ist das kaum wahrnehmbar, aber hier interpolieren wir die Entfernung der Schärfe neben dem Player (2.177151) und die Helligkeit des Effekts sehr weit (1.91303). Nach dieser Berechnung addieren wir uns zur Helligkeit 1.0 (Zeile 10). Warum brauchst du das? Angenommen, die oben gezeigte Lerp-Operation gab uns 0,0. Nach dem Hinzufügen von 1.0 erhalten wir natürlich 1,0, und dieser Wert beeinflusst das Pixel beim Schärfen nicht. Lesen Sie unten mehr darüber.

Beim Hinzufügen von Schärfe möchten wir den Himmel nicht beeinflussen. Dies kann durch Hinzufügen eines einfachen bedingten Tests erreicht werden: In The Witcher 3 beträgt die Pixeltiefe des Himmels 1,0, sodass wir eine Art „binärer Filter“ erhalten (interessanter Fakt: In diesem Fall funktioniert Schritt nicht richtig).

// Не выполнять sharpen для неба
float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;




Nun können wir die interpolierte Helligkeit mit dem "Himmelsfilter" multiplizieren:


Diese Multiplikation wird in Zeile 13 durchgeführt.

Ein Beispiel für einen Shader-Code:

// Вычисление финального значения sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;


Pixel-Sampling Center


In SV_Position gibt es einen Aspekt, der hier wichtig ist: ein Versatz von einem halben Pixel . Es stellt sich heraus, dass dieses Pixel in der oberen linken Ecke (0, 0) aus Sicht von SV_Position.xy keine Koordinaten (0, 0) hat, sondern (0,5, 0,5). Wow!

Hier wollen wir eine Probe in der Mitte des Pixels nehmen. Schauen wir uns die Zeilen 14-16 an. Sie können sie auf HLSL schreiben: Und später probieren wir die Farbeingabetextur von texcoords uvCenter. Machen Sie sich keine Sorgen, das Ergebnis der Probenahme ist das gleiche wie in der "normalen" Methode (SV_Position.xy / ViewportSize.xy).

// Сэмплируем центр пикселя.
// Избавляемся от "половинопиксельного" смещения в SV_Position.xy.
float2 uvCenter = trunc( Input.Position.xy );

// Прибавляем половину пикселя, чтобы мы сэмплировали именно центр пикселя
uvCenter += float2(0.5, 0.5);
uvCenter /= g_Viewport.xy




Schärfen oder nicht schärfen


Die Entscheidung, ob der Scharfzeichner 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 an der Zeit, das Innere des Algorithmus genau zu betrachten.

Im Wesentlichen führt er die folgenden Aktionen aus:

- tastet die Eingabefarbtextur an den Pixelecken viermal ab,

- addiert die Abtastwerte und berechnet den Durchschnittswert,

- berechnet die Differenz zwischen "center" und "cornerAverage",

- ermittelt den maximalen absoluten Anteil der Differenz,

- korrigiert das Maximum . abs Komponente, unter Verwendung der Werte von scale + bias,

- bestimmt die Stärke des Effekts unter Verwendung von max. abs Komponente,

- berechnet den Helligkeitswert (Luma) für "centerColor" und "averageColor",

- dividiert das colorCenter durch seine Luma,

- berechnet den neuen interpolierten Luma-Wert anhand der Effektgröße,

- multipliziert das colorCenter mit dem neuen Luma-Wert.

Es war ziemlich viel Arbeit, und es fiel mir schwer, das herauszufinden, weil ich nie mit Schärfefiltern experimentiert habe.

Beginnen wir mit dem Stichprobenmuster. Wie Sie im Assemblycode sehen, werden vier Texturlesevorgänge ausgeführt.

Am besten zeigen Sie dies am Beispiel eines Pixelbildes (der Kenntnisstand des Künstlers ist ein Experte ):


Alle Lesevorgänge im Shader verwenden bilineare Abtastung (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 es auf HLSL implementiert werden kann. Mal sehen: Nun sind alle vier Samples in der Variable „colorCorners“ zusammengefasst. Führen Sie die folgenden Schritte aus: Die Kantenerkennung wird durch Berechnen der max. abs die Differenzkomponente. Intelligenter Umzug! Sehen Sie seine Visualisierung:

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;




// Вычисляем среднее четырёх углов
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);





Visualisierung der maximalen absoluten Komponente der Differenz.

Großartig Ready HLSL-Shader hier gepostet . Entschuldigung für die ziemlich schlechte Formatierung. Sie können mein HLSLexplorer- Programm verwenden und mit dem Code experimentieren.

Ich freue mich sagen zu können, dass der obige Code den gleichen Assembler-Code erzeugt wie im Spiel!

Zusammenfassend: Der Witcher 3-Schärfe-Shader ist sehr gut geschrieben (beachten Sie, dass fPixelSharpenAmount größer als 1,0 ist! Das ist interessant ...). Darüber hinaus können Sie die Helligkeit des Effekts hauptsächlich über die Helligkeit von nahen / fernen Objekten ändern. In diesem Spiel sind sie keine Konstanten; Ich habe einige Beispiele für die Werte gesammelt:

Skellige:

SharpenNear SharpenFar SharpenDistanceScale sharpenDistanceBias SharpenLumScale schärfenLumBias
niedrig
hoch 2,0 1.8 0,025
-0,25
-13,33333
1.33333

Kaer Morhen:

SharpenNear
SharpenFar
SharpenDistanceScale
sharpenDistanceBias
SharpenLumScale
schärfenLumBias
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. Mittlere Helligkeit


Die Berechnung der durchschnittlichen Helligkeit des aktuellen Frames kann in fast jedem modernen Videospiel gefunden werden. Dieser Wert wird später häufig für den Effekt der Augenanpassung und der Tonwertkorrektur verwendet (siehe den vorherigen Teil des Beitrags). In einfachen Lösungen wird zum Beispiel eine Helligkeitsberechnung für eine Textur mit einer Größe von 512 2 verwendet , dann werden deren Mip-Pegel berechnet und der letztere wird angewendet. Dies funktioniert normalerweise, schränkt jedoch die Möglichkeiten stark ein. In komplexeren Lösungen werden Rechen-Shader verwendet, die beispielsweise eine parallele Reduktion durchführen .

Finden wir heraus, wie das Team von CD Projekt Red dieses Problem in The Witcher 3 gelöst hat. Im vorherigen Teil habe ich bereits die tonale Korrektur und Anpassung des Auges untersucht, so dass das einzige verbleibende Puzzleteil die durchschnittliche Helligkeit blieb.

Zum Berechnen der durchschnittlichen Helligkeit von The Witcher 3 sind zwei Durchgänge erforderlich. Der Klarheit halber habe ich mich dazu entschieden, sie in einzelne Teile zu zerlegen, und zunächst betrachten wir den ersten Durchgang - die „Helligkeitsverteilung“ (Berechnung des Helligkeitshistogramms).

Helligkeitsverteilung


Diese beiden Durchgänge sind in jedem Frame-Analysator relativ einfach zu finden. Dies sind weitere Aufrufe an Dispatch, bevor sich das Auge anpasst:


Schauen wir uns die Eingabe für diesen Pass an. Er benötigt zwei Texturen:

1) HDR-Pufferfarbe, deren Maßstab auf 1/4 x 1/4 (z. B. von 1920 x 1080 auf 480 x 270) reduziert wird,

2) Vollbild- Tiefenpuffer


HDR-Farbpuffer mit 1/4 x 1/4 Auflösung. Beachten Sie den kniffligen Trick - dieser Puffer ist Teil eines größeren Puffers. Die Verwendung von Puffern ist eine gute Praxis.


Vollbild-Tiefenpuffer

Warum die Größe des Farbpuffers verringern? Ich denke, es geht um Leistung.

Die Ausgabe dieses Durchlaufs ist ein strukturierter Puffer. 256 Elemente mit jeweils 4 Bytes.

Hier haben Shader keine Debug-Informationen, nehmen Sie also an, dass dies nur ein Puffer von vorzeichenlosen int-Werten ist.

Wichtig: In der ersten Stufe der Berechnung der durchschnittlichen Helligkeit setzt ClearUnorderedAccessViewUint alle Elemente des strukturierten Puffers zurück.

Untersuchen wir den Assembler-Code des Berechnungs-Shader (dies ist der erste Berechnungs-Shader für unsere gesamte Analyse!) Und den Puffer der Konstanten:

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





Wir wissen bereits, dass der erste Eingang der HDR-Farbpuffer ist. Mit FullHD ist die Auflösung 480x270. Schauen Sie sich den Dispatch-Aufruf an.

Dispatch (270, 1, 1) - Dies bedeutet, dass 270 Gruppen von Threads ausgeführt werden. Einfach ausgedrückt, führen wir eine Fadengruppe für jede Zeile des Farbpuffers aus.


Jede Thread-Gruppe führt eine Zeile des HDR-Puffers mit Farben aus.

Nachdem wir nun 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 einen gemeinsam genutzten Speicher mit 256 Elementen mit jeweils 4 Bytes (dcl_tgsm_structured g0, 4, 256).

Beachten Sie, dass im Shader SV_GroupThreadID (vThreadIDInGroup.x) [0-63] und SV_GroupID  (vThreadGroupID.x) [0-269] verwendet wird.

1) Wir beginnen damit, allen Elementen des gemeinsam genutzten Speichers Nullwerte zuzuweisen. Da der Gesamtspeicher 256 Elemente und 64 Streams pro Gruppe enthält, kann dies bequem in einem einfachen Zyklus durchgeführt werden: 2) Danach wird eine Barriere mit eingerichtet

// Первый шаг - присвоение всем общим данным нулевых значений.
// Так как в каждой группе потоков есть 64 потока, каждый из них может с помощью простого смещения обнулить 4 элемента.
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = 0;
}


GroupMemoryBarrierWithGroupSync  (sync_g_t). Damit stellen wir sicher, dass alle Threads im Shared Memory der Gruppen auf Null zurückgesetzt werden, bevor mit der nächsten Stufe fortgefahren wird.

3) Jetzt führen wir eine Schleife aus, die grob wie folgt geschrieben werden kann: Dies ist eine einfache for-Schleife mit einem Inkrement von 64 (haben Sie verstanden, warum?). Der nächste Schritt ist die Berechnung der Position des geladenen Pixels. Lass uns darüber nachdenken. Für die Y-Koordinate können wir SV_GroupID.x verwenden, da 270 Thread-Gruppen gestartet wurden. Für die X-Koordinate können wir ... den aktuellen Gruppenfluss nutzen! Lass es uns versuchen. Da sich in jeder Gruppe 64 Streams befinden, werden bei dieser Lösung alle Pixel umgangen. Betrachten Sie eine Gruppe von Threads (0, 0, 0).

// cb0_v0.x - это ширина буфера цветов уменьшенного масштаба. Для 1920x1080 она равна 1920/4 = 480;
float ViewportSizeX = cb0_v0.x;
[loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )
{
...
















- 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 Strom (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: Siehe? Es ist ziemlich einfach! Ich habe auch die Helligkeit berechnet (Zeile 21 des Assembler-Codes). Toll, wir haben die Helligkeit bereits aus dem Farbpixel berechnet. Der nächste Schritt ist das Laden (nicht Sampling!) Des entsprechenden Tiefenwerts.

// Мы попиксельно перемещаемся вдоль оси 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);








Hier haben wir jedoch ein Problem, weil wir einen Tiefenpuffer mit voller Auflösung angeschlossen haben. Was soll ich damit machen?

Es ist überraschend einfach: Multiplizieren Sie einfach die colorPos mit einer Konstanten (cb0_v2.z). Wir haben die HDR-Pufferfarbe um den Faktor vier reduziert. also wird der Wert 4 sein! Soweit ist alles super! Aber ... wir haben die Zeilen 24-25 erreicht ... Also. Erstens haben wir einen Vergleich der Gleichheit mit einem Fließkomma, das Ergebnis wird in r2.x geschrieben, und gleich danach geht es weiter ... was? Bitweise und ?? Im Ernst Für Gleitkommawerte? Was zur Hölle??? Problem 'eq + und' Lassen Sie mich einfach sagen, dass dies der schwierigste Teil des Shaders für mich war. Ich habe sogar seltsame Kombinationen asint / asfloat ausprobiert ...

const int iDepthTextureScale = (int) cb0_v2.z;
uint2 depthPos = iDepthTextureScale * colorPos;
float depth = texture1.Load( int3(depthPos, 0) ).x;




24: eq r2.x, r2.x, cb0[2].w
25: and r2.x, r2.x, cb0[2].y








Und wenn Sie einen etwas anderen Ansatz verwenden? Lassen Sie uns einfach den üblichen Float-Float-Vergleich in HLSL durchführen. Und so sieht die Ausgabe im Assembler-Code aus: Interessant, oder? Ich habe nicht erwartet, hier "und" zu sehen. 0x3f800000 ist nur 1.0f ... Es ist sinnvoll, da wir 1.0 und 0.0 erhalten, wenn der Vergleich erfolgreich ist. Was aber, wenn wir 1.0 durch einen anderen Wert "ersetzen"? Zum Beispiel: Holen Sie sich dieses Ergebnis: Ha! Es hat funktioniert. Dies ist nur der Zauber des HLSL-Compilers. Hinweis: Wenn Sie 0.0 durch etwas anderes ersetzen, erhalten Sie einfach movc.

float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y);
return test;
}




0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, l(0x3f800000)
2: ret








float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y) ? cb0_v0.z : 0.0;
return test;
}




0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, cb0[0].z
2: ret




Kehren wir zum rechnerischen Shader zurück. Der nächste Schritt ist zu prüfen, ob die Tiefe dem Wert von cb0_v2.w entspricht. Es ist immer 0.0, dh wir prüfen, ob sich das Pixel in der Ferne (im Himmel) befindet. Wenn ja, 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 (der Wert von cb0_v2.x, der oft ungefähr 0,0 beträgt). Ich gehe davon aus, dass es notwendig ist, die Wichtigkeit des Himmels bei der Berechnung der durchschnittlichen Helligkeit zu kontrollieren. Normalerweise nimmt die Bedeutung ab. Sehr kluge 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, ist der nächste Schritt die Berechnung des natürlichen Logarithmus, um eine gute Verteilung zu erhalten. Aber warte mal, lumaOk ist 0.0. Wir wissen, dass der Wert von log (0) undefiniert ist, also addieren wir 1.0, da log (1) = 0.0 ist.

Danach skalieren wir den berechneten Logarithmus um 128, um ihn auf 256 Zellen zu verteilen. Sehr schlau!

Und von hier aus wird dieser Wert auf 88.722839 genommen. Dies 128 * натуральный логарифм (2).

HLSL berechnet nur so Logarithmen.

Im HLSL-Assemblycode gibt es nur eine Funktion, die Logarithmen berechnet: log , und sie hat Basis 2. Zum Schluss berechnen wir den Zellenindex aus der logarithmisch verteilten Helligkeit und fügen der entsprechenden Zelle im gemeinsam genutzten Speicher 1 hinzu.

// Предположим, что lumaOk равно 0.0.
// log(0) имеет значение undefined
// log(1) = 0.
// вычисляем натуральный логарифм яркости
lumaOk = log(lumaOk + 1.0);

// Масштабируем логарифм яркости на 128
lumaOk *= 128;




// Вычисляем правильный индекс. Значение имеет формат Uint, поэтому в массиве 256 элементов,
// нужно убедиться, что мы не вышли за границы.
uint uLuma = (uint) lumaOk;
uLuma = min(uLuma, 255);

// Прибавляем 1 к соответствующему значению яркости.
InterlockedAdd( shared_data[uLuma], 1 );


Als nächstes müssen Sie die Barriere erneut installieren, um sicherzustellen, dass alle Pixel in der Zeile verarbeitet wurden.

Der letzte Schritt ist das Hinzufügen von Werten aus dem gemeinsamen Speicher zu einem strukturierten Puffer. Dies geschieht auf dieselbe Weise durch eine einfache Schleife: Nachdem alle 64 Threads in der Thread-Gruppe die allgemeinen Daten eingegeben haben, fügt jeder Stream dem Ausgangspuffer 4 Werte hinzu. Betrachten Sie einen Ausgabepuffer. Lass uns darüber nachdenken. Die Summe aller Werte im Puffer entspricht der Gesamtzahl der Pixel! (bei 480 x 270 = 129 600). Das heißt, wir wissen, wie viele Pixel einen bestimmten Helligkeitswert haben.

// Ждём, пока обработаются все пиксели в строке
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 );
}






Wenn Sie sich mit mathematischen Shadern (wie mir) nicht auskennen, ist dies möglicherweise zunächst nicht klar. Lesen Sie den Post noch ein paar Mal, nehmen Sie Papier und einen Stift und versuchen Sie, die Konzepte zu verstehen, auf denen diese Technik basiert.

Das ist alles! So berechnet "The Witcher 3" das Helligkeitshistogramm. Ich habe beim Schreiben dieses Teils viel gelernt. Gratulation an die Jungs von Projekt Red CD mit hervorragender Arbeit!

Wenn Sie an einem vollständigen HLSL-Shader interessiert sind, wird dieser hier veröffentlicht . Ich bemühe mich immer, so nahe wie möglich an den Code zum Zusammenstellen von Spielen heranzukommen, und ich bin absolut glücklich, dass es mir wieder gelang!

Durchschnittliche Helligkeitsberechnung


Dies ist der zweite Teil der Analyse der Durchschnittshelligkeitsberechnungen in "The Witcher 3: Wild Hunt".

Bevor wir uns mit einem anderen Computer-Shader bekämpfen, wiederholen wir kurz das, was im letzten Teil passiert ist: Wir haben mit einem HDR-Farbpuffer mit einer auf 1 / 4x1 / 4 reduzierten Skala gearbeitet. Nach dem ersten Durchgang haben wir ein Helligkeitshistogramm erhalten (strukturierter Puffer mit 256 vorzeichenlosen ganzzahligen Werten). Wir haben den Logarithmus für die Helligkeit jedes Pixels berechnet, auf 256 Zellen verteilt und den entsprechenden Wert des strukturierten Puffers um 1 pro Pixel erhöht. Daher ist die Gesamtsumme aller Werte in diesen 256 Zellen gleich der Anzahl der Pixel.


Ein Beispiel für die Ausgabe des ersten Durchlaufs. Hier sind 256 Elemente.

Zum Beispiel ist unser Vollbildpuffer 1920x1080. Nach dem Herauszoomen wurde im ersten Durchlauf ein Puffer mit 480 x 270 Pixeln verwendet. Die Summe aller 256 Werte im Puffer entspricht 480 * 270 = 129 600.

Nach dieser kurzen Einführung können wir mit dem nächsten Schritt fortfahren: zu den Berechnungen.

Diesmal wird nur eine Thread-Gruppe verwendet (Dispatch (1, 1, 1)).

Schauen wir uns den Assembler-Code des Berechnungs-Shaders an: Hier gibt es einen konstanten Puffer:

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





Sehen Sie sich kurz den Assembler-Code an: Es werden zwei UAVs angehängt (u0: Eingangspuffer aus dem ersten Teil und u1: Ausgangstextur des 1x1-Formats R32_FLOAT). Wir sehen auch, dass es 64 Streams pro Gruppe und 256 Elemente des gemeinsam genutzten 4-Byte-Gruppenspeichers gibt.

Beginnen wir damit, den Shared Memory mit Daten aus dem Eingangspuffer zu füllen. Wir haben 64 Threads, also müssen wir fast dasselbe machen wie zuvor.

Um absolut sicher zu sein, dass alle Daten zur weiteren Verarbeitung geladen werden, setzen wir eine Barriere. Alle Berechnungen werden nur in einem Datenstrom ausgeführt, alle anderen werden einfach dazu verwendet, Werte aus dem Puffer in den gemeinsamen Speicher zu laden.

// Первый этап - заполнение всех общих данных данными из предыдущего этапа.
// Так как в каждой группе потоков по 64 потока, каждый может заполнить 4 элемента в одном потоке
// с помощью простого смещения.
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = g_buffer[offset];
}
// Здесь мы устанавливаем барьер, то есть блокируем выполнение всех потоков группы, пока не будет завершён
// весь общий доступ групп и все потоки в группе не достигнут этого вызова.
GroupMemoryBarrierWithGroupSync();




Der "berechnende" Thread hat den Index 0. Warum? Theoretisch können wir jeden Stream aus dem Intervall [0-63] verwenden, aber dank eines Vergleichs mit 0 können wir einen zusätzlichen Integer-Integer-Vergleich ( dhq- Anweisungen ) vermeiden .

Der Algorithmus basiert auf der Angabe des Pixelintervalls, das bei der Operation berücksichtigt wird.

In Zeile 11 multiplizieren wir die Breite * Höhe, um die Gesamtzahl der Pixel zu erhalten, und multipliziert sie mit zwei Zahlen aus dem Intervall [0.0f-1.0f], was den Beginn und das Ende des Intervalls angibt. Weitere Einschränkungen werden angewendet, 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, sind unten zwei Zyklen. Das Problem bei ihnen (oder beim Assemblierungscode) ist, dass am Ende der Zyklen seltsame bedingte Sprünge auftreten. Es war sehr schwierig für mich, sie neu zu erstellen. Schauen Sie sich auch Zeile 21 an. Warum gibt es eine -1? Ich werde das unten erklären.

Die Aufgabe des ersten Zyklus besteht darin, pixelToConsiderStart zu löschen und den Pufferzellenindex anzugeben , in dem pixel pixelToConsiderStart  +1 vorhanden ist (sowie die Anzahl aller Pixel in den vorherigen Zellen).

Angenommen, pixelToConsiderStartungefähr gleich 30.000 und im Puffer 37.000 Pixel in der Zelle "null" (dies geschieht im Spiel nachts). Daher möchten wir die Helligkeit etwa bei Pixel 30001 analysieren, die sich in der Zelle „Null“ befindet. In diesem Fall verlassen wir die Schleife sofort und erhalten den Anfangsindex '0' und null Pixel.

Schauen Sie sich den HLSL-Code an: Die geheimnisvolle Zahl "-1" aus Zeile 21 des Assembler-Codes ist mit einer booleschen Bedingung für die Ausführung eines Zyklus verknüpft (ich habe dies fast zufällig entdeckt). Nachdem wir die Anzahl der Pixel aus den LumaValue- Zellen und dem LumaValue selbst erhalten haben , können wir mit dem zweiten Zyklus fortfahren. Die Aufgabe des zweiten Zyklus ist die Berechnung des Einflusses von Pixeln und der durchschnittlichen Helligkeit. Wir beginnen mit lumaValue , berechnet in der ersten Schleife.

// Количество уже обработанных пикселей
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++;
}
}










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;


In diesem Stadium haben wir den Helligkeitswert erhalten, der im Intervall [0.0f-255.f] codiert ist.

Der Decodierungsprozess ist ziemlich einfach - Sie müssen die Berechnungen der Codierungsphase umkehren.

Eine kurze Wiederholung des Codierungsprozesses: Um die Helligkeit zu decodieren, kehren wir den Codierungsprozess beispielsweise folgendermaßen um: Dann berechnen wir die Verteilung, indem wir die Anzahl der Pixel mit einer gegebenen Helligkeit mit der decodierten Helligkeit multiplizieren und diese summieren, bis die Pixelverarbeitung pixToConsiderEnd erreicht ist . Danach teilen wir den Gesamteffekt durch die Anzahl der analysierten Pixel. Hier ist der Rest der Schleife (und der Shader): Der vollständige Shader ist hier veröffentlicht . Es ist voll kompatibel mit meinem HLSLexplorer- Programm .

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);




// начинаем с прибавления 0.5f (мы не хотим, чтобы получился нулевой результат)
float fDecodedLuma = encodedLumaFloat + 0.5;

// и декоридуем яркость:

// Делим на 128
fDecodedLuma /= 128.0;

// exp(x), что отменяет log(x)
fDecodedLuma = exp(fDecodedLuma);

// Вычитаем 1.0
fDecodedLuma -= 1.0;








// Вычисляем влияние этой яркости
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;


Ohne das konnte ich die Berechnung der durchschnittlichen Helligkeit in The Witcher 3 (und all den anderen Effekten auch!) nicht effektiv nachstellen.

Zum Schluss noch ein paar Gedanken. Unter dem Gesichtspunkt der Berechnung der durchschnittlichen Helligkeit war es schwierig, diesen Shader neu zu erstellen. Die Hauptgründe sind:

1) Seltsame "verzögerte" Prüfungen der Zyklusausführung, es dauerte viel länger als ich früher erwartet hatte.

2) Probleme beim Debuggen dieses Berechnungsshaders in RenderDoc (v. 1.2).

Die Operationen "ld_structured_indexable" werden nicht vollständig unterstützt, obwohl das Leseergebnis aus dem Index 0 den korrekten Wert angibt, alle anderen geben Nullen zurück, weshalb die Zyklen unbegrenzt fortgesetzt werden.

Obwohl ich nicht den gleichen Assembler-Code wie im Original erhalten konnte (Unterschiede in der Abbildung unten), konnte ich mithilfe von RenderDoc diesen Shader in die Pipeline einfügen - und die Ergebnisse waren die gleichen!


Das Ergebnis der Schlacht. Links - mein Shader, rechts - der Original-Baugruppencode.

Teil 8. Der Mond und seine Phasen


Im achten Teil des Artikels erforsche ich den Shader des Mondes von The Witcher 3 (und genauer von der Erweiterung Blood and Wine).

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 wahres Vergnügen, nachts in TW3 zu laufen.

Schau dir diese Szene einfach an!


Bevor wir uns dem Pixel-Shader annehmen, sage ich noch ein paar Worte zu den Render-Nuancen. Geometrisch ist der Mond nur eine Kugel (siehe unten), die Texturkoordinaten, Normalenvektoren und Tangentenvektoren aufweist. Der Vertex-Shader berechnet die Position im Weltraum sowie die normalisierten Vektoren von Normalen, Tangenten und Tangenten an zwei Punkten (unter Verwendung des Vektorprodukts), multipliziert mit der Matrix der Welt.

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 Mond

gezeichnet wurde : Nun, alles, ich denke, Sie können fortfahren. Schauen wir uns den Pixel-Shader an: Der Hauptgrund, warum ich den Shader von „Blood and Wine“ gewählt habe, ist einfach - er ist kürzer. Zuerst berechnen wir den Versatz für die Texturabtastung. cb0 [0] .w wird als Versatz entlang der X-Achse verwendet: Mit diesem einfachen Trick können Sie 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 Puffer von Konstanten

Eine Textur (1024x512) wird als Eingabe angehängt. In den RGB-Kanälen wird die normale Karte kodiert und im Alphakanal die Farbe der Mondoberfläche. Clever!


Der Alphakanal der Textur ist die Farbe der Mondoberfläche.


Textur-RGB-Kanäle sind eine normale Karte.

Mit den richtigen Texturkoordinaten probieren wir die RGBA-Kanäle. Wir müssen die normale Karte auspacken und eine Gammakorrektur der Oberflächenfarbe durchführen. Derzeit kann der HLSL-Shader beispielsweise folgendermaßen geschrieben werden: Der nächste Schritt besteht darin, die Normalen zu binden, jedoch nur in den XY-Komponenten. (In The Witcher 3 zeigt die Z-Achse nach 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 die Zeilen 15-16 noch einmal an: Was ist das geheimnisvolle 0.033864? Zunächst scheint es keinen Sinn zu haben, aber wenn wir die Umkehrung davon berechnen, werden wir ungefähr 29,53 erhalten, was der Dauer des synodischen Monats entspricht.

float4 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)


am tag Das ist es, was ich auf Details aufmerksam mache!

Wir können sicher 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.

Die Berechnung des Skalarprodukts zwischen dem Normalvektor und dem "Mond" -Vektor simuliert eine Mondphase. 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)) );






Die letzte Stufe besteht aus einer Reihe von Multiplikationsoperationen zur Berechnung der endgültigen Farbe. Sie verstehen möglicherweise nicht, warum dieser Shader den Alpha-Wert 0,0 an die Ausgabe sendet. Dies liegt daran, dass der Mond mit aktivierter Mischung 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);





Auf diese Weise können Sie die Farbe des Hintergrunds (Himmel) ermitteln, wenn dieser Shader Schwarz zurückgibt.

Wenn Sie sich für einen kompletten Shader interessieren, können Sie ihn hier mitnehmen . Es hat große konstante Puffer und sollte bereits für die Injektion in RenderDoc anstelle des ursprünglichen Shader bereit sein (benennen Sie einfach "MoonPS" in "EditedShaderPS" um).

Eine letzte Sache: Ich wollte die Ergebnisse mit Ihnen teilen:

Links mein Shader, rechts der ursprüngliche Shader aus dem Spiel.

Der Unterschied ist minimal und beeinflusst die Ergebnisse nicht.


Wie Sie sehen, war dieser Shader ziemlich einfach zu erstellen.

Teil 9. G-Puffer


In diesem Abschnitt werden einige Details zum Puffer in The Witcher 3 behandelt.

Wir gehen davon aus, dass Sie die Grundlagen der verzögerten Schattierung kennen.

Eine kurze Wiederholung: Die Idee des Verschiebens besteht nicht darin, die gesamte Beleuchtung und Schattierung gleichzeitig zu berechnen, sondern die Berechnungen in zwei Schritte zu unterteilen.

Im ersten (Geometriedurchlauf) füllen wir den GBuffer mit Oberflächendaten (Position, Normalen, Spiegelfarbe usw.), und im zweiten (Beleuchtungsdurchgang) kombinieren wir alles und berechnen die Beleuchtung.

Die verzögerte Schattierung ist ein sehr beliebter Ansatz, da Sie damit Techniken für einen einzelnen Vollbilddurchlauf berechnen können, z. B. verzögerte Schattierung mit Kacheln , wodurch die Leistung erheblich verbessert wird.

Einfach ausgedrückt ist GBuffer eine Sammlung 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-Technologien erkunden .

Nach dieser kurzen Einführung schauen wir uns ein Beispiel eines Frames von The Witcher 3: Blood and Wine an:


Eines der vielen Hotels in Tussenta

Der Haupt-GBuffer besteht aus drei Vollbild-Renderzielformaten DXGI_FORMAT_R8G8B8A8_UNORM und dem Tiefenpuffer- und Schablonenformat DXGI_FORMAT_D24_UNORM_S8_UINT.

Hier sind ihre Screenshots:


Renderziel 0 - RGB-Kanäle, Oberflächenfarbe


Renderziel 0 - Alphakanal. Ehrlich gesagt habe ich keine Ahnung, was diese Informationen sind.


Renderziel 1 - RGB-Kanäle. Hier werden die Normalenvektoren im Intervall [0-1] geschrieben.


Renderziel 1 - Alphakanal. Es sieht aus wie ein Reflexionsvermögen!


Renderziel 2 - RGB-Kanäle. Sieht aus wie eine Spiegelfarbe!

In dieser Szene ist der Alphakanal schwarz (wird später jedoch verwendet).


Tiefenpuffer Beachten Sie, dass hier die umgekehrte Tiefe verwendet wird.


Schablonenpuffer zum Markieren eines bestimmten

Pixeltyps (z. B. Haut, Vegetation usw.) Dies ist nicht der gesamte GBuffer. Der Lichtpass verwendet auch Lichtsonden und andere Puffer, aber ich werde sie in diesem Artikel nicht berücksichtigen.

Bevor ich zum "Hauptteil" des Posten weiter gehe, werde ich allgemeine Bemerkungen machen:

Allgemeine Bemerkungen



1) Der einzige zu löschende Puffer ist der Tiefen- / Schablonenpuffer.

Wenn Sie die oben genannten Texturen in einem guten Frame-Analysator analysieren, werden Sie ein wenig überrascht sein, da der Aufruf "Clear" für sie nicht verwendet wird, mit Ausnahme von Depth / Stencil.

Das heißt, RenderTarget1 sieht in Wirklichkeit so aus (beachten Sie die "verschwommenen" Pixel auf der fernen Ebene):


Dies ist eine einfache und intelligente Optimierung.

Wichtige Lektion: Sie müssen Ressourcen für Aufrufe von ClearRenderTargetView verschwenden . Verwenden Sie sie daher nur bei Bedarf.

2) Inverted Tiefe - es ist kühl

in vielen Artikeln bereits geschrieben über die Genauigkeit des Tiefenpuffers mit Gleitkomma. Hexer 3 verwendet reversed-z. Dies ist eine natürliche Wahl für ein solches Open-World-Spiel und Langstrecken-Rendering.

Es ist einfach, auf DirectX umzuschalten:

a) Löschen Sie den Tiefenpuffer mit der Eingabe „0“ und nicht mit „1“.

Bei dem traditionellen Ansatz wurde der Fernwert "1" zum Reinigen des Tiefenpuffers verwendet. Nach der Umkehrung der Tiefe wurde der neue Wert für "Fernbereich" auf 0 gesetzt, sodass Sie alles ändern müssen.

b) Vertauschen Sie die nahen und fernen Grenzen bei der Berechnung der Projektionsmatrix.

c) Ändern Sie die Tiefenprüfung von "weniger" in "mehr".

Für OpenGL muss etwas mehr Arbeit ausgeführt werden (siehe die oben genannten Artikel), aber es lohnt sich.

3) Behalte keine Position in der Welt

Ja, alles ist so einfach. Im Gang der Lichtposition in der Welt aus der Tiefe neu zu schaffen.

Pixel-Shader


In diesem Teil wollte ich genau den Pixel-Shader zeigen, der Oberflächendaten an GBuffer liefert.

So wissen wir jetzt schon, wie man Farbe, Normalen und Spiegelungen 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 übertragenen Texturen und der Anzahl der verwendeten Parameter aus dem Puffer der Konstanten (wahrscheinlich aus dem Puffer der Konstanten, die das Material beschreiben).

Für die Analyse entschied ich mich für dieses schöne Fass:


Unser heroisches Fass!

Bitte begrüßen Sie Texturen:


Wir haben also Albedo, normale Karten- und Spiegelfarbe. Ziemlich Standardfall.

Bevor wir beginnen, ein paar Worte zur Geometrieeingabe: Die

Geometrie wird mit Position, Texkoord, normalen Puffern und Tangenten übertragen.

Der Vertex-Shader zeigt mindestens Texcoords, normalisierte Tangenten-, Normal- und Tangenten-Zwei-Punkt-Vektoren, die zuvor mit der Weltmatrix multipliziert wurden. Für komplexere Materialien (z. B. mit zwei diffusen Maps oder zwei normalen Maps) kann der Vertex-Shader andere Daten ausgeben, ich wollte hier jedoch ein einfaches Beispiel zeigen.

Pixel-Shader im Assembler-Code: Der Shader besteht aus mehreren Stufen. 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




Zunächst aber wie üblich ein Screenshot mit den Werten aus dem Puffer der Konstanten:


Albedo


Wir werden mit komplexen Dingen beginnen. Dies 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ättigungsreduktionspuffer" bezeichnet. Lassen Sie mich den HLSL-Code anzeigen: Bei den meisten Objekten gibt dieser Code nur die ursprüngliche Farbe aus der Textur zurück. Dies wird durch die entsprechenden Werte von "Material-Puffer" erreicht. cb4_v1.x ist auf 1,0 gesetzt, was eine Maske von 0,0 zurückgibt und die Eingabefarbe von der Lerp- Anweisung zurückgibt . Es gibt jedoch einige Ausnahmen. Der höchste von mir gefundene desaturationFactor- Wert ist 4,0 (er ist nie kleiner als 1,0) und desaturatedColor

float3 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;
}




hängt vom Material ab. Es kann etwas wie (0,2, 0,3, 0,4) sein; Es gibt keine strengen Regeln. Natürlich konnte ich nicht anders , als dies in seinem eigenen DX11-Rahmen zu realisieren, und hier sind die Ergebnisse, wo alle Werte desaturatedColor gleich float3 (0,25, 0,3, 0,45)


Entsättigungsfaktor = 1,0 (keine Auswirkung)


Entsättigungsfaktor = 2,0


Entsättigungsfaktor = 3,0


desaturationFactor = 4.0

Ich bin sicher, es ist nur die Anwendung der Materialparameter, aber nicht das Ende des Teils mit Albedo.

Die Zeilen 15–20 fügen den letzten Schliff hinzu: v0.z ist die Ausgabe des Vertex-Shaders, und sie sind Null. Vergiss das nicht, denn v0.z wird später ein paar Mal verwendet. Es sieht so aus, als wäre dies eine Art Koeffizient, und der gesamte Code sieht aus wie eine leichte Verdunkelung der Albedo, aber da v0.z gleich 0 ist, bleibt die Farbe gleich. HLSL: Was RT0.a angeht, so wird es, wie wir sehen, aus dem Puffer der Materialkonstanten genommen. Da der Shader jedoch keine Debug-Informationen enthält, ist es schwierig zu sagen, was es ist. Mögliche Transluzenz? Das erste Renderziel ist 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;






Normale


Beginnen wir mit dem Auspacken der normalen Karte, und dann werden wir wie üblich die Normalen binden: Bis jetzt ist nichts überraschend. Schauen Sie sich die Zeilen 28-33 an: Wir können sie grob wie folgt schreiben: Nicht sicher, ob es die richtige Schreibweise ist. Wenn Sie wissen, was diese mathematische Operation ist, lassen Sie es mich wissen. Wir sehen, dass der Pixel-Shader SV_IsFrontFace verwendet. Was ist das? Dokumentation hilft mir (ich wollte "msdn" schreiben, aber ...):

/* НОРМАЛИ */
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;
}








Bestimmt, ob das Dreieck in die Kamera blickt. Für Linien und Punkte gilt IsFrontFace. Ausnahmen sind die aus Dreiecken gezogenen Linien (Drahtgittermodus), die IsFrontFace auf ähnliche Weise wie das Rastern eines Dreiecks im Vollmodus angeben. Es kann mit einem Geometrieshader geschrieben werden, und das Lesen davon kann mit einem Pixel-Shader erfolgen.

Ich wollte es mir selbst ansehen. Tatsächlich ist der Effekt nur im Drahtgittermodus spürbar. Ich nehme an, dass dieses Code-Snippet benötigt wird, um die Normalen (und damit die Beleuchtung) im Drahtgittermodus korrekt zu berechnen.

Hier ein Vergleich: Beide Farben des Frames der fertigen Szene mit diesem aktivierten / deaktivierten Trick sowie die Textur-Normalen von gbuffer [0-1] mit dem aktivierten / deaktivierten Trick:


Szenenfarbe ohne Stunt


Farbszene mit einem Gimmick


Normalen [0-1] ohne Stunt


Normalen [0-1] mit einem Stunt:

Haben Sie bemerkt, dass jedes Renderziel in GBuffer das Format R8G8B8A8_UNORM hat? Das bedeutet, dass es pro Komponente 256 mögliche Werte gibt. Reicht das aus, um Normalen zu speichern?

Das Speichern von qualitativ hochwertigen 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 angewendet wird. Es muss gesagt werden, dass in der gesamten Geometriedurchführung eine zusätzliche Textur an Schlitz 13 angebracht ist ...:



Hah The Witcher 3 verwendet eine Technik namens " Best Fit Normals ". Ich werde es hier nicht ausführlich erklären (siehe Präsentation). Es wurde von Crytek zwischen 2009 und 2010 erfunden. Da CryEngine Open Source ist, ist BFN auch Open Source .

BFN verleiht der normalen Textur ein "körniges" Aussehen.

Nach dem Skalieren der Normalen mit BFN codieren wir sie vom Intervall [-1; 1] in [0, 1] um.

Spiegelnd


Beginnen wir mit der Linie 34 und probieren Sie die Spiegelstruktur: Wie Sie sehen, haben wir hier den von Albedo bekannten "Dimmfilter": Berechnen Sie die Komponente mit max. Wert, und dann berechnen wir die "dunkler" Farbe und interpolieren sie mit der ursprünglichen Spiegelfarbe, wobei wir den Parameter aus dem Vertex-Shader nehmen ... der Wert 0 ist. Daher erhalten wir bei der Ausgabe 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 Lichtdurchgang beeinflusst. Tatsache ist, dass der Alphakanal der Eingabe-Normal-Map zusätzliche Daten enthält:


Alphakanal-Textur "normale Karten". Montagecode

: Sag Hallo zu unserem alten Freund - v0.z! Seine Bedeutung ist Albedo und Spiegelung ähnlich: Großartig! Dies ist das Ende der Analyse der ersten Variante des Pixel-Shaders. Hier ist ein Vergleich meines Shaders (links) mit dem Original (rechts):

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;







Diese Unterschiede wirken sich nicht auf die Berechnungen aus, daher ist meine Arbeit hier beendet.

Pixel Shader: Option Albedo + Normalen


Ich entschied mich für eine andere Option, jetzt nur mit Albedo-Karten und Normalen, ohne spiegelnde Textur. Der Assembler-Code ist etwas länger: Der Unterschied zwischen dieser und den vorherigen Versionen ist wie folgt: a) Zeilen 1, 19 : Der Interpolationsparameter v0.z wird mit cb4 [0] .x aus dem Konstantenpuffer multipliziert. Dieses Produkt wird jedoch nur für die Interpolationsalbedo in Zeile 19 verwendet. Für andere Ausgänge wird der „normale“ Wert von v0.z verwendet. b) Zeilen 54-55 : o2.w ist jetzt gesetzt, vorausgesetzt (cb4 [7] .x> 0.0) Wir erkennen dieses Muster bereits als „irgendeine Art von Vergleich - AND“ an der Berechnung des Helligkeitshistogramms. Es kann wie folgt 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;



Es gibt hier keine spiegelnde Textur. Schauen wir uns den für diesen Teil verantwortlichen Assembler-Code an: Beachten Sie, was wir hier verwendet haben (1 ist die reflektierte Fähigkeit). Zum Glück ist es in HLSL recht einfach zu schreiben: Ich möchte hinzufügen, dass in dieser Variante der Puffer der Konstanten mit diesen Materialien etwas mehr ist. Hier werden diese zusätzlichen Werte verwendet, um eine Spiegelfarbe zu emulieren. Der Rest des Shaders ist derselbe wie in der vorherigen Version. 72 Zeilen Assemblercode sind zu viel, um in WinMerge angezeigt zu werden. Nehmen Sie sich also mein Wort: Mein Code entsprach fast dem des Originals. Oder Sie können meinen HLSLexplorer herunterladen und überzeugen Sie sich selbst!

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 so weit gelesen haben, möchten Sie vielleicht ein bisschen tiefer gehen.

Was im wirklichen Leben einfach erscheint, ist oft nicht so, und die Datenübertragung zum Puffer The Witcher 3 ist keine Ausnahme. Ich habe Ihnen nur die einfachsten Versionen von Pixel-Shadern gezeigt, die dafür verantwortlich sind, und auch allgemeine Beobachtungen angeführt, die sich auf das verzögerte Shadowing als Ganzes beziehen.

Für die meisten Patienten gibt es zwei Optionen für Pixel-Shader in Pastebin:

Option 1 - mit einer spiegelnden Textur

Option 2 - ohne eine spiegelnde Textur

Teil 10. Regenvorhänge in der Ferne


In diesem Teil betrachten wir einen wunderbaren atmosphärischen Effekt, den ich sehr mag - die fernen Regen- / Lichtvorhänge am Horizont. Im Spiel sind sie auf den Skelligen-Inseln am einfachsten zu treffen.


Ich persönlich mag dieses atmosphärische Phänomen sehr und war gespannt, wie es die Grafikprogrammierer CD Projekt Red umgesetzt haben. Lass es uns herausfinden!

Hier sind zwei Screenshots vor und nach dem Anbringen von Regenvorhängen:


Bis zu den Regenvorhängen


Nach den Regenvorhängen

Geometrie


Zuerst konzentrieren wir uns auf die Geometrie. Die Idee ist, einen kleinen Zylinder zu verwenden:


Ein Zylinder in einem lokalen Raum

Aus Sicht seiner Position in einem lokalen Raum ist er klein genug - seine Position liegt im Bereich (0,0 - 1,0).

Das Eingabeschema für diesen Renderaufruf sieht folgendermaßen aus:


Für uns hier ist Folgendes wichtig: Texcoords und Instance_Transform.

Texcoords sind recht einfach verpackt: U der oberen und der unteren Basis liegen im Intervall [0,02777 - 1,02734]. V an der unteren Basis beträgt 1,0 und an der oberen Basis 0,0. Wie Sie sehen, können Sie dieses Netz ganz einfach sogar prozedural erstellen.

Nachdem wir diesen kleinen Zylinder im lokalen Raum erhalten haben, multiplizieren wir ihn mit der Weltmatrix, die für jede Instanz des Eingangselements INSTANCE_TRANSFORM bereitgestellt wird. Lassen Sie uns die Werte dieser Matrix überprüfen:




Sieht ziemlich unheimlich aus, oder? Aber keine Sorge, wir analysieren diese Matrix und sehen, was sie verbirgt! Die Ergebnisse sind sehr interessant: Es ist wichtig, die Position der Kamera in diesem Bild zu kennen: (-116.5338, 234.8695, 2.09) Wie Sie sehen, haben wir den Zylinder so skaliert, dass er im globalen Raum recht groß ist (in TW3 ist die Z-Achse nach oben gerichtet), relativ zur Kameraposition verschoben und drehte sich um. So sieht der Zylinder nach der Transformation der Scheitelpunkt-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 Konvertierung durch Vertex-Shader. Sehen Sie, wie es relativ zur Sichtbarkeitspyramide positioniert ist.

Vertex-Shader


Die Eingabegeometrie und der Vertex-Shader sind streng voneinander abhängig.

Schauen wir uns den Vertex-Shader-Assembler-Code genauer an: Neben der einfachen Übertragung von Texcoords (Zeile 0) und Instance_LOD_Params (Zeile 8) benötigen wir zwei weitere Elemente für die Ausgabe: SV_Position (offensichtlich) und Height (Komponente .z) der Position in der Welt. Denken Sie daran, dass der lokale Raum im Bereich [0-1] liegt. Direkt vor dem Anwenden der Weltmatrix verwendet der Vertex-Shader Skalierung und Ablenkung, um die lokale Position zu ändern. Intelligenter Umzug! In diesem Fall ist scale = float3 (4, 4, 2) und bias = float3 (-2, -2, -1). < Das zwischen den Zeilen 9 und 28 sichtbare Muster ist die Multiplikation von zwei Reihenmatrizen. Schauen wir uns den fertigen Vertex-Shader auf 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! Zuerst zeige ich Ihnen die Eingabedaten: Hier werden

zwei Texturen verwendet: die Rauschtextur und der Tiefenpuffer:



Werte aus konstanten Puffern:





Und Code für die Pixel-Shader-Montage: Wow! Ziemlich viel, aber eigentlich ist alles nicht so schlecht. Was ist hier los? Zuerst berechnen wir das animierte UV-Licht unter Verwendung der verstrichenen Zeit aus dem Puffer (cb0 [0] .x) und dem Maßstab / Offset. Diese Texcoords werden verwendet, um das Rauschen einer Textur (Zeile 2) abzutasten. Nachdem wir den Rauschwert aus der Textur erhalten haben, interpolieren wir zwischen den Min / Max-Werten (normalerweise 0 und 1). Dann führen wir Multiplikationen aus, z. B. auf der Koordinate der Textur V (denken Sie daran, dass die Koordinate V von 1 nach 0 geht?) - Zeile 5. Wir haben also 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 (Leuchtturm, Berge ...) verschwunden sind. Dies geschah, weil der Zylinder den Tiefentest bestanden hat - der Zylinder befindet sich nicht in der Fernebene und wird über diese Objekte gezogen:


Tiefentest

Wir möchten simulieren, dass der Regenvorhang eingeschaltet ist (aber nicht unbedingt in der Fernebene). Dazu berechnen wir eine andere Maske, die "Maske entfernter Objekte".

Sie wird nach folgender Formel berechnet:

farObjectsMask = saturate( (FrustumDepth - CylinderWorldSpaceHeight) * 0.001 );

(0,001 wird aus dem Puffer genommen), wodurch die erforderliche Maske erhalten wird:


(In dem Abschnitt über den Effekt "Scharfzeichnen" habe ich bereits oberflächlich erklärt, wie die Tiefe der Sichtbarkeitspyramide aus dem Tiefenpuffer abgerufen wird.)

Ich persönlich glaube, dass dieser Effekt kostengünstiger implementiert werden könnte, ohne die Höhe im Weltraum zu berechnen, 0,0004.

Durch das Multiplizieren beider Masken erhält man das Finale:


Nachdem wir diese letzte Maske (Zeile 16) erhalten haben, führen wir eine weitere Interpolation durch, tun so gut wie nichts (zumindest im getesteten Fall) und multiplizieren dann die letzte Maske mit der Vorhangfarbe (Zeile 19) und führen eine Gammakorrektur durch (Zeile 20) -22) und letzte Multiplikationen (23-24).

Am Ende geben wir eine Farbe mit dem Alpha-Wert Null zurück. Dies geschieht, weil das Mischen in diesem Gang aktiviert ist:

FinalColor = SourceColor * 1.0 + (1.0 - SourceAlpha) * DestColor

Wenn Sie nicht ganz

sicher sind, wie das Mischen funktioniert, eine kurze Erklärung: SourceColor ist die RGB-Ausgabe des Pixel-Shader und DestColor ist die aktuelle RGB-Farbe des Pixels im Renderziel . Da SourceAlpha immer gleich 0,0, reduziert sich die obige Gleichung zu: FinalColor = SourceColor + DestColor.

Einfach ausgedrückt, hier wird additiv gemischt. Wenn der Pixel-Shader (0, 0, 0) zurückkehrt, bleibt die Farbe gleich.

Hier ist der fertige HLSL-Code - ich denke, nach der Erklärung wird es viel einfacher zu verstehen sein: Ich kann mit Freude sagen, dass mein Pixel-Shader denselben Assembler-Code wie im Original erzeugt. Ich hoffe der Artikel hat dir 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 );
}





Jetzt auch beliebt: