Wie das Rendering von The Witcher 3 implementiert wird: Blitz, das Flair des Hexers und andere Effekte

Bild

Teil 1. Reißverschlüsse


In diesem Teil werden wir uns den Prozess des Renderns von Blitzen in Witcher 3: Wild Hunt ansehen.

Das Blitz-Rendering wird etwas später als der Regenvorhang- Effekt ausgeführt, tritt jedoch immer noch im direkten Rendering-Durchgang auf. In diesem Video ist ein Blitz zu sehen:


Sie verschwinden sehr schnell, daher ist es besser, das Video mit einer Geschwindigkeit von 0,25 anzusehen.

Sie können sehen, dass dies keine statischen Bilder sind. Mit der Zeit ändert sich ihre Helligkeit leicht.

In Bezug auf das Rendern von Nuancen gibt es viele Ähnlichkeiten mit dem Zeichnen eines Regenvorhangs in der Ferne, z. B. dieselben Mischzustände (additive Mischung) und Tiefe (Überprüfung ist aktiviert, Tiefenaufzeichnung wird nicht durchgeführt).


Szene ohne Blitz


Blitzszene

In Bezug auf die Blitzgeometrie ist The Witcher 3 ein baumartiges Netz. Dieses Beispiel eines Blitzes wird durch das folgende Netz dargestellt:


Es hat UV-Koordinaten und normale Vektoren. All dies ist praktisch im Vertex-Shader-Stadium.

Vertex-Shader


Werfen wir einen Blick auf den zusammengestellten Vertex-Shader-Code:

vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret 

Es gibt viele Ähnlichkeiten mit dem Vertex-Shader-Regenvorhang, daher werde ich nicht wiederholen. Ich möchte Ihnen den wichtigen Unterschied in den Zeilen 11-18 zeigen:

  11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 

Erstens ist cb1 [8] .xyz die Position der Kamera und r2.xyz ist die Position im Weltraum, dh Zeile 11 berechnet den Vektor von der Kamera zur Position in der Welt. Dann berechnen die Zeilen 12-15 die Länge (worldPos - cameraPos) * 0,000001.

v2.xyz ist der Normalenvektor der eingehenden Geometrie. Zeile 16 erweitert es vom Intervall [0-1] bis zum Intervall [-1; 1].

Dann wird die endgültige Position in der Welt berechnet:

finalWorldPos = worldPos + Länge (worldPos - cameraPos) * 0,000001 * normalVector
Das HLSL-Code-Snippet für diese Operation sieht ungefähr so ​​aus:

  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output; 

Diese Operation führt zu einer kleinen "Explosion" des Netzes (in Richtung des Normalenvektors). Ich habe experimentiert, indem ich 0,000001 durch mehrere andere Werte ersetzt habe. Hier sind die Ergebnisse:


0,000002


0,000005


0,00001


0,000025

Pixel Shader


Nun, wir haben den Vertex-Shader herausgefunden, jetzt ist es Zeit, zum Assembler-Code für den Pixel-Shader zu gelangen!

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret 

Gute Nachricht: Der Code ist nicht so lang.

Schlechte Nachrichten:

  3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 

... worum geht es?

Ehrlich gesagt ist dies nicht das erste Mal, dass ich einen solchen Assembler-Code in den Witcher 3-Shadern sehe. Aber als ich ihn zum ersten Mal traf, dachte ich: "Was zum Teufel ist das?"

Ähnliches gilt für einige andere TW3-Shader. Ich werde meine Abenteuer mit diesem Fragment nicht beschreiben und nur sagen, dass die Antwort im ganzzahligen Rauschen liegt :

  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } 

Wie Sie sehen können, wird es im Pixel-Shader zweimal aufgerufen. Anhand der Anleitungen auf dieser Website können wir verstehen, wie sanftes Rauschen korrekt implementiert wird. Ich werde gleich darauf zurückkommen.

Schauen Sie sich Zeile 0 an - hier animieren wir nach folgender Formel:

animation = elapsedTime * animationSpeed ​​+ TextureUV.x
Diese Werte werden nach zukünftiger Rundung auf die untere Seite ( Etage ) (Anweisung round_ni ) zu Eingabepunkten für ganzzahliges Rauschen. Normalerweise berechnen wir den Rauschwert für zwei Ganzzahlen und dann den endgültigen interpolierten Wert zwischen ihnen (Einzelheiten finden Sie auf der libnoise-Website).

