Das Reverse Engineering des Renderings von The Witcher 3

Vor kurzem begann ich mich mit dem Rendern von The Witcher 3 zu beschäftigen. Dieses Spiel hat erstaunliche Rendering-Techniken. Darüber hinaus ist sie in Bezug auf Handlung / Musik / Gameplay großartig.



In diesem Artikel werde ich über die Lösungen sprechen, die zum Rendern von The Witcher 3 verwendet werden. Es wird zumindest vorerst nicht so umfassend sein wie die Analyse von GTA V- Grafiken durch Adrian Correger.

Wir werden mit dem Reverse Engineering der Tonkorrektur beginnen.

Teil 1: Tonkorrektur


In den meisten modernen AAA-Spielen ist einer der Rendering-Schritte notwendigerweise die Tonkorrektur.

Ich möchte Sie daran erinnern, dass es im wirklichen Leben einen ziemlich großen Helligkeitsbereich gibt, während dieser auf Computerbildschirmen sehr begrenzt ist (8 Bit pro Pixel, was 0-255 ergibt). Hier hilft Tonemapping, sodass Sie einen größeren Bereich in ein begrenztes Beleuchtungsintervall einpassen können. In diesem Prozess gibt es normalerweise zwei Datenquellen: ein HDR-Bild mit einem Gleitkomma, dessen Farbwerte 1,0 überschreiten, und die durchschnittliche Beleuchtung der Szene (letztere kann auf verschiedene Arten berechnet werden, auch unter Berücksichtigung der Anpassung des Auges, um das Verhalten des menschlichen Auges zu simulieren, spielt hier jedoch keine Rolle).

Der nächste (und letzte) Schritt besteht darin, die Verschlusszeit zu ermitteln, die Farbe mit der Verschlusszeit zu berechnen und anhand der Tonkorrekturkurve zu verarbeiten. Und hier wird alles ziemlich verwirrend, weil neue Konzepte auftauchen, wie der „weiße Punkt“ (weißer Punkt) und das „mittlere Grau“ (mittleres Grau). Es gibt mindestens einige beliebte Kurven, von denen einige in Matt Pettineos A Closer Look at Tone Mapping behandelt werden .

Ehrlich gesagt hatte ich immer Probleme mit der korrekten Implementierung der Tonkorrektur in meinem eigenen Code. Es gibt zumindest ein paar verschiedene Beispiele online, die mir bis zu einem gewissen Grad nützlich waren. Einige von ihnen berücksichtigen HDR-Helligkeit / Weißpunkt / mittleres Grau, andere nicht - daher helfen sie nicht wirklich. Ich wollte eine "kampferprobte" Implementierung finden.

Wir werden in RenderDoc mit der Erfassung dieses Rahmens einer der Hauptquests von Novigrad arbeiten. Alle Einstellungen sind maximal:


Nachdem ich ein bisschen gesucht hatte, fand ich einen Draw Call für die Tonkorrektur! Wie oben erwähnt, gibt es einen Puffer mit HDR-Farben (Texturnummer 0, volle Auflösung) und der durchschnittlichen Helligkeit der Szene (Texturnummer 1, 1x1, Gleitkomma, zuvor vom Compute Shader berechnet).


Werfen wir einen Blick auf den Assembler-Code für den Pixel-Shader:

ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed 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 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret 

Es gibt einige erwähnenswerte Punkte. Erstens muss die geladene Helligkeit nicht der verwendeten entsprechen, da sie innerhalb der von den Künstlern ausgewählten Werte (aus dem konstanten Puffer) begrenzt ist (max / min-Aufrufe). Dies ist praktisch, da Sie zu lange oder zu lange Verschlusszeiten vermeiden können. Dieser Schritt scheint ziemlich alltäglich zu sein, aber ich habe ihn noch nie zuvor gemacht. Zweitens erkennt jemand, der mit Tonkorrekturkurven vertraut ist, diesen „11,2“ -Wert sofort, da dies tatsächlich der Wert des Weißpunkts aus John Hables Uncharted2-Tonkorrekturkurve ist.

AF-Parameter werden aus dem Puffer geladen.

Wir haben also drei weitere Parameter: cb3_v16.x, cb3_v16.y, cb3_v16.z. Wir können ihre Bedeutung untersuchen:


Meine Vermutung:

Ich glaube, dass „x“ eine Art „weiße Skala“ oder mittelgrau ist, weil es mit 11,2 multipliziert wird (Zeile 4) und danach als Zähler bei der Berechnung der Verschlusszeiteinstellung (Zeile 10) verwendet wird.

"Y" - Ich habe es den "u2-Zählerfaktor" genannt, und bald werden Sie sehen, warum.

"Z" ist der "Potenzierungsparameter", da er im Dreifachprotokoll / mul / exp verwendet wird (tatsächlich bei der Potenzierung).

Aber behandeln Sie diese Variablennamen mit einer gewissen Skepsis!

Auch:

cb3_v4.yz - Min / Max-Werte der zulässigen Helligkeit,
cb3_v7.xyz - AC-Parameter der Uncharted2-Kurve,
cb3_v8.xyz - DF-Parameter der Uncharted2-Kurve.

Kommen wir nun zum schwierigen Teil - wir werden einen HLSL-Shader schreiben, der uns genau den gleichen Assembler-Code gibt.

Dies kann sehr schwierig sein, und je länger der Shader ist, desto schwieriger ist die Aufgabe. Glücklicherweise habe ich vor einiger Zeit ein Tool geschrieben, mit dem ich schnell nach hlsl-> asm suchen kann.

Sehr geehrte Damen und Herren, willkommen bei D3DShaderDisassembler!


Nachdem ich mit dem Code experimentiert hatte, bekam ich die vorgefertigte HLSL- Tonwertkorrektur The Witcher 3 :

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); } 

Ein Screenshot von meinem Dienstprogramm, um dies zu bestätigen:


Voila!

Ich glaube, dies ist eine ziemlich genaue Implementierung der TW3-Tonkorrektur, zumindest in Bezug auf den Assembler-Code. Ich habe es bereits in meinem Framework angewendet und es funktioniert großartig!

Ich sagte "genug", weil ich keine Ahnung habe, warum der Nenner in ToneMapU2Func bei Null maximal wird. Wenn Sie durch 0 teilen, sollten Sie undefiniert werden?

Dies könnte beendet werden, aber fast zufällig fand ich in diesem Rahmen eine andere Version des TW3-Ton-Shaders, der für einen wunderschönen Sonnenuntergang verwendet wurde (es ist interessant, dass er mit minimalen Grafikeinstellungen verwendet wird!).


Lass es uns überprüfen. Zunächst der Assembler-Code für den Shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed 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 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret 

Auf den ersten Blick mag der Code einschüchternd aussehen, aber tatsächlich ist nicht alles so schlecht. Nach einer kurzen Analyse werden Sie feststellen, dass die Funktion Uncharted2 zwei Aufrufe mit unterschiedlichen Eingabedatensätzen (AF, min / max Helligkeit ...) enthält. Ich habe noch nie eine solche Entscheidung gesehen.

Und HLSL:

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); } 

Das heißt, wir haben zwei Sätze von Steuerparametern, wir berechnen zwei Farben mit Tonkorrektur und am Ende interpolieren wir sie. Kluge Entscheidung!

Teil 2: Augenanpassung


Der zweite Teil wird viel einfacher sein.

Im ersten Teil habe ich gezeigt, wie die Tonkorrektur in TW3 durchgeführt wird. Ich erläuterte kurz den theoretischen Hintergrund und erwähnte kurz die Anpassung des Auges. Und weißt du was? In diesem Teil werde ich darüber sprechen, wie diese Anpassung des Auges realisiert wird.

Aber warte, was ist Augenanpassung und warum brauchen wir sie? Wikipedia weiß alles darüber, aber ich erkläre: Stellen Sie sich vor, Sie befinden sich in einem dunklen Raum (denken Sie daran, dass das Leben seltsam ist) oder in einer Höhle und gehen Sie nach draußen, wo es hell ist. Beispielsweise kann die Hauptbeleuchtungsquelle die Sonne sein.

Im Dunkeln werden unsere Pupillen erweitert, so dass mehr Licht durch sie in die Netzhaut gelangt. Wenn es hell wird, nehmen unsere Pupillen ab und manchmal schließen wir unsere Augen, weil es „weh tut“.