Nun, dies ist ein ganzzahliges Rauschen, aber schließlich sind alle zuvor genannten Werte (auch abgerundet) float!

Beachten Sie, dass es hier keine ftoi- Anweisungen gibt . Ich gehe davon aus, dass die Programmierer von CD Projekt Red hier die interne Funktion HLSL asint verwendet haben , die die Konvertierung von Gleitkommawerten „reinterpret_cast“ durchführt und diese als ganzzahliges Muster behandelt.

Das Interpolationsgewicht für die beiden Werte wird in den Zeilen 10-11 berechnet.

interpolationWeight = 1.0 - frac (Animation);
Dieser Ansatz ermöglicht es uns, zwischen Werten über die Zeit zu interpolieren.

Um ein gleichmäßiges Rauschen zu erzeugen, wird dieser Interpolator an die SCurve- Funktion übergeben:

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } 


Smoothstep-Funktion [libnoise.sourceforge.net]

Diese Funktion wird als "Smoothstep" bezeichnet. Wie Sie dem Assembler-Code entnehmen können, handelt es sich hierbei nicht um eine interne Smoothstep- Funktion von HLSL. Eine interne Funktion wendet Einschränkungen an, damit die Werte wahr sind. Da wir jedoch wissen, dass interpolationWeight immer im Bereich [0-1] liegt, können diese Überprüfungen sicher übersprungen werden.

Bei der Berechnung des Endwerts werden mehrere Multiplikationsoperationen verwendet. Sehen Sie, wie sich die endgültige Alpha-Ausgabe abhängig vom Rauschwert ändern kann. Dies ist praktisch, da es die Deckkraft des gerenderten Blitzes beeinflusst, genau wie im wirklichen Leben.

Bereit für den Pixel-Shader:

  cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); } 

Zusammenfassend


In diesem Teil habe ich in The Witcher 3 eine Möglichkeit beschrieben, Blitze zu rendern.

Ich freue mich sehr, dass der Assembler-Code, der aus meinem Shader stammt, vollständig mit dem Original übereinstimmt!


Teil 2. Dumme Himmels-Tricks


Dieser Teil unterscheidet sich geringfügig von den vorherigen. Darin möchte ich Ihnen einige Aspekte des Sky Shader Witcher 3 zeigen.

Warum "dumme Tricks" und nicht der ganze Shader? Nun, es gibt mehrere Gründe. Erstens ist der Witcher 3 Sky Shader ein ziemlich komplexes Tier. Der Pixel-Shader aus der Version 2015 enthält 267 Zeilen Assembler-Code, und der Shader aus dem Blood and Wine-DLC enthält 385 Zeilen.

Darüber hinaus erhalten sie viele Eingaben, was für das Reverse Engineering des vollständigen (und lesbaren!) HLSL-Codes nicht sehr förderlich ist.

Deshalb habe ich beschlossen, nur einen Teil der Tricks dieser Shader zu zeigen. Wenn ich etwas Neues finde, werde ich den Beitrag ergänzen.

Die Unterschiede zwischen der Version 2015 und dem DLC (2016) sind sehr deutlich. Insbesondere enthalten sie Unterschiede in der Berechnung von Sternen und deren Flimmern, eine andere Herangehensweise an das Rendern der Sonne ... Der Blut- und Wein- Shader berechnet sogar die Milchstraße bei Nacht.

Ich werde mit den Grundlagen beginnen und dann über dumme Tricks sprechen.

Die Grundlagen


Wie die meisten modernen Spiele verwendet Witcher 3 Skydome, um den Himmel zu modellieren. Schauen Sie sich die dafür verwendete Halbkugel in Witcher 3 (2015) an. Hinweis: In diesem Fall liegt der Begrenzungsrahmen dieses Netzes im Bereich von [0,0,0] bis [1,1,1] (Z ist die nach oben weisende Achse) und weist gleichmäßig verteilte UV-Strahlen auf. Später benutzen wir sie.


Die Idee hinter Skydome ähnelt der Idee von Skybox (der einzige Unterschied ist das verwendete Netz). Im Vertex-Shader-Stadium transformieren wir den Skydome relativ zum Betrachter (normalerweise entsprechend der Kameraposition), wodurch die Illusion entsteht, dass der Himmel tatsächlich sehr weit entfernt ist - wir werden ihn niemals erreichen.