Diese Änderung erfolgt nicht sofort. Das Auge muss sich an Helligkeitsänderungen anpassen. Deshalb führen wir eine Augenanpassung beim Echtzeit-Rendering durch.

Ein gutes Beispiel dafür, dass ein Mangel an Augenanpassung erkennbar ist, ist das HDRToneMappingCS11 des DirectX SDK. Scharfe Änderungen mittlerer Helligkeit sind eher unangenehm und unnatürlich.

Fangen wir an! Aus Gründen der Konsistenz werden wir denselben Rahmen von Novigrad analysieren.


Jetzt werden wir uns eingehend mit dem Frame-Capture-Programm RenderDoc befassen. Die Anpassung des Auges erfolgt normalerweise unmittelbar vor der Tonwertkorrektur, und The Witcher 3 ist keine Ausnahme.


Schauen wir uns den Status des Pixel-Shaders an:


Wir haben zwei Eingabequellen - 2 Texturen, R32_FLOAT, 1x1 (ein Pixel). textur0 enthält die durchschnittliche Helligkeit der Szene aus dem vorherigen Bild. textur1 enthält die durchschnittliche Helligkeit der Szene aus dem aktuellen Bild (berechnet unmittelbar vor diesem Compute-Shader - ich habe dies blau markiert).

Es wird erwartet, dass es einen Ausgang gibt - R32_FLOAT, 1x1. Schauen wir uns den Pixel-Shader an.

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret 

Wow, wie einfach! Nur 7 Zeilen Assembler-Code. Was ist hier los? Ich werde jede Zeile erklären:

0) Ermitteln Sie die durchschnittliche Helligkeit des aktuellen Frames.
1) Ermitteln Sie die durchschnittliche Helligkeit des vorherigen Frames.
2) Führen Sie eine Überprüfung durch: Ist die aktuelle Helligkeit kleiner oder gleich der Helligkeit des vorherigen Frames?
Wenn ja, nimmt die Helligkeit ab, wenn nicht, nimmt die Helligkeit zu.
3) Berechnen Sie die Differenz: Differenz = currentLum - previousLum.
4) Diese bedingte Übertragung (movc) weist dem konstanten Puffer einen Geschwindigkeitsfaktor zu. Abhängig vom Ergebnis der Prüfung können ab Zeile 2 zwei verschiedene Werte zugewiesen werden. Dies ist ein kluger Schachzug, da Sie auf diese Weise unterschiedliche Anpassungsgeschwindigkeiten erhalten können, um die Helligkeit zu verringern und zu erhöhen. Im untersuchten Rahmen sind beide Werte jedoch gleich und variieren von 0,11 bis 0,3.
5) Die endgültige Berechnung der angepassten Helligkeit: adaptierte Leuchtdichte = Geschwindigkeitsfaktor * Differenz + vorherige Leuchtdichte.
6) Das Ende des Shaders

Dies ist in HLSL ganz einfach implementiert:

  // The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; } 

Diese Zeilen geben uns den gleichen Assembler-Code. Ich würde nur vorschlagen, die Art der Ausgabe durch float4 durch float zu ersetzen . Keine Notwendigkeit für Bandbreitenverschwendung. So implementiert Witcher 3 die Augenanpassung. Ziemlich einfach, oder?

PS. Vielen Dank an Baldur Karlsson (Twitter: @baldurk ) für RenderDoc. Das Programm ist einfach toll.

Teil 3: chromatische Aberration


Chromatische Aberration ist ein Effekt, der hauptsächlich bei billigen Linsen auftritt. Dies tritt auf, weil Linsen unterschiedliche Brechungsindizes für unterschiedliche Längen des sichtbaren Lichts haben. Infolgedessen tritt eine sichtbare Verzerrung auf. Es gefällt jedoch nicht jedem. Glücklicherweise ist dieser Effekt in Witcher 3 sehr subtil und daher im Gameplay nicht ärgerlich (zumindest ich). Sie können es aber ausschalten, wenn Sie möchten.

Schauen wir uns ein Beispiel einer Szene mit und ohne chromatische Aberration genauer an:


Chromatische Aberration eingeschlossen


Chromatische Aberration deaktiviert

Bemerken Sie Unterschiede in der Nähe der Kanten? Ich auch nicht. Versuchen wir eine andere Szene:


Chromatische Aberration ist enthalten. Beachten Sie die leichte „rote“ Verzerrung im angegebenen Bereich.

Ja, viel besser! Hier ist der Kontrast zwischen den dunklen und hellen Bereichen stärker und in der Ecke sehen wir eine leichte Verzerrung. Wie Sie sehen können, ist dieser Effekt sehr schwach. Ich habe mich jedoch gefragt, wie es implementiert wird. Kommen wir zum merkwürdigsten Teil: dem Code!

Implementierung

Als erstes müssen Sie mit einem Pixel-Shader den richtigen Draw-Aufruf finden. Tatsächlich ist die chromatische Aberration Teil des großen Pixel-Shaders „Nachbearbeitung“, der aus chromatischer Aberration, Vignettierung und Gammakorrektur besteht. All dies befindet sich in einem einzelnen Pixel-Shader. Schauen wir uns den Assembler-Code für den Pixel-Shader genauer an:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ... 

Und zu den Pufferwerten:


Versuchen wir also zu verstehen, was hier passiert. Tatsächlich ist cb3_v17.xy das Zentrum der chromatischen Aberration, daher berechnen die ersten Zeilen den 2d-Vektor von den Texelkoordinaten (cb3_v17.zw = Kehrwert der Größe des Ansichtsfensters) zum „Zentrum der chromatischen Aberration“ und seiner Länge und führen dann andere Berechnungen, Überprüfungen und Verzweigungen durch . Bei Anwendung der chromatischen Aberration berechnen wir die Verschiebungen anhand bestimmter Werte aus dem konstanten Puffer und verzerren die Kanäle R und G. Im Allgemeinen ist der Effekt umso stärker, je näher die Ränder des Bildschirms sind. Zeile 10 ist ziemlich interessant, weil dadurch die Pixel näher kommen, insbesondere wenn wir die Aberration übertreiben. Gerne teile ich Ihnen meine Erkenntnis der Wirkung mit. Nehmen Sie wie üblich Variablennamen mit einem (soliden) Anteil an Skepsis. Beachten Sie, dass der Effekt vor der Gammakorrektur angewendet wird.

  void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } } 

Ich habe "fChromaticAberrationIntensity" hinzugefügt, um die Größe des Offsets und damit die Stärke des Effekts zu erhöhen, wie der Name schon sagt (TW3 = 1.0). Intensität = 40:


Das ist alles! Hoffe dir hat dieser Teil gefallen.

Teil 4: Vignettierung


Vignettierung ist einer der häufigsten Nachbearbeitungseffekte in Spielen. Er ist auch in der Fotografie beliebt. Leicht schattierte Ecken können einen schönen Effekt erzielen. Es gibt verschiedene Arten der Vignettierung. Beispielsweise verwendet Unreal Engine 4 natural. Aber zurück zu The Witcher 3. Klicken Sie hier , um einen interaktiven Vergleich von Frames mit und ohne Vignettierung zu sehen. Der Vergleich stammt aus dem NVIDIA-Leistungshandbuch für The Witcher 3 .


Screenshot von "The Witcher 3" mit aktivierter Vignettierung.

Beachten Sie, dass die obere linke Ecke (Himmel) nicht so schattiert ist wie die anderen Teile des Bildes. Später werden wir darauf zurückkommen.

Implementierungsdetails

Erstens gibt es einen kleinen Unterschied zwischen der Vignettierung in der Originalversion von The Witcher 3 (die am 19. Mai 2015 veröffentlicht wurde) und in The Witcher 3: Blood and Wine. Im ersten Fall wird der „inverse Gradient“ innerhalb des Pixel-Shaders berechnet, und im zweiten Fall wird er in eine 256 x 256 2D-Textur vorberechnet:


Textur 256x256, verwendet als "umgekehrter Gradient" in der Ergänzung "Blut und Wein".

Ich werde den Shader von "Blood and Wine" verwenden (übrigens ein großartiges Spiel). Wie in den meisten anderen Spielen wird die Witcher 3-Vignettierung im Pixel-Shader der endgültigen Nachbearbeitung berechnet. Schauen Sie sich den Assembler-Code an:

  ... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ... 