Wenn Sie die vorherigen Teile dieser Artikelserie lesen, wissen Sie, dass „The Witcher 3“ die inverse Tiefe verwendet, dh die ferne Ebene ist 0.0f und die nächste ist 1.0f. Damit die Skydome-Ausgabe vollständig in der Fernebene ausgeführt wird, setzen wir in den Parametern des Suchfensters MinDepth auf den gleichen Wert wie MaxDepth :


Klicken Sie hier (docs.microsoft.com), um zu erfahren, wie die Felder MinDepth und MaxDepth während der Konvertierung des Suchfensters verwendet werden.

Vertex-Shader


Beginnen wir mit dem Vertex-Shader. In Witcher 3 (2015) lautet der Assembler-Shader-Code wie folgt:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret 

In diesem Fall überträgt der Vertex-Shader nur Texcoords und eine Position im Weltraum an die Ausgabe. In Blood and Wine zeigt er auch einen normalisierten Normalenvektor an. Ich werde die Version 2015 in Betracht ziehen, weil sie einfacher ist.

Schauen Sie sich den als cb2 bezeichneten konstanten Puffer an:


Hier haben wir eine Matrix der Welt (einheitliche Skalierung um 100 und Übertragung relativ zur Kameraposition). Nichts kompliziertes. cb2_v4 und cb2_v5 sind die Skalierungs- / Abweichungsfaktoren, die zum Konvertieren der Scheitelpunktpositionen vom Intervall [0-1] in das Intervall [-1; 1] verwendet werden. Hier „komprimieren“ diese Koeffizienten jedoch die Z-Achse (nach oben).


In den vorherigen Teilen der Serie hatten wir ähnliche Vertex-Shader. Der allgemeine Algorithmus besteht darin, Texcoords weiter zu übertragen, dann wird die Position unter Berücksichtigung von Skalierungs- / Abweichungskoeffizienten berechnet, dann wird PositionW im Weltraum berechnet, dann wird die endgültige Position des Clipping-Raums durch Multiplikation von matWorld und matViewProj berechnet -> ihr Produkt wird verwendet, um mit Position zu multiplizieren, um die endgültige SV_Position zu erhalten .

Daher sollte die HLSL dieses Vertex-Shaders ungefähr so ​​aussehen:

  struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; } 

Vergleich meines Shaders (links) und des Originals (rechts):


Eine hervorragende Eigenschaft von RenderDoc ist, dass wir unseren eigenen Shader anstelle des ursprünglichen einfügen können. Diese Änderungen wirken sich auf die Pipeline bis zum Ende des Frames aus. Wie Sie dem HLSL-Code entnehmen können, habe ich verschiedene Optionen zum Zoomen und Transformieren der endgültigen Geometrie bereitgestellt. Sie können mit ihnen experimentieren und sehr lustige Ergebnisse erzielen:


Vertex Shader-Optimierung


Haben Sie das Problem des ursprünglichen Vertex-Shaders bemerkt? Die Scheitelpunktmultiplikation einer Matrix mit einer Matrix ist völlig redundant! Ich fand dies in mindestens einigen Vertex-Shadern (zum Beispiel im Shader ein Regenvorhang in der Ferne ). Wir können es optimieren, indem wir PositionW sofort mit matViewProj multiplizieren!

Wir können diesen Code also durch HLSL ersetzen:

  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); 

wie folgt:

  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW ); 

Die optimierte Version gibt uns den folgenden Assembler-Code:

  vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret 

Wie Sie sehen, haben wir die Anzahl der Anweisungen von 26 auf 12 reduziert - eine ziemlich bedeutende Änderung. Ich weiß nicht, wie weit verbreitet dieses Problem im Spiel ist, aber um Gottes willen, CD Projekt Red, veröffentlichen Sie vielleicht einen Patch? :) :)

Und ich scherze nicht. Sie können meinen optimierten Shader anstelle des ursprünglichen RenderDoc einfügen und Sie werden sehen, dass diese Optimierung keine visuellen Auswirkungen hat. Ehrlich gesagt verstehe ich nicht, warum CD Projekt Red beschlossen hat, eine Scheitelpunktmultiplikation einer Matrix mit einer Matrix durchzuführen ...

Die Sonne


In The Witcher 3 (2015) besteht die Berechnung der atmosphärischen Streuung und der Sonne aus zwei separaten Zeichnungsaufrufen:


Hexer 3 (2015) - Bis


Hexer 3 (2015) - mit dem Himmel


Hexer 3 (2015) - mit Himmel + Sonne

Das Rendering der Sonne in der Version 2015 ist dem Rendering des Mondes in Bezug auf Geometrie und Mischungs- / Tiefenzustände sehr ähnlich.

Andererseits wird in "Blut und Wein" der Himmel mit der Sonne in einem Durchgang gerendert:


The Witcher 3: Blut und Wein (2016) - Zum Himmel


The Witcher 3: Blut und Wein (2016) - mit Himmel und Sonne

Egal wie Sie die Sonne rendern, irgendwann benötigen Sie immer noch die (normalisierte) Richtung des Sonnenlichts. Der logischste Weg, diesen Vektor zu erhalten, ist die Verwendung von sphärischen Koordinaten . Tatsächlich benötigen wir nur zwei Werte, die zwei Winkel angeben (im Bogenmaß!): Phi und Theta . Nachdem wir sie erhalten haben, können wir annehmen, dass r = 1 ist , wodurch es reduziert wird. Dann können Sie für die kartesischen Koordinaten mit der Y-Achse nach oben den folgenden Code in HLSL schreiben:

  float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir); 

In der Regel wird die Richtung des Sonnenlichts in der Anwendung berechnet und dann zur zukünftigen Verwendung an den konstanten Puffer übergeben.

Nachdem wir die Richtung des Sonnenlichts erhalten haben, können wir tiefer in den Assembler-Code des Pixel-Shaders „Blood and Wine“ eintauchen ...

  ... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ... 

Erstens ist cb12 [0] .xyz die Kameraposition, und in r0.xyz speichern wir die Scheitelpunktposition (dies ist die Ausgabe vom Scheitelpunkt-Shader). Daher berechnet Zeile 100 den Vektor worldToCamera . Aber schauen Sie sich die Zeilen 105-107 an. Wir können sie als normalize (-worldToCamera) schreiben, d. H. Wir berechnen den normalisierten cameraToWorld- Vektor.

  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx 

Dann berechnen wir das Skalarprodukt der Vektoren cameraToWorld und sunDirection ! Denken Sie daran, dass sie normalisiert werden müssen. Wir sättigen diesen vollständigen Ausdruck auch, um ihn auf das Intervall [0-1] zu beschränken.

Großartig! Dieses Skalarprodukt ist in r1.x gespeichert. Mal sehen, wo es als nächstes gilt ...

  152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x 

Die Dreifaltigkeit "log, mul, exp" ist Potenzierung. Wie Sie sehen können, erhöhen wir unseren Kosinus (das Skalarprodukt normalisierter Vektoren) in gewissem Maße. Sie fragen sich vielleicht warum. Auf diese Weise können wir einen Farbverlauf erzeugen, der die Sonne nachahmt. (Und Zeile 155 wirkt sich auf die Deckkraft dieses Verlaufs aus, sodass wir ihn beispielsweise zurücksetzen, um die Sonne vollständig zu verbergen.) Hier einige Beispiele:


Exponent = 54


Exponent = 2400

Mit diesem Farbverlauf interpolieren wir zwischen skyColor und sunColor ! Um Artefakte zu vermeiden, müssen Sie den Wert in Zeile 120 sättigen.

Es ist erwähnenswert, dass dieser Trick verwendet werden kann, um die Kronen des Mondes zu simulieren (bei niedrigen Exponentenwerten). Dazu benötigen wir den moonDirection- Vektor, der leicht mit sphärischen Koordinaten berechnet werden kann.

Der vorgefertigte HLSL-Code sieht möglicherweise wie folgt aus:

  float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient ); 

Bewegung der Sterne


Wenn Sie den klaren Nachthimmel von Witcher 3 im Zeitraffer betrachten, können Sie sehen, dass die Sterne nicht statisch sind - sie bewegen sich ein wenig über den Himmel! Ich habe das fast zufällig bemerkt und wollte wissen, wie es umgesetzt wurde.