Interessant! Es scheint, dass sowohl Gamma (Linie 46) als auch lineare Räume (Linie 51) zur Berechnung der Vignettierung verwendet werden. In Zeile 48 wird die Textur des "inversen Gradienten" abgetastet. cb3 [9] .xyz hat nichts mit Vignettierung zu tun. In jedem überprüften Frame wird ihm der Wert float3 (1.0, 1.0, 1.0) zugewiesen, dh es handelt sich wahrscheinlich um den endgültigen Filter, der bei Ein- / Ausblendeffekten verwendet wird. Es gibt drei Hauptparameter für die Vignettierung in TW3:

  • Opazität (cb3 [6] .w) - beeinflusst die Stärke der Vignettierung. 0 - keine Vignettierung, 1 - maximale Vignettierung. Nach meinen Beobachtungen beträgt sie in der Basis The Witcher 3 ungefähr 1,0, während sie in Blood and Wine um 0,15 schwankt.
  • Farbe (cb3 [7] .xyz) - Ein hervorragendes Merkmal der TW3-Vignettierung ist die Möglichkeit, ihre Farbe zu ändern. Es muss nicht schwarz sein, aber in der Praxis ... Normalerweise hat es die Werte float3 (3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0) usw. - im allgemeinen Fall sind dies Vielfache von 0.00392156 = 1.0 / 255.0
  • Gewichte (cb3 [6] .xyz) ist ein sehr interessanter Parameter. Ich habe immer "flache" Vignetten gesehen, zum Beispiel:



Typische Vignettierungsmaske

Mit Gewichten (Zeile 52) können Sie jedoch sehr interessante Ergebnisse erzielen:


TW3-Vignettierungsmaske, berechnet anhand von Gewichten

Die Gewichte liegen nahe bei 1,0. Schauen Sie sich die Pufferdaten der Konstanten für ein Bild aus Blood and Wine (eine magische Welt mit einem Regenbogen) an: Aus diesem Grund hat die Vignettierung die hellen Pixel des Himmels oben nicht beeinflusst.


Code

Hier ist meine Implementierung der TW3-Vignettierung auf HLSL.

GammaToLinear = pow (Farbe, 2.2)

  /* // The Witcher 3 vignette. // // Input color is in gamma space // Output color is in gamma space as well. */ float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; } 

Ich hoffe es hat euch gefallen. Sie können auch meinen HLSLexplorer ausprobieren , der mir beim Verständnis des HLSL-Assembler-Codes sehr geholfen hat.

Nehmen Sie nach wie vor die Namen von Variablen mit einem gewissen Maß an Skepsis - TW3-Shader werden von D3DStripShader verarbeitet, sodass ich fast nichts über sie weiß, kann ich nur raten. Außerdem übernehme ich keine Verantwortung für den Schaden, der durch diesen Shader an Ihrer Ausrüstung verursacht wird;)

Bonus: Berechnung des Gradienten

In The Witcher 3, veröffentlicht im Jahr 2015, wurde der inverse Gradient im Pixel-Shader berechnet, anstatt eine vorberechnete Textur abzutasten. Schauen Sie sich den Assembler-Code an:

  35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000) 

Zum Glück ist es ganz einfach. Auf HLSL sieht es ungefähr so ​​aus:

  float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; } 

Das heißt, wir berechnen einfach den Abstand vom Zentrum zum Textil, zaubern damit (Multiplikation, Sättigung ...) und berechnen dann ... das Polynom! Genial.



Teil 5: die Wirkung der Vergiftung


Mal sehen, wie das Spiel „The Witcher 3: Wild Hunt“ den Effekt der Vergiftung umsetzt. Wenn Sie es noch nicht gespielt haben, lassen Sie alles fallen, kaufen und spielen Sie, sehen Sie sich ein Video an:

Abend:



Nacht:


Erstens sehen wir ein doppeltes und wirbelndes Bild, das oft entsteht, wenn Sie im wirklichen Leben trinken. Je weiter ein Pixel von der Bildmitte entfernt ist, desto stärker ist der Rotationseffekt. Ich habe absichtlich das zweite Video mit der Nacht gepostet, weil Sie diese Drehung auf den Sternen deutlich sehen können (siehe 8 separate Punkte?).

Der zweite Teil des Vergiftungseffekts, der möglicherweise nicht sofort erkennbar ist, ist eine geringfügige Änderung des Zooms. Es fällt in der Nähe des Zentrums auf.

Es ist wahrscheinlich offensichtlich, dass dieser Effekt eine typische Nachbearbeitung ist (Pixel Shader). Die Position in der Rendering-Pipeline ist jedoch möglicherweise nicht so offensichtlich. Es stellt sich heraus, dass der Effekt der Vergiftung unmittelbar nach der Tonwertkorrektur und unmittelbar vor der Bewegungsunschärfe angewendet wird (das „betrunkene“ Bild ist die Eingabe für Bewegungsunschärfe).

Beginnen wir die Spiele mit Assembler-Code:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Hier werden zwei separate konstante Puffer verwendet. Lassen Sie uns ihre Werte überprüfen:



Wir sind an einigen von ihnen interessiert:

cb0_v0.x -> verstrichene Zeit (in Sekunden)
cb0_v1.xyzw - Ansichtsfenstergröße und der Kehrwert der Ansichtsfenstergröße (auch als „Pixelgröße“ bezeichnet)
cb3_v0.x - Drehung um ein Pixel hat immer den Wert 1,0.
cb3_v0.y - das Ausmaß der Wirkung der Vergiftung. Nach dem Einschalten arbeitet es nicht mit voller Stärke, sondern steigt allmählich von 0,0 auf 1,0 an.
cv3_v1.xy - Pixel-Offsets (mehr dazu weiter unten). Dies ist ein Sin / Cos-Paar, sodass Sie Sincos (Zeit) im Shader verwenden können, wenn Sie dies wünschen.
cb3_v2.xy ist das Zentrum des Effekts, normalerweise float2 (0,5, 0,5).
Hier möchten wir uns darauf konzentrieren, zu verstehen, was passiert, und den Shader nicht nur blind umschreiben.

Wir werden mit den ersten Zeilen beginnen:

  ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 

Ich nenne Zeile 0 "Zoomverhältnis" und Sie werden bald sehen, warum. Unmittelbar danach (Zeile 1) berechnen wir die „Rotationsverschiebung“. Dies ist nur ein eingegebenes Sin / Cos-Datenpaar multipliziert mit 0,05.

Zeilen 2-4: Zuerst berechnen wir den Vektor vom Zentrum des Effekts zu den UV-Koordinaten der Textur. Dann berechnen wir das Quadrat aus Abstand (3) und einfachem Abstand (4) (von der Mitte zum Texel).

Texturkoordinaten zoomen


Schauen wir uns den folgenden Assembler-Code an:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 

Da sie auf diese Weise verpackt sind, können wir nur ein Paar Schwimmer analysieren.

Für den Anfang sind r0.yz "Rotationsversätze", r1.z ist der Abstand von der Mitte zum Texel, r1.xy ist der Vektor von der Mitte zum Texel, r0.x ist der "Zoomfaktor".

Um dies zu verstehen, nehmen wir zunächst an, dass zoomFactor = 1.0 ist, dh Sie können Folgendes schreiben:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center; 

Aber zoomFactor = 1.0:

  r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter; 

Ähnliches gilt für r3.xy:

  10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center 


Aber zoomFactor = 1.0:

r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter

Großartig.Das heißt, im Moment haben wir im Wesentlichen den aktuellen TextureUV (Texel) ± Rotationsversatz, aber was ist mit zoomFactor? Schauen Sie sich Zeile 0 an. Tatsächlich ist zoomFactor = 1,0 - 0,1 * betrunkenAmount. Für den maximalen betrunkenen Betrag sollte der Wert von zoomFactor 0,9 betragen, und die Texturkoordinaten mit zoom werden jetzt wie folgt berechnet:

  baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter 

Vielleicht wäre eine solche Erklärung intuitiver: Es ist einfach eine lineare Interpolation um einen Faktor zwischen den normalisierten Texturkoordinaten und dem Zentrum. Dies ist ein vergrößertes Bild. Um dies zu verstehen, ist es am besten, mit den Werten zu experimentieren. Hier ist ein Link zu Shadertoy, wo Sie den Effekt in Aktion sehen können.

Texturversatz


Das ganze Fragment im Assembler-Code:

  2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset 