Beginnen wir mit der Tatsache, dass die Sterne in Witcher 3 als kubische Karte der Größe 1024x1024x6 dargestellt werden. Wenn Sie darüber nachdenken, können Sie verstehen, dass dies eine sehr praktische Lösung ist, mit der Sie leicht Anweisungen zum Abtasten einer kubischen Karte abrufen können.

Schauen wir uns den folgenden Assembler-Code an:

  159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0 

Um den endgültigen Abtastvektor (Zeile 173) zu berechnen, berechnen wir zunächst den normalisierten worldToCamera- Vektor (Zeilen 159-162).

Dann berechnen wir zwei Vektorprodukte (163-164, 165-166) mit moonDirection und später drei Skalarprodukte, um den endgültigen Abtastvektor zu erhalten. HLSL-Code:

  float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb; 

Hinweis für mich: Dies ist ein sehr gut gestalteter Code, und ich sollte ihn genauer untersuchen.

Hinweis für die Leser: Wenn Sie mehr über diesen Vorgang wissen, sagen Sie es mir!

Funkelnde Sterne


Ein weiterer interessanter Trick, den ich genauer untersuchen möchte, ist das Flackern von Sternen.Wenn Sie beispielsweise bei klarem Wetter durch Novigrad wandern, werden Sie feststellen, dass die Sterne funkeln.

Ich war gespannt, wie das umgesetzt wurde. Es stellte sich heraus, dass der Unterschied zwischen der Version 2015 und „Blood and Wine“ ziemlich groß ist. Der Einfachheit halber werde ich die Version 2015 betrachten.

Wir beginnen also direkt nach dem Abtasten von starsColor aus dem vorherigen Abschnitt:

  174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx 

Hm. .

starsColor 173 - offset . offset (r2.xyz, 235), , - (237-242) (243).

, ? , . offset . skydome — .

offset , , UV skydome (v0.xy) , (cb[0].x).

ishr/xor/and, .

, , , . , ( iadd ) ( reversebits ; bfrev ).

, . .

4 «» . , 4 :

  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B ); 

4 ( , itof ):

1 — r5.x,

2 — r4.w,

3 — r1.w,

4 — r5.y

itof ( 216) :

  217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 

S- UV, . Also:

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY ); 

, :

  229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb; 

offset :


Nach der Berechnung von starsColorDisturbed ist der schwierigste Teil abgeschlossen. Hurra!

Der nächste Schritt besteht darin, eine Gammakorrektur sowohl für starsColor als auch für starsColorDisturbed durchzuführen. Anschließend werden sie multipliziert:

  starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed; 

Sterne - der letzte Schliff


Wir haben StarsFinal in r1.xyz. Am Ende der Sternverarbeitung tritt Folgendes auf:

  256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000) 

Dies ist viel einfacher als funkelnde und sich bewegende Sterne.

Wir beginnen also damit, StarsFinal auf eine Potenz von 2,5 anzuheben - dies ermöglicht es uns, die Dichte der Sterne zu steuern. Ziemlich schlau. Dann machen wir die maximale Farbe der Sterne gleich float3 (1, 1, 1).

cb0 [9] .w wird verwendet, um die Gesamtsichtbarkeit von Sternen zu steuern. Daher können wir erwarten, dass dieser Wert tagsüber 1,0 (was eine Multiplikation mit Null ergibt) und nachts 0,0 beträgt.

Am Ende erhöhen wir die Sichtbarkeit von Sternen um 10. Und das war's!

Teil 3. Das Hexer-Flair (Objekte und Helligkeitskarte)


Fast alle zuvor beschriebenen Effekte und Techniken waren nicht wirklich mit Witcher 3 verbunden. Dinge wie Tonkorrektur, Vignettierung oder Berechnung der durchschnittlichen Helligkeit sind in fast jedem modernen Spiel vorhanden. Sogar die Wirkung einer Vergiftung ist weit verbreitet.

Deshalb habe ich mich entschlossen, die Rendermechanik des „Hexerinstinkts“ genauer zu betrachten. Geralt ist ein Hexer, und deshalb sind seine Gefühle viel schärfer als die eines gewöhnlichen Menschen. Folglich kann er mehr sehen und hören als andere Menschen, was ihm bei seinen Ermittlungen sehr hilft. Die Flairmechanik des Hexers ermöglicht es dem Spieler, solche Spuren zu visualisieren.

Hier ist eine Demonstration des Effekts:


Und noch eine mit besserer Beleuchtung:


Wie Sie sehen können, gibt es zwei Arten von Objekten: diejenigen, mit denen Geralt interagieren kann (gelber Umriss) und die mit der Untersuchung verbundenen Spuren (roter Umriss). Nachdem Geralt die rote Spur untersucht hat, kann sie sich in Gelb verwandeln (erstes Video). Beachten Sie, dass der gesamte Bildschirm grau wird und ein Fischaugeneffekt (zweites Video) hinzugefügt wird.

Dieser Effekt ist ziemlich kompliziert, deshalb habe ich beschlossen, seine Forschung in drei Teile zu unterteilen.

Im ersten werde ich über die Auswahl von Objekten sprechen, im zweiten - über die Erzeugung der Kontur und im dritten - über die endgültige Vereinigung all dessen zu einem Ganzen.

Wählen Sie Objekte


, , . Witcher 3 -. GBuffer, „“ (), stencil = 8. , „“ , stencil = 4.

, -:



-


Schablonenpuffer werden häufig in Spielen verwendet, um Netze zu markieren. Bestimmten Kategorien von Netzen wird dieselbe ID zugewiesen.

Die Idee ist, die Always- Funktion mit dem Operator Ersetzen zu verwenden , wenn der Schablonentest erfolgreich ist, und in allen anderen Fällen mit dem Operator Behalten .

So wird es mit D3D11 implementiert:

  D3D11_DEPTH_STENCIL_DESC depthstencilState; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue ); 

Der in den Puffer zu schreibende Stensilwert wird im API-Aufruf als StencilRef übergeben :

  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... ); 

Helligkeit rendern


R11G11B10_FLOAT, R G.

? , , , .

:



, .

: „“, — :


— :


— :


, , ? -!

-, , „8“ ( ) „4“.

- :


… :


? - . - :

  if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel 

wo:
StencilRef — , API,

StencilReadMask — , (, , ),

OP — , API,

StencilValue — - .

, AND.

, , :







! , ReadMask. ! -:

  Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE 

Clever. Wie Sie sehen, vergleichen wir in diesem Fall nicht den Schablonenwert, sondern prüfen, ob ein bestimmtes Bit des Schablonenpuffers gesetzt ist. Jedes Pixel des Schablonenpuffers hat das Format uint8, sodass das Werteintervall [0-255] beträgt.

Hinweis: Alle DrawIndexed (36) -Aufrufe beziehen sich auf das Rendern von Footprints als Footprints. In diesem speziellen Frame hat die Helligkeitskarte daher die folgende endgültige Form:


Vor dem Schablonentest gibt es jedoch einen Pixel-Shader. Sowohl 28738 als auch 28748 verwenden denselben Pixel-Shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret 

render target, 24-27 .

, — ( ), 1. ( 2-6).

(cb3[7].xyz — , !), ( 7-9).

:

— cb3[0].rgb — . float3(0, 1, 0) () float3(1, 0, 0) ( ),
— cb3[6].y — . .

. , .

color * intensity .

HLSL :

  struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; } 

() () .


. , .

4. ( )


:


Im ersten Teil der Analyse der Wirkung des Hexeninstinkts habe ich gezeigt, wie die „Helligkeitskarte“ erzeugt wird.

Wir haben eine Vollbild-Textur im Format R11G11B10_FLOAT, die folgendermaßen aussehen könnte:


Der grüne Kanal bedeutet "Fußabdrücke", die roten - interessanten Objekte, mit denen Geralt interagieren kann.

Nachdem wir diese Textur erhalten haben, können wir mit der nächsten Stufe fortfahren - ich nannte sie die „Konturkarte“.


Dies ist eine etwas seltsame Textur des 512x512 R16G16_FLOAT-Formats. Es ist wichtig, dass es im Stil von "Tischtennis" implementiert ist. Die Konturkarte aus dem vorherigen Frame sind die Eingabedaten (zusammen mit der Helligkeitskarte), um eine neue Konturkarte im aktuellen Frame zu generieren.

Ping-Pong-Puffer können auf viele Arten implementiert werden, aber ich persönlich mag Folgendes (Pseudocode) am meisten:

  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; } 

, [m_outlineIndex] , [!m_outlineIndex] , .

:

  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_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret 

, , , :

  0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 

floor( TextureUV * 2.0 ), :


:

  float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; } 

, 1.0 float2(0.0, 0.0).

. , texcoords float2(1, 0), float2(0, 1), — float2(1.0, 1.0).

Also:

  float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 ); 

mask , , . , mask.r mask.w :


mask.r


mask.w

Wir haben Maske , lass uns weitermachen . Zeile 15 tastet die Luminanzkarte ab. Beachten Sie, dass die Luminanztextur das Format R11G11B10_FLOAT hat, obwohl wir alle rgba-Komponenten abtasten. In dieser Situation wird angenommen, dass .a 1.0f ist.

Die für diesen Vorgang verwendeten Texcoords können als frac (TextureUV * 2.0) berechnet werden . Daher kann das Ergebnis dieser Operation beispielsweise folgendermaßen aussehen:


Sehen Sie die Ähnlichkeit?

Der nächste Schritt ist sehr klug - das Vier-Komponenten-Skalarprodukt (dp4) wird ausgeführt:

  16: dp4 r1.x, r1.xyzw, r2.xyzw 

( ), — ( ), — ( .w 1.0). . :


masterFilter , . , . — .

: (: 1.0/256.0!) :

  float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference); 

Wenn wir nun den Filter mit maxAbsDifference multiplizieren ...


Sehr einfach und effizient.

Nachdem wir die Konturen erhalten haben, probieren wir die Konturkarte aus dem vorherigen Frame aus.

Um einen „gespenstischen“ Effekt zu erzielen, nehmen wir einen Teil der Parameter, die für den aktuellen Durchgang berechnet wurden, und die Werte aus der Konturkarte.

Begrüßen Sie unseren alten Freund - ganzzahliges Rauschen. Er ist hier anwesend. Animationsparameter (cb3 [0] .zw) werden aus dem konstanten Puffer entnommen und ändern sich im Laufe der Zeit.

  float2 outlines = masterFilter * maxAbsDifference; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y 

: , [-1;1] ( -). TW3 , .

, ( 1.0/512.0), .x:

  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0; 

, , , :

  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0; 

Der nächste Schritt besteht darin, den Wert von der „alten“ Konturkarte mithilfe von Rauschen zu verzerren. Dies ist die Hauptlinie, die der Ausgabetextur ein blockartiges Gefühl verleiht.

Dann gibt es andere Berechnungen, nach denen ganz am Ende die „Dämpfung“ berechnet wird.

  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0); 

Hier ist ein kurzes Video, das eine Übersichtskarte in Aktion zeigt:


, . RenderDoc.

(, , ) , Witcher 3, RenderDoc !

: (. ) , .r . .g? , - „-“ — , .r .g + - .

5: (» " )


, : , , , . , .

. ! — . : , .

:



:


:


, , , « », ( ) , .

:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret 

82 — , !

:

  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x ); 

, — fisheyeAmount . , 0.0 1.0, . , , , fisheye ( ).

, — , :

  0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 

HLSL :

  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); } 

[-1; 1] UV . «». :


.

, «».

  22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 

Zunächst werden die "doppelten" Texturkoordinaten berechnet und die Subtraktion float2 (1, 1) durchgeführt:

  float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0); 

Ein solcher Texcoord kann wie folgt visualisiert werden:


Dann wird der skalare Produktpunkt (uv4, uv4) berechnet , der uns die Maske gibt:


welches verwendet wird, um mit den obigen Texcoords zu multiplizieren:


Wichtig: In der oberen linken Ecke (schwarze Pixel) sind die Werte negativ. Sie werden aufgrund der eingeschränkten Genauigkeit des Formats R11G11B10_FLOAT in Schwarz (0,0) angezeigt. Es hat kein Vorzeichenbit, daher können keine negativen Werte darin gespeichert werden.

Dann wird der Dämpfungskoeffizient berechnet (wie oben erwähnt, variiert fisheyeAmount von 0,0 bis 1,0).

  float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation; 

Dann werden die Einschränkung (max / min) und eine Multiplikation durchgeführt.

Somit wird der Offset berechnet. Um die endgültige UV zu berechnen, die zum

Abtasten der Farbtextur verwendet wird , führen wir einfach die Subtraktion durch: float2 colorUV = mainUv - offset;

Durch Abtasten der eingegebenen colorUV-Farbtextur erhalten wir ein verzerrtes Bild in der Nähe der Ecken:


Umrisse


Der nächste Schritt besteht darin, die Konturkarte abzutasten, um die Konturen zu finden. Es ist ziemlich einfach: Zuerst finden wir Texcoords, um die Konturen interessanter Objekte abzutasten, und dann machen wir dasselbe für die Tracks:

  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y 


Interessante Objekte aus der Konturkarte


Spuren aus der Konturkarte

Es ist erwähnenswert, dass wir nur den .x-Kanal aus der Konturkarte abtasten und nur die oberen Quadrate berücksichtigen.

Bewegung


, . 8 , .

, 8.0.

[0-1] 2 , 1 :


, , . 15-21. , (, - ). (15-21) (41-42):

  15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 

Wie Sie sehen können, betrachten wir nur Texel von [0,00 - 0,03] neben jeder Oberfläche, fassen ihre Werte zusammen, multiplizieren 20 und sättigen. So sehen sie nach den Zeilen 15-21 aus:


Und so geht's nach Zeile 41:


In Zeile 42 multiplizieren wir dies mit 0,03. Dieser Wert ist der Radius des Kreises für den gesamten Bildschirm. Wie Sie sehen können, wird der Radius näher an den Bildschirmrändern kleiner.

Jetzt können wir uns den Assembler-Code ansehen, der für die Bewegung verantwortlich ist:

  40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 

. 40 — elapsedTime * 0.1 . 43 , .

r0.x ( 41-42) — , , . r4.x ( 44) — , r4.y ( 45) — ( 8!), r4.z ( 46) — .

, 8 . i * PI_4 , 2*PI — . .

Mit sincos bestimmen wir den Abtastpunkt (Einheitskreis) und ändern den Radius durch Multiplikation (Linie 54).

Danach gehen wir in einem Kreis um das Pixel herum und probieren die Konturen und Farben aus. Nach dem Zyklus erhalten wir die Durchschnittswerte (aufgrund der Division durch 8) der Konturen und Farben.

  float timeParam = time * 0.1; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; } 

Die Farbabtastung wird auf die gleiche Weise durchgeführt, aber wir werden der Grundfarbe UV einen Versatz multipliziert mit einem „einzelnen“ Kreis hinzufügen .

Helligkeit


Nach dem Zyklus probieren wir die Helligkeitskarte aus und ändern die endgültigen Helligkeitswerte (da die Helligkeitskarte nichts über die Konturen weiß):

  67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 

HLSL-Code:

  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces ); 

Graue Ecken und die endgültige Vereinigung von allem


Die graue Farbe näher an den Ecken wird mit dem Skalarprodukt (Assembler-Linie 69) berechnet:

  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx; 


Dann folgen zwei Interpolationen. Die erste kombiniert Grau mit der „Farbe im Kreis“ unter Verwendung der ersten von mir beschriebenen Maske, sodass die Ecken grau werden. Zusätzlich gibt es einen Koeffizienten von 0,6, der die Sättigung des endgültigen Bildes verringert:


Die zweite kombiniert die erste Farbe mit der obigen mit fisheyeAmount . Dies bedeutet, dass der Bildschirm allmählich dunkler (aufgrund der Multiplikation mit 0,6) und an den Ecken grau wird! Genial.

HLSL:

  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount ); 

Jetzt können wir die Konturen von Objekten hinzufügen.

Die Farben (rot und gelb) werden aus dem konstanten Puffer entnommen.

  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting; 


Fuh! Wir sind fast am Ziel!

Wir haben die endgültige Farbe, da ist die Farbe des Hexeninstinkts ... es bleibt, sie irgendwie zu kombinieren!

Und dafür ist eine einfache Zugabe nicht geeignet. Zuerst berechnen wir das Skalarprodukt:

  78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); 

das sieht so aus:


Und diese Werte ganz am Ende werden verwendet, um zwischen Farbe und dem (gesättigten) Hexenflair zu interpolieren:

  76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 ); 


Und das ist alles.

Der vollständige Shader ist hier verfügbar .

Vergleich meiner (links) und ursprünglichen (rechts) Shader:


Ich hoffe dir hat dieser Artikel gefallen! In der Mechanik des „Hexerinstinkts“ gibt es viele brillante Ideen, und das Endergebnis ist sehr plausibel.

[Vorherige Teile der Analyse: erster und zweiter .]

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


All Articles