erzeugt einen bestimmten Gradienten, nennen wir ihn die "Verschiebungsintensitätsmaske". In der Tat gibt es zwei Bedeutungen. Das erste ist in r0.w (wir werden es später verwenden) und das zweite ist 5-mal stärker in r0.x (Zeile 15). Letzteres dient tatsächlich als Faktor für die Texelgröße, so dass es die Vorspannungskraft beeinflusst.

Rotationsbezogene Abtastung

Als nächstes wird eine Reihe von Texturabtastungen durchgeführt. Tatsächlich werden 2 Serien von 8 Proben verwendet, eine für jede „Seite“. In HLSL können Sie dies wie folgt schreiben:

  static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB; 

Der Trick besteht darin, dass wir baseTexcoordsA / B einen zusätzlichen Versatz hinzufügen, der auf dem Einheitskreis liegt, multipliziert mit der zuvor erwähnten „Intensität der Verschiebung der Texturkoordinaten“. Je weiter das Pixel von der Mitte entfernt ist, desto größer ist der Radius des Kreises um das Pixel - wir probieren es 8 Mal aus, was auf den Sternen deutlich sichtbar ist. PointsAroundPixel-Werte (Vielfache von 45 Grad):


Einzelkreis

Zoombezogene Abtastung

Der zweite Teil des Trunkenheitseffekts in The Witcher 3 ist das Zoomen mit Vergrößern und Verkleinern. Schauen wir uns den Assembler-Code an, der diese Aufgabe ausführt:

  56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 

Wir sehen, dass es drei separate Texturaufrufe gibt, dh drei verschiedene Texturkoordinaten. Lassen Sie uns analysieren, wie Texturkoordinaten daraus berechnet werden. Aber zuerst zeigen wir die Eingabe für diesen Teil:

  float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60 

Ein paar Worte zur Eingabe. Wir berechnen den Versatz in Texeln (z. B. 8,0 * Texelgröße), der dann zu den Basis-UV-Koordinaten addiert wird. Die Amplitude liegt einfach zwischen 0,98 und 1,02, um ein Gefühl für den Zoom zu vermitteln, ebenso wie zoomFactor in dem Teil, der die Drehung ausführt.

Beginnen wir mit dem ersten Paar - r1.xy (Zeile 61)

  r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude); 

Also:

 float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude); 

Lassen Sie uns das zweite Paar überprüfen - r3.xy (Zeile 62)

  r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV 

Also:

  float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates; 

Lassen Sie uns das dritte Paar überprüfen - r0.xy (Zeilen 63-64)

  r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV 

Also:

  float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates 

Alle drei Texturabfragen werden addiert und das Ergebnis im Register r1 gespeichert. Es ist erwähnenswert, dass dieser Pixel-Shader einen Sampler mit begrenzter Adressierung verwendet.

Alles zusammenfügen

Im Moment haben wir also das Ergebnis der Rotation im r2-Register und drei gefaltete Zoomanforderungen im r1-Register. Schauen wir uns die letzten Zeilen des Assembler-Codes an:

  70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Über zusätzliche Eingaben: r0.w stammt aus Zeile 7, dies ist unsere Intensitätsmaske, und cb3 [0] .y ist die Stärke des Intoxikationseffekts.

Mal sehen, wie es funktioniert. Mein erster Ansatz war Brute Force:

  float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor; 

Aber was solls, niemand schreibt solche Shader . Ich nahm einen Bleistift mit Papier und schrieb diese Formel:

  finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart 


Wobei t = effectAmount * IntensitätMask

Also erhalten wir:

  finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t ) 

Und wir kommen zu Folgendem:

  finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity); 

Ja, dieser Teil des Artikels erwies sich als sehr detailliert, aber wir waren endlich fertig! Persönlich habe ich beim Schreiben etwas gelernt, ich hoffe du auch!

Wenn Sie interessiert sind, legte die vollständige Quellcode in HLSL heraus hier . Ich habe sie mit meinem HLSLexplorer getestet , und obwohl es keine direkten Eins-zu-Eins-Entsprechungen mit dem ursprünglichen Shader gibt, sind die Unterschiede so gering (eine Zeile weniger), dass ich mit Sicherheit sagen kann, dass es funktioniert. Danke fürs Lesen!

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


All Articles