Das Reverse Engineering des Renderings von The Witcher 3: verschiedene Himmelseffekte

Bild

[Vorherige Teile der Analyse: erster und zweiter und dritter .]

Teil 1. Cirruswolken


Wenn das Spiel auf freiem Feld stattfindet, ist der Himmel einer der Faktoren, die die Glaubwürdigkeit der Welt bestimmen. Denken Sie darüber nach - die meiste Zeit nimmt der Himmel buchstäblich 40-50% des gesamten Bildschirms ein. Der Himmel ist viel mehr als ein schönes Gefälle. Es hat Sterne, die Sonne, den Mond und schließlich Wolken.

Obwohl aktuelle Trends in der volumetrischen Darstellung von Wolken mithilfe von Raymarching zu bestehen scheinen (siehe diesen Artikel ), basieren die Wolken in The Witcher 3 vollständig auf Texturen. Ich habe sie bereits zuvor untersucht, aber es stellte sich heraus, dass bei ihnen alles komplizierter ist, als ich ursprünglich erwartet hatte. Wenn Sie meiner Artikelserie gefolgt sind, wissen Sie, dass es einen Unterschied zwischen dem Blood and Wine DLC und dem Rest des Spiels gibt. Und wie Sie sich vorstellen können, gibt es einige Änderungen in der Arbeit mit Wolken im DLC.

Der Hexer 3 hat mehrere Wolkenschichten. Je nach Wetter können es sich nur um Zirruswolken , hohe Cumuluswolken , möglicherweise einige Wolken aus der Familie der Schichtwolken handeln (z. B. während eines Sturms). Am Ende kann es überhaupt keine Wolken geben.

Einige Ebenen unterscheiden sich hinsichtlich der Texturen und Shader, mit denen sie gerendert werden. Dies wirkt sich offensichtlich auf die Komplexität und Länge des Assembler-Codes für den Pixel-Shader aus.

Trotz all dieser Vielfalt gibt es einige gängige Muster, die beim Rendern von Wolken in Witcher 3 beobachtet werden können. Erstens werden alle in einem proaktiven Durchgang gerendert, und dies ist die perfekte Wahl. Alle verwenden Mischen (siehe unten). Dies macht es viel einfacher zu steuern, wie eine separate Ebene den Himmel bedeckt - dies wird durch den Alpha-Wert des Pixel-Shaders beeinflusst.


Interessanterweise werden einige Ebenen zweimal mit denselben Parametern gerendert.

Nachdem ich mir den Code angesehen hatte, wählte ich den kürzesten Shader, um (1) höchstwahrscheinlich sein vollständiges Reverse Engineering durchzuführen, (2) alle seine Aspekte herauszufinden.

Ich habe mir die Zirruswolken aus Witcher 3: Blut und Wein genauer angesehen.

Hier ist ein Beispielrahmen:


Vor dem Rendern


Nach dem ersten Renderpass


Nach dem zweiten Renderdurchgang

In diesem speziellen Frame sind Zirruswolken die erste Ebene beim Rendern. Wie Sie sehen können, wird es zweimal gerendert, wodurch die Helligkeit erhöht wird.

Geometrischer und Vertex-Shader


Vor dem Pixel-Shader werden wir kurz auf die verwendeten geometrischen und Vertex-Shader eingehen. Das Netz zum Anzeigen von Wolken ähnelt einer normalen Himmelskuppel:


Alle Scheitelpunkte liegen im Intervall [0-1]. Um das Netz auf den Punkt (0,0,0) zu zentrieren, werden vor der Konvertierung in worldViewProj Skalierung und Abweichung verwendet (dieses Muster kennen wir bereits aus den vorherigen Teilen der Serie). Bei Wolken erstreckt sich das Netz stark entlang der XY-Ebene (die Z-Achse zeigt nach oben), um mehr Raum als die Sichtbarkeitspyramide abzudecken. Das Ergebnis ist wie folgt:


Zusätzlich hat das Netz normale und tangentiale Vektoren. Der Vertex-Shader berechnet auch den Bi-Tangenten-Vektor anhand des Vektorprodukts - alle drei werden in normalisierter Form angezeigt. Es gibt auch eine Top-Berechnung des Nebels (Farbe und Helligkeit).

Pixel Shader


Der Code für die Pixel-Shader-Assembly sieht folgendermaßen aus:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb12[238], immediateIndexed  
    dcl_constantbuffer cb4[13], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.w  
    dcl_input_ps linear v3.xyzw  
    dcl_input_ps linear v4.xyz  
    dcl_input_ps linear v5.xyz  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  
   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx  
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx  
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   8: add r1.xyz, r1.xyzx, r1.xyzx  
   9: dp3 r0.w, r1.xyzx, r1.xyzx  
  10: rsq r0.w, r0.w  
  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  
  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x  
  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x  
  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x  
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x  
  53: mul r0.w, r0.w, r1.x  
  54: mul_sat r0.w, r0.w, cb4[6].x  
  55: mul o0.xyz, r0.wwww, r0.xyzx  
  56: mov o0.w, r0.w  
  57: ret 

Es werden zwei nahtlose Texturen eingegeben. Eine davon enthält eine normale Karte ( xyz- Kanäle ) und eine Wolkenform (Kanal a ). Das zweite ist Rauschen, um die Form zu verzerren.


Normale Karte, CD Projekt Red Property


Wolkenform, Immobilien-CD Projekt Rot


Rauschstruktur, Eigenschaft von CD Projekt Red

Der Hauptpuffer von Konstanten mit Cloud-Parametern ist cb4. Für diesen Rahmen hat er folgende Bedeutung:


Zusätzlich werden andere Werte aus anderen Cbuffern verwendet. Mach dir keine Sorgen, wir werden sie auch berücksichtigen.

Invertiertes Sonnenlicht in Z-Richtung


Das erste, was im Shader passiert, ist die Berechnung der normalisierten Richtung des Sonnenlichts, das entlang der Z-Achse invertiert ist:

   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  

   float3 invertedSunlightDir = normalize(lightDir * float3(1, 1, -1) );

Wie bereits erwähnt, ist die Z-Achse nach oben gerichtet, und cb0 [9] ist die Richtung des Sonnenlichts. Dieser Vektor ist auf die Sonne gerichtet - es ist wichtig! Sie können dies überprüfen, indem Sie einen einfachen Computer-Shader schreiben, der eine einfache NdotL ausführt, und ihn in den verzögerten Shader-Durchlauf einfügen.

Cloud Texture Sampling


Der nächste Schritt besteht darin, Texcoords zu berechnen, um die Wolkentextur abzutasten, den normalen Vektor zu entpacken und zu normalisieren.

   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx   
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx   
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0   
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)   
   8: add r1.xyz, r1.xyzx, r1.xyzx   
   9: dp3 r0.w, r1.xyzx, r1.xyzx   
  10: rsq r0.w, r0.w   
   
   
   // Calc sampling coords  
   float2 cloudTextureUV = Texcoords * textureScale + elapsedTime * speedFactors;  
   
   // Sample texture and get data from it  
   float4 cloudTextureValue = texture0.Sample( sampler0, cloudTextureUV ).rgba;  
   float3 normalMap = cloudTextureValue.xyz;  
   float cloudShape = cloudTextureValue.a;  
   
   // Unpack normal and normalize it  
   float3 unpackedNormal = (normalMap - 0.5) * 2.0;  
   unpackedNormal = normalize(unpackedNormal);

Lassen Sie uns schrittweise damit umgehen.

Um die Bewegung der Wolken zu erhalten, benötigen wir die verstrichene Zeit in Sekunden ( cb [0] .x ) multipliziert mit dem Geschwindigkeitskoeffizienten, der beeinflusst, wie schnell sich die Wolken über den Himmel bewegen ( cb4 [5] .xy ).

Wie ich bereits sagte, werden UV-Strahlen entlang der Geometrie der Himmelskuppel gestreckt, und wir benötigen auch Textur-Skalierungsfaktoren, die die Größe der Wolken beeinflussen ( cb4 [4] .xy ).

Die endgültige Formel lautet:

samplingUV = Input.TextureUV * textureScale + time * speedMultiplier;

Nach dem Abtasten aller 4 Kanäle haben wir eine normale Karte (RGB-Kanäle) und eine Wolkenform (Kanal a).

So entpacken Sie die normale Karte aus dem Intervall [0; 1] im Intervall [-1; 1] Wir verwenden die folgende Formel:

unpackedNormal = (packedNormal - 0.5) * 2.0;

Sie können dies auch verwenden:

unpackedNormal = packedNormal * 2.0 - 1.0;

Schließlich normalisieren wir den entpackten Normalenvektor.

Normalen überlagern


Mit den Normalenvektoren, den Tangenten- und den Bi-Tangentenvektoren aus dem Vertex-Shader und dem Normalenvektor aus der Normalkarte bilden wir normalerweise die Normalen ab.

  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
    
   // Perform bump mapping  
   float3 SkyTangent = Input.Tangent;  
   float3 SkyNormal = (float3( Input.Texcoords.zw, Input.param3.w ));  
   float3 SkyBitangent = Input.param3.xyz;  
        
   float3x3 TBN = float3x3(SkyTangent, SkyBitangent, SkyNormal);  
   float3 finalNormal = (float3)mul( unpackedNormal, (TBN) );

Helligkeit (1)


Im nächsten Schritt wird die NdotL-Berechnung angewendet, die sich auf die Beleuchtungsstärke eines bestimmten Pixels auswirkt.

Betrachten Sie den folgenden Assembler-Code:

  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  

Hier ist die Visualisierung von NdotL auf dem fraglichen Frame:


Dieses Skalarprodukt (mit Sättigung) wird verwendet, um zwischen minIntensity und maxIntensity zu interpolieren. Dank dessen werden Teile der vom Sonnenlicht beleuchteten Wolken heller.

   // Calculate cosine between normal and up-inv lightdir  
   float NdotL = saturate( dot(invertedSunlightDir, finalNormal) );  
   
   // Param 1, line 19, r0.x  
   float intensity1 = lerp( param1Min, param1Max, NdotL );

Helligkeit (2)


Ein weiterer Faktor beeinflusst die Helligkeit der Wolken.

Wolken, die sich in dem Teil des Himmels befinden, in dem sich die Sonne befindet, sollten stärker hervorgehoben werden. Dazu berechnen wir den Gradienten basierend auf der XY-Ebene.

Dieser Gradient wird verwendet, um die lineare Interpolation zwischen den Min / Max-Werten zu berechnen, ähnlich wie in Teil (1).

Das heißt, theoretisch können wir darum bitten, die Wolken auf der gegenüberliegenden Seite der Sonne abzudunkeln, aber dies geschieht in diesem speziellen Rahmen nicht, da param2Min und param2Max ( cb4 [0] .x und cb4 [1] .x ) auf 1.0f gesetzt sind.

  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x   
   
   
   // Calculate normalized -lightDir.xy (20-22)  
   float2 lightDirXY = normalize( -lightDir.xy );  
   
   // Calculate world to camera  
   float3 vWorldToCamera = ( CameraPos - WorldPos );  
   float worldToCamera_distance = length(vWorldToCamera);  
        
   // normalize vector  
   vWorldToCamera = normalize( vWorldToCamera );  
        
   
   float LdotV = saturate( dot(lightDirXY, vWorldToCamera.xy) );  
   float highlightedSkySection = saturate( 2*LdotV );  
   float intensity2 = lerp( param2Min, param2Max, highlightedSkySection );  
   
   float finalIntensity = pow( intensity2 *intensity1, 2.2);

Ganz am Ende multiplizieren wir beide Helligkeiten und erhöhen das Ergebnis auf eine Potenz von 2,2.

Wolkenfarbe


Die Berechnung der Farbe der Wolken beginnt mit dem Erhalten von zwei Werten aus den Pufferkonstanten, die die Farbe der Wolken neben der Sonne und der Wolken auf der gegenüberliegenden Seite des Himmels angeben. Zwischen ihnen wird eine lineare Interpolation basierend auf hervorgehobenem SkySection durchgeführt .

Dann wird das Ergebnis mit finalIntensity multipliziert .

Und am Ende wird das Ergebnis mit Nebel gemischt (aus Leistungsgründen wurde es vom Vertex-Shader berechnet).

  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
   
  float3 cloudsColor = lerp( cloudsColorBack, cloudsColorFront, highlightedSunSection );  
  cloudsColor *= finalIntensity;  
  cloudsColor = lerp( cloudsColor, FogColor, FogAmount );

Machen Sie Cirruswolken am Horizont besser sichtbar


Dies ist auf dem Rahmen nicht sehr auffällig, aber tatsächlich ist diese Schicht in der Nähe des Horizonts besser sichtbar als über Geralts Kopf. Hier erfahren Sie, wie es geht.

Sie konnten feststellen, dass wir bei der Berechnung der zweiten Helligkeit die Länge des Vektors worldToCamera berechnet haben :

  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w

Lassen Sie uns die folgenden Vorkommen dieser Länge im Code finden:

  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  ...  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x

Wow, was ist los mit uns?

cb [7] .x und cb [8] .x haben die Werte 2000.0 und 7000.0.

Es stellt sich heraus, dass dies das Ergebnis der Verwendung der Linstep- Funktion ist .

Sie erhält drei Parameter: min / max - Intervall und v - Wert.

Dies funktioniert wie folgt: Wenn v im Intervall [ min - max ] liegt, gibt die Funktion im Intervall [0.0 - 1.0] eine lineare Interpolation zurück. Wenn andererseits v außerhalb des Bereichs liegt, gibt linstep 0.0 oder 1.0 zurück.

Ein einfaches Beispiel:

linstep( 1000.0, 2000.0, 999.0) = 0.0
linstep( 1000.0, 2000.0, 1500.0) = 0.5
linstep( 1000.0, 2000.0, 2000.0) = 1.0

Das heißt, es ist dem glatten Schritt von HLSL ziemlich ähnlich , außer dass in diesem Fall anstelle der hermitischen Interpolation eine lineare durchgeführt wird.

Linstep ist keine Funktion in HLSL, aber sehr nützlich. Es lohnt sich, es in Ihrem Toolkit zu haben.

 // linstep:  
 //  
 // Returns a linear interpolation between 0 and 1 if t is in the range [min, max]   
 // if "v" is <= min, the output is 0  
 // if "v" i >= max, the output is 1  
   
 float linstep( float min, float max, float v )  
 {  
   return saturate( (v - min) / (max - min) );  
 } 

Kehren wir zu Hexer 3 zurück: Nachdem wir diesen Indikator berechnet und angegeben haben, wie weit ein bestimmter Teil des Himmels von Geralt entfernt ist, verwenden wir ihn, um die Helligkeit der Wolken zu schwächen:

  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
   
   float distanceAttenuation = linstep( fadeDistanceStart, fadeDistanceEnd, worldToCamera_distance );  
    
   float fadedCloudShape = closeCloudsHidingFactor * cloudShape;  
   cloudShape = lerp( fadedCloudShape, cloudShape, distanceAttenuation );

cloudShape ist der .a-Kanal aus der ersten Textur, und closeCloudsHidingFactor ist ein konstanter Pufferwert, der die Sichtbarkeit der Wolken über Geralts Kopf steuert. In allen von mir getesteten Frames war es gleich 0,0, was dem Fehlen von Wolken entspricht. Wenn sich die Entfernungsabschwächung 1,0 nähert (die Entfernung von der Kamera zur Kuppel des Himmels nimmt zu), werden die Wolken sichtbarer.

Probenahme von Rauschtexturen


Berechnung der Koordinaten der Abtastrauschtextur ähnliche Berechnungen für die Textur von Wolken, außer dass Sie einen anderen Satz von textureScale und speedMultiplier verwenden .

Natürlich wird ein Sampler mit aktiviertem Wrap- Adressierungsmodus verwendet, um alle diese Texturen abzutasten .

  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
   
   // Calc sampling coords for noise  
   float2 noiseTextureUV = Texcoords * textureScaleNoise + elapsedTime * speedFactorsNoise;  
   
   // Sample texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;

Alles zusammenfügen


Nachdem wir den Rauschwert erhalten haben, müssen wir ihn mit cloudShape kombinieren.

Ich hatte einige Probleme beim Verständnis dieser Zeilen, in denen param2.w (immer 1.0) und NoiseMult (Wert 5.0 aus dem konstanten Puffer) vorhanden sind.

Wie auch immer, das Wichtigste hier ist der Endwert von generalCloudsVisibility , der sich auf die Sichtbarkeit der Wolken auswirkt.

Sehen Sie sich auch den Endwert des Rauschens an. Die Ausgabefarbe von cloudColor wird mit dem endgültigen Rauschen multipliziert, das auch an den Alphakanal ausgegeben wird.

  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x
  53: mul r0.w, r0.w, r1.x
  54: mul_sat r0.w, r0.w, cb4[6].x
  55: mul o0.xyz, r0.wwww, r0.xyzx
  56: mov o0.w, r0.w
  57: ret   

   // Sample noise texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;  
   noiseTextureValue = noiseTextureValue * noiseMult - noiseMult;  
     
   float noiseValue = saturate( noiseMult * Input.param2.w + noiseTextureValue);  
   noiseValue *= cloudShape;  
     
   float finalNoise = saturate( noiseValue * generalCloudsVisibility);  
   
   return float4( cloudsColor*finalNoise, finalNoise ); 

Gesamt


Das Endergebnis sieht sehr glaubwürdig aus.

Sie können vergleichen. Das erste Bild ist mein Shader, das zweite ist der Game Shader:


Wenn Sie neugierig sind, finden Sie den Shader hier .

Teil 2. Nebel


Nebel kann auf verschiedene Arten implementiert werden. Die Zeiten, in denen wir einen einfachen entfernungsabhängigen Nebel anwenden und ihn beseitigen konnten, waren jedoch (höchstwahrscheinlich) für immer in der Vergangenheit. Das Leben in der Welt der programmierbaren Shader hat die Tür für neue verrückte, aber vor allem physikalisch genaue und visuell realistische Lösungen geöffnet.

Aktuelle Trends bei der Nebelwiedergabe basieren auf Computer-Shadern (Details siehe diese Präsentation von Bart Wronsky).

Trotz der Tatsache, dass diese Präsentation im Jahr 2014 erschien und The Witcher 3 im Jahr 2015/2016 veröffentlicht wurde, ist der Nebel im letzten Teil von Geralts Abenteuern vollständig vom Bildschirm abhängig und wird als typische Nachbearbeitung implementiert.

Bevor wir mit der nächsten Reverse Engineering-Sitzung beginnen, muss ich sagen, dass ich im vergangenen Jahr mindestens fünf Mal versucht habe, den Nebel von Witcher 3 herauszufinden, und jedes Mal fehlgeschlagen ist. Der Assembler-Code ist, wie Sie gleich sehen werden, ziemlich kompliziert, und dies macht das Erstellen eines lesbaren Fog-Shaders auf HLSL fast unmöglich.

Es gelang mir jedoch, im Internet einen Nebel-Shader zu finden, der meine Aufmerksamkeit sofort auf sich zog, da er hinsichtlich der Variablennamen und der allgemeinen Reihenfolge der Anweisungen dem Nebel von The Witcher 3 ähnlich war. Dieser Shader war nicht genau der gleiche wie im Spiel, also musste ich ihn ein bisschen überarbeiten. Ich möchte sagen, dass der Hauptteil des HLSL-Codes, den Sie hier sehen, mit zwei Ausnahmen nicht von mir erstellt / analysiert wurde. Merk dir das.

Hier ist der Assembler-Code für den Pixel Fog Shader - es ist erwähnenswert, dass er für das gesamte Spiel (den Hauptteil von 2015 und beide DLCs) gleich ist:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[2], immediateIndexed  
    dcl_constantbuffer cb12[214], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_resource_texture2d (float,float,float,float) t2  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 7  
   0: ftou r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r1.x, r0.xyww, t0.xyzw  
   3: mad r1.y, r1.x, cb12[22].x, cb12[22].y  
   4: lt r1.y, r1.y, l(1.000000)  
   5: if_nz r1.y  
   6:  utof r1.yz, r0.xxyx  
   7:  mul r2.xyzw, r1.zzzz, cb12[211].xyzw  
   8:  mad r2.xyzw, cb12[210].xyzw, r1.yyyy, r2.xyzw  
   9:  mad r1.xyzw, cb12[212].xyzw, r1.xxxx, r2.xyzw  
  10:  add r1.xyzw, r1.xyzw, cb12[213].xyzw  
  11:  div r1.xyz, r1.xyzx, r1.wwww  
  12:  ld_indexable(texture2d)(float,float,float,float) r2.xyz, r0.xyww, t1.xyzw  
  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)  
  24:  div r0.yzw, r0.yyzw, r1.xxxx  
  25:  mad r1.y, r0.w, cb12[22].z, cb12[0].z  
  26:  add r1.x, r1.x, -cb12[22].z  
  27:  max r1.x, r1.x, l(0)  
  28:  min r1.x, r1.x, cb12[42].z  
  29:  mul r1.z, r0.w, r1.x  
  30:  mul r1.w, r1.x, cb12[43].x  
  31:  mul r1.zw, r1.zzzw, l(0.000000, 0.000000, 0.062500, 0.062500)  
  32:  dp3 r0.y, cb12[38].xyzx, r0.yzwy  
  33:  add r0.z, r0.y, cb12[42].x  
  34:  add r0.w, cb12[42].x, l(1.000000)  
  35:  div_sat r0.z, r0.z, r0.w  
  36:  add r0.w, -cb12[43].z, cb12[43].y  
  37:  mad r0.z, r0.z, r0.w, cb12[43].z  
  38:  mul r0.w, abs(r0.y), abs(r0.y)  
  39:  mad_sat r2.w, r1.x, l(0.002000), l(-0.300000)  
  40:  mul r0.w, r0.w, r2.w  
  41:  lt r0.y, l(0), r0.y  
  42:  movc r3.xyz, r0.yyyy, cb12[39].xyzx, cb12[41].xyzx  
  43:  add r3.xyz, r3.xyzx, -cb12[40].xyzx  
  44:  mad r3.xyz, r0.wwww, r3.xyzx, cb12[40].xyzx  
  45:  movc r4.xyz, r0.yyyy, cb12[45].xyzx, cb12[47].xyzx  
  46:  add r4.xyz, r4.xyzx, -cb12[46].xyzx  
  47:  mad r4.xyz, r0.wwww, r4.xyzx, cb12[46].xyzx  
  48:  ge r0.y, r1.x, cb12[48].y  
  49:  if_nz r0.y  
  50:   add r0.y, r1.y, cb12[42].y  
  51:   mul r0.w, r0.z, r0.y  
  52:   mul r1.y, r0.z, r1.z  
  53:   mad r5.xyzw, r1.yyyy, l(16.000000, 15.000000, 14.000000, 13.000000), r0.wwww  
  54:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  55:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  56:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  57:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  58:   mul r1.z, r5.y, r5.x  
  59:   mul r1.z, r5.z, r1.z  
  60:   mul r1.z, r5.w, r1.z  
  61:   mad r5.xyzw, r1.yyyy, l(12.000000, 11.000000, 10.000000, 9.000000), r0.wwww  
  62:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  63:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  64:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  65:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:   mul r1.z, r1.z, r5.x  
  67:   mul r1.z, r5.y, r1.z  
  68:   mul r1.z, r5.z, r1.z  
  69:   mul r1.z, r5.w, r1.z  
  70:   mad r5.xyzw, r1.yyyy, l(8.000000, 7.000000, 6.000000, 5.000000), r0.wwww  
  71:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  72:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  73:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  74:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  75:   mul r1.z, r1.z, r5.x  
  76:   mul r1.z, r5.y, r1.z  
  77:   mul r1.z, r5.z, r1.z  
  78:   mul r1.z, r5.w, r1.z  
  79:   mad r5.xy, r1.yyyy, l(4.000000, 3.000000, 0.000000, 0.000000), r0.wwww  
  80:   max r5.xy, r5.xyxx, l(0, 0, 0, 0)  
  81:   add r5.xy, r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  82:   div_sat r5.xy, r1.wwww, r5.xyxx  
  83:   add r5.xy, -r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  84:   mul r1.z, r1.z, r5.x  
  85:   mul r1.z, r5.y, r1.z  
  86:   mad r0.w, r1.y, l(2.000000), r0.w  
  87:   max r0.w, r0.w, l(0)  
  88:   add r0.w, r0.w, l(1.000000)  
  89:   div_sat r0.w, r1.w, r0.w  
  90:   add r0.w, -r0.w, l(1.000000)  
  91:   mul r0.w, r0.w, r1.z  
  92:   mad r0.y, r0.y, r0.z, r1.y  
  93:   max r0.y, r0.y, l(0)  
  94:   add r0.y, r0.y, l(1.000000)  
  95:   div_sat r0.y, r1.w, r0.y  
  96:   add r0.y, -r0.y, l(1.000000)  
  97:   mad r0.y, -r0.w, r0.y, l(1.000000)  
  98:   add r0.z, r1.x, -cb12[48].y  
  99:   mul_sat r0.z, r0.z, cb12[48].z  
  100:  else  
  101:   mov r0.yz, l(0.000000, 1.000000, 0.000000, 0.000000)  
  102:  endif  
  103:  log r0.y, r0.y  
  104:  mul r0.w, r0.y, cb12[42].w  
  105:  exp r0.w, r0.w  
  106:  mul r0.y, r0.y, cb12[48].x  
  107:  exp r0.y, r0.y  
  108:  mul r0.yw, r0.yyyw, r0.zzzz  
  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, cb12[188].w, l(-1.000000)  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.z, r0.w  
  115:  lt r0.z, l(0), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, cb12[190].w, l(-1.000000)  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.z, r0.w  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif  
  126:  mul r0.z, r0.x, r5.w  
  127:  mul r0.x, r0.x, r0.y  
  128:  dp3 r0.y, l(0.333000, 0.555000, 0.222000, 0.000000), r2.xyzx  
  129:  mad r1.xyz, r0.yyyy, r4.xyzx, -r2.xyzx  
  130:  mad r0.xyw, r0.xxxx, r1.xyxz, r2.xyxz  
  131:  add r1.xyz, -r0.xywx, r5.xyzx  
  132:  mad r0.xyz, r0.zzzz, r1.xyzx, r0.xywx  
  133: else  
  134:  mov r0.xyz, l(0, 0, 0, 0)  
  135: endif  
  136: mov o0.xyz, r0.xyzx  
  137: mov o0.w, l(1.000000)  
  138: ret 

Ehrlich gesagt ist der Shader ziemlich lang. Wahrscheinlich zu lang für einen effektiven Reverse Engineering-Prozess.

Hier ist ein Beispiel einer Sonnenuntergangsszene mit Nebel:


Werfen wir einen Blick auf die Eingabe:

Für Texturen haben wir einen Tiefenpuffer, Ambient Occlusion und einen HDR-Farbpuffer.


Eingehender Tiefenpuffer


Eingehende Umgebungsokklusion


Der eingehende HDR-Farbpuffer

... und das Ergebnis der Anwendung des Nebel-Shaders in dieser Szene sieht folgendermaßen aus:


HDR-Textur nach dem Auftragen von Nebel. Der

Tiefenpuffer wird verwendet, um die Position in der Welt wiederherzustellen. Dies ist das Standardmuster für Witcher 3-Shader.

Wenn wir Umgebungsokklusionsdaten haben (falls aktiviert), können wir den Nebel verdecken. Eine sehr kluge Idee, vielleicht eine offensichtliche, aber ich habe es nie so gesehen. Ich werde später auf diesen Aspekt zurückkommen.

Ein Shader ermittelt zunächst, ob sich ein Pixel am Himmel befindet. Wenn das Pixel am Himmel liegt (Tiefe == 1,0), gibt der Shader Schwarz zurück. Befindet sich das Pixel in der Szene (Tiefe <1,0), erstellen wir die Position in der Welt mithilfe des Tiefenpuffers (Zeilen 7-11) neu und berechnen den Nebel weiter.

Der Durchgang von Nebel erfolgt kurz nach dem verzögerten Beschattungsprozess. Möglicherweise stellen Sie fest, dass einige Elemente im Zusammenhang mit dem Vorwärtslauf noch nicht verfügbar sind. In dieser speziellen Szene wurden verzögerte Beleuchtungsvolumina angewendet, und danach haben wir Geralts Haare / Gesicht / Augen gerendert.

Das erste, was Sie über Nebel in „The Witcher 3“ wissen müssen: Es besteht aus zwei Teilen - „Farbe des Nebels“ und „Farbe der Atmosphäre“.

 struct FogResult  
 {  
    float4 paramsFog;     // RGB: color, A: influence  
    float4 paramsAerial;  // RGB: color, A: influence  
 };

Für jedes Teil gibt es drei Farben: vorne, in der Mitte und hinten. Das heißt, im konstanten Puffer befinden sich Daten wie "FogColorFront", "FogColorMiddle", "AerialColorBack" usw. ... Schauen wir uns die eingehenden Daten an:


   // *** Inputs *** //  
   float3 FogSunDir = cb12_v38.xyz;  
   float3 FogColorFront = cb12_v39.xyz;  
   float3 FogColorMiddle = cb12_v40.xyz;  
   float3 FogColorBack = cb12_v41.xyz;  
     
   float4 FogBaseParams = cb12_v42;  
   float4 FogDensityParamsScene = cb12_v43;  
   float4 FogDensityParamsSky = cb12_v44;  
     
   float3 AerialColorFront = cb12_v45.xyz;  
   float3 AerialColorMiddle = cb12_v46.xyz;  
   float3 AerialColorBack = cb12_v47.xyz;  
   float4 AerialParams = cb12_v48;

Bevor wir die endgültigen Farben berechnen, müssen wir die Vektoren und Skalarprodukte berechnen. Der Shader hat Zugriff auf die Pixelposition in der Welt, die Kameraposition (cb12 [0] .xyz) und die Nebel- / Beleuchtungsrichtung (cb12 [38] .xyz). Dies ermöglicht es uns, das Skalarprodukt des Vektors der Form und Richtung des Nebels zu berechnen.

   float3 frag_vec = fragPosWorldSpace.xyz - customCameraPos.xyz;  
   float frag_dist = length(frag_vec);  
     
   float3 frag_dir = frag_vec / frag_dist;  
   
   float dot_fragDirSunDir = dot(GlobalLightDirection.xyz, frag_dir);

Um den Mischungsgradienten zu berechnen, müssen Sie das Quadrat des absoluten Skalarprodukts verwenden und das Ergebnis erneut mit einem Parameter multiplizieren, der von der Entfernung abhängt:

   float3 curr_col_fog;  
   float3 curr_col_aerial;  
   {  
     float _dot = dot_fragDirSunDir;  
   
     float _dd = _dot;  
     {  
       const float _distOffset = -150;  
       const float _distRange = 500;  
       const float _mul = 1.0 / _distRange;  
       const float _bias = _distOffset * _mul;  
   
       _dd = abs(_dd);  
       _dd *= _dd;  
       _dd *= saturate( frag_dist * _mul + _bias );  
     }  
   
     curr_col_fog = lerp( FogColorMiddle.xyz, (_dot>0.0f ? FogColorFront.xyz : FogColorBack.xyz), _dd );  
     curr_col_aerial = lerp( AerialColorMiddle.xyz, (_dot>0.0f ? AerialColorFront.xyz : AerialColorBack.xyz), _dd );  
   }

Dieser Codeblock macht uns klar, woher diese 0,002 und -0,300 stammen. Wie wir sehen können, ist das Skalarprodukt zwischen den Vektoren von Ansicht und Beleuchtung für die Wahl zwischen den Farben „vorne“ und „hinten“ verantwortlich. Klug!

Hier ist eine Visualisierung des resultierenden endgültigen Gradienten (_dd).


Die Berechnung der Wirkung der Atmosphäre / des Nebels ist jedoch viel komplizierter. Wie Sie sehen können, haben wir viel mehr Optionen als nur RGB-Farben. Dazu gehört beispielsweise die Szenendichte. Wir verwenden Raymarching (16 Schritte, und deshalb kann der Zyklus erweitert werden), um die Größe des Nebels und den Skalierungsfaktor zu bestimmen:

Mit einem Vektor [Kamera ---> Welt] können wir alle seine Komponenten in 16 Teile unterteilen - dies ist ein Raymarching-Schritt. Wie wir unten sehen, ist nur die .z (Höhe) -Komponente ( curr_pos_z_step ) an den Berechnungen beteiligt .

Lesen Sie mehr über die von raymarching implementiert Nebel zum Beispiel hier .

   float fog_amount = 1;  
   float fog_amount_scale = 0;  
   [branch]  
   if ( frag_dist >= AerialParams.y )  
   {  
     float curr_pos_z_base = (customCameraPos.z + FogBaseParams.y) * density_factor;  
     float curr_pos_z_step = frag_step.z * density_factor;  
   
     [unroll]  
     for ( int i=16; i>0; --i )  
     {  
       fog_amount *= 1 - saturate( density_sample_scale / (1 + max( 0.0, curr_pos_z_base + (i) * curr_pos_z_step ) ) );  
     }  
   
     fog_amount = 1 - fog_amount;  
     fog_amount_scale = saturate( (frag_dist - AerialParams.y) * AerialParams.z );  
   }  
   
   FogResult ret;  
   
   ret.paramsFog = float4 ( curr_col_fog, fog_amount_scale * pow( abs(fog_amount), final_exp_fog ) );  
   ret.paramsAerial = float4 ( curr_col_aerial, fog_amount_scale * pow( abs(fog_amount), final_exp_aerial ) );

Die Nebelmenge hängt offensichtlich von der Höhe ab (Komponenten .z), am Ende wird die Nebelmenge auf den Grad Nebel / Atmosphäre erhöht.

final_exp_fog und final_exp_aerial werden aus dem konstanten Puffer entnommen; Mit ihnen können Sie steuern, wie sich die Farben von Nebel und Atmosphäre mit zunehmender Höhe auf die Welt auswirken.

Nebel außer Kraft setzen


Der Shader, den ich gefunden habe, hatte nicht das folgende Assemblycode-Fragment:

  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, l(-1.000000), cb12[188].w  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.w, r0.z  
  115:  lt r0.z, l(0.000000), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, l(-1.000000), cb12[190].w  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.w, r0.z  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif

Nach dem, was ich verstehen konnte, ist dies wie eine Neudefinition der Farbe und der Wirkung des Nebels:

Meistens wird nur eine Neudefinition durchgeführt (cb12_v192.x ist 0.0), aber in diesem speziellen Fall beträgt der Wert ~ 0.22, also machen wir die zweite Überschreibung.


 #ifdef OVERRIDE_FOG  
     
   // Override  
   float fog_influence = ret.paramsFog.w; // r0.w  
   
   float override1ColorScale = cb12_v189.x;  
   float override1ColorBias = cb12_v189.y;  
   float3 override1Color = cb12_v188.rgb;  
     
   float override1InfluenceScale = cb12_v189.z;  
   float override1InfluenceBias = cb12_v189.w;  
   float override1Influence = cb12_v188.w;  
     
   float override1ColorAmount = saturate(fog_influence * override1ColorScale + override1ColorBias);  
   float override1InfluenceAmount = saturate(fog_influence * override1InfluenceScale + override1InfluenceBias);    
     

   float4 paramsFogOverride;  
   paramsFogOverride.rgb = lerp(curr_col_fog, override1Color, override1ColorAmount ); // ***r5.xyz   
     
   float param1 = lerp(1.0, override1Influence, override1InfluenceAmount); // r0.x  
   paramsFogOverride.w = saturate(param1 * fog_influence ); // ** r5.w  
   
     
   const float extraFogOverride = cb12_v192.x;  
     
   [branch]   
   if (extraFogOverride > 0.0)  
   {  
     float override2ColorScale = cb12_v191.x;  
     float override2ColorBias = cb12_v191.y;  
     float3 override2Color = cb12_v190.rgb;  
     
     float override2InfluenceScale = cb12_v191.z;  
     float override2InfluenceBias = cb12_v191.w;  
     float override2Influence = cb12_v190.w;  
       
     float override2ColorAmount = saturate(fog_influence * override2ColorScale + override2ColorBias);  
     float override2InfluenceAmount = saturate(fog_influence * override2InfluenceScale + override2InfluenceBias);  
      

     float4 paramsFogOverride2;  
     paramsFogOverride2.rgb = lerp(curr_col_fog, override2Color, override2ColorAmount); // r3.xyz   
           
     float ov_param1 = lerp(1.0, override2Influence, override2InfluenceAmount); // r0.z  
     paramsFogOverride2.w = saturate(ov_param1 * fog_influence); // r3.w  
   
     paramsFogOverride = lerp(paramsFogOverride, paramsFogOverride2, extraFogOverride);  
   
   }  
   ret.paramsFog = paramsFogOverride;  
     
 #endif

Hier ist unser Endpreis ohne Neudefinition von Nebel (erstes Bild) mit einer Neudefinition (zweites Bild) und einer doppelten Neudefinition (drittes Bild, Endergebnis):




Regulierung der Umgebungsokklusion


Der Shader, den ich gefunden habe, hat auch überhaupt keine Umgebungsokklusion verwendet. Schauen wir uns noch einmal die Textur von AO und den Code an, der uns interessiert:


  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)

Vielleicht ist diese Szene nicht das beste Beispiel, weil wir die Details auf einer fernen Insel nicht sehen. Schauen wir uns jedoch den konstanten Puffer an, mit dem der Umgebungsokklusionswert festgelegt wird:


Wir beginnen mit dem Laden von AO aus der Textur und führen dann den Befehl max aus. In dieser Szene ist cb3_v1.x sehr hoch (0,96888), was die AO sehr schwach macht.

Der nächste Teil des Codes berechnet den Abstand zwischen den Positionen der Kamera und den Pixeln in der Welt.

Ich glaube, dass der Code manchmal für sich selbst spricht. Schauen wir uns also HLSL an, das den Großteil dieses Setups erledigt:

 float AdjustAmbientOcclusion(in float inputAO, in float worldToCameraDistance)  
 {  
   // *** Inputs *** //  
   const float aoDistanceStart = cb3_v0.x;  
   const float aoDistanceEnd = cb3_v0.y;  
   const float aoStrengthStart = cb3_v0.z;  
   const float aoStrengthEnd = cb3_v0.w;  
      
   // * Adjust AO  
   float aoDistanceIntensity = linstep( aoDistanceStart, aoDistanceEnd, worldToCameraDistance );  
   float aoStrength = lerp(aoStrengthStart, aoStrengthEnd, aoDistanceIntensity);   
   float adjustedAO = lerp(1.0, inputAO, aoStrength);  
     
   return adjustedAO;   
 }

Der berechnete Abstand zwischen Kamera und Welt wird für die Linstep-Funktion verwendet. Wir kennen diese Funktion bereits, sie erschien im Cirrus Cloud Shader.

Wie Sie sehen können, haben wir im konstanten Puffer die AO-Start- / Endabstandswerte. Die Ausgabe von linstep beeinflusst die Stärke des AO (sowie von cbuffer), und die Stärke beeinflusst die Ausgabe des AO.

Ein kurzes Beispiel: Das Pixel ist weit entfernt, der Abstand beträgt beispielsweise 500.

linstep gibt 1,0 zurück;
aoStrength ist gleich aoStrengthEnd;

Dies führt zu einer AO-Rückgabe, die ungefähr 77% (Endkraft) des Eingabewerts beträgt.

Der eingehende AO für diese Funktion wurde zuvor der Maximaloperation unterzogen.

Alles zusammenfügen


Nachdem Sie die Farbe und den Effekt für die Farbe des Nebels und die Farbe der Atmosphäre erhalten haben, können Sie sie endlich kombinieren.

Wir beginnen damit, den Effekt mit dem resultierenden AO abzuschwächen:

   ...
   FogResult fog = CalculateFog( worldPos, CameraPosition, fogStart, ao, false );  
      
   // Apply AO to influence  
   fog.paramsFog.w *= ao;  
   fog.paramsAerial.w *= ao; 
      
   // Mix fog with scene color  
   outColor = ApplyFog(fog, colorHDR);

Die ganze Magie geschieht in der ApplyFog- Funktion :

 float3 ApplyFog(FogResult fog, float3 color)  
 {  
   const float3 LuminanceFactors = float3(0.333f, 0.555f, 0.222f);  
   
   float3 aerialColor = dot(LuminanceFactors, color) * fog.paramsAerial.xyz;  
   color = lerp(color, aerialColor, fog.paramsAerial.w);  
   color = lerp(color, fog.paramsFog.xyz, fog.paramsFog.w);  
    
   return color.xyz;  
 }

Zuerst berechnen wir die Leuchtkraft von Pixeln:


Dann multiplizieren wir es mit der Farbe der Atmosphäre:


Dann kombinieren wir die HDR-Farbe mit der Farbe der Atmosphäre:


Der letzte Schritt besteht darin, das Zwischenergebnis mit der Farbe des Nebels zu kombinieren:


Das ist alles!

Einige Debugging-Screenshots



Atmosphärischer Effekt


Atmosphärenfarbe


Nebeleffekt


Nebelfarbe


Fertige Szene ohne Nebel


Fertige Szene nur mit Nebel


Die fertige Szene ist nur der Hauptnebel


Wieder eine fertige Szene mit all dem Nebel zum leichteren Vergleich

Gesamt


Ich denke, Sie können viel von dem oben genannten verstehen, wenn Sie sich den Shader ansehen, ist er hier .

Ich kann mit Vergnügen sagen, dass dieser Shader genau der gleiche ist wie der ursprüngliche - es macht mich sehr glücklich.

Im Allgemeinen hängt das Endergebnis stark von den an den Shader übergebenen Werten ab. Dies ist keine „magische“ Lösung, die perfekte Farben für die Ausgabe liefert. Es sind viele Iterationen und Künstler erforderlich, damit das Endergebnis anständig aussieht. Ich denke, es kann ein langer Prozess sein, aber nachdem Sie ihn abgeschlossen haben, wird das Ergebnis sehr überzeugend sein, genau wie diese Sonnenuntergangsszene.

Der Witcher 3 Sky Shader verwendet auch Nebelberechnungen, um einen reibungslosen Farbübergang in der Nähe des Horizonts zu erzielen. Ein anderer Satz von Dichtekoeffizienten wird jedoch an den Sky Shader übergeben.

Ich möchte Sie daran erinnern, dass der größte Teil dieses Shaders nicht von mir erstellt / analysiert wurde. Alle Bestätigungen sollten an CD PROJEKT RED gesendet werden. Unterstützen Sie sie, sie machen einen exzellenten Job.

Teil 3. Sternschnuppen


In The Witcher 3 gibt es ein kleines, aber merkwürdiges Detail - Sternschnuppen. Interessanterweise scheinen sie nicht im Blood and Wine DLC zu sein.

Im Video können Sie sehen, wie sie aussehen:


Mal sehen, wie wir diesen Effekt erzielt haben.

Wie Sie sehen können, ist der Körper eines Sternschnuppen viel heller als der Schwanz. Dies ist eine wichtige Eigenschaft, die wir später verwenden werden.

Unsere Agenda ist ziemlich vertraut: Zuerst werde ich die allgemeinen Eigenschaften beschreiben, dann werde ich über Themen im Zusammenhang mit Geometrie sprechen, und am Ende werden wir zum Pixel-Shader übergehen, wo die interessantesten Dinge passieren.

1. Allgemeiner Überblick


Beschreiben Sie kurz, was passiert.

Sternschnuppen werden in einer proaktiven Passage unmittelbar nach der Kuppel von Himmel, Himmel und Mond gezeichnet:



DrawIndexed (720) - die Kuppel des Himmels,
DrawIndexed (2160) - die Kugel für Himmel / Mond,
DrawIndexed (36) - ist irrelevant, sieht aus wie ein Parallelepiped der Okklusion der Sonne (?)
DrawIndexed (12) - der Sternschnuppe
DrawIndexedInstanced (1116, 1) - Zirruswolken

Wie Zirruswolken wird jeder Sternschnuppen zweimal hintereinander gezeichnet.


Vor dem ersten Draw Call


Ergebnis des ersten Draw Calls


Ergebnis des zweiten Draw Calls

Zusätzlich wird, wie in vielen Elementen des präventiven Passes dieses Spiels, der folgende Mischungszustand verwendet:


2. Geometrie


In Bezug auf die Geometrie ist zunächst zu erwähnen, dass jeder Sternschnuppen durch ein dünnes Quad mit Texkoordinaten dargestellt wird: 4 Eckpunkte, 6 Indizes. Dies ist das einfachste Quad, das möglich ist.


Ungefähres Quad eines Shooting Stars.


Noch näher ist das ungefähre Quad eines Shooting Stars. Sie können das Drahtmodell der Linie sehen, die zwei Dreiecke kennzeichnet.

Warte eine Minute , aber es gibt DrawIndexed (12) ! Bedeutet das, dass wir zwei Sternschnuppen gleichzeitig zeichnen?

Ja.


In diesem Rahmen befindet sich einer der Sternschnuppen vollständig außerhalb der Sichtbarkeitspyramide.

Schauen wir uns den Assembler-Code für den Vertex-Shader an:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[193], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xyzw  
    dcl_input v2.xy  
    dcl_input v3.xy  
    dcl_input v4.xy  
    dcl_input v5.xyz  
    dcl_input v6.x  
    dcl_input v7.x  
    dcl_output o0.xyzw  
    dcl_output o1.xyzw  
    dcl_output o2.xy  
    dcl_output o3.xyzw  
    dcl_output_siv o4.xyzw, position  
    dcl_temps 5  
   0: mov r0.xyz, v0.xyzx  
   1: mov r0.w, l(1.000000)  
   2: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   3: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   4: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   5: add r0.x, v2.x, v2.y  
   6: add r0.y, -v2.y, v2.x  
   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx  
  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx  
  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx  
  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000)  
  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  
  30: add r0.xyz, r0.xyzx, -cb12[0].xyzx  
  31: dp3 r0.w, r0.xyzx, r0.xyzx  
  32: sqrt r0.w, r0.w  
  33: div r0.xyz, r0.xyzx, r0.wwww  
  34: add r0.w, r0.w, -cb12[22].z  
  35: max r0.w, r0.w, l(0)  
  36: min r0.w, r0.w, cb12[42].z  
  37: dp3 r0.x, cb12[38].xyzx, r0.xyzx  
  38: mul r0.y, abs(r0.x), abs(r0.x)  
  39: mad_sat r1.x, r0.w, l(0.002000), l(-0.300000)  
  40: mul r0.y, r0.y, r1.x  
  41: lt r1.x, l(0), r0.x  
  42: movc r1.yzw, r1.xxxx, cb12[39].xxyz, cb12[41].xxyz  
  43: add r1.yzw, r1.yyzw, -cb12[40].xxyz  
  44: mad r1.yzw, r0.yyyy, r1.yyzw, cb12[40].xxyz  
  45: movc r2.xyz, r1.xxxx, cb12[45].xyzx, cb12[47].xyzx  
  46: add r2.xyz, r2.xyzx, -cb12[46].xyzx  
  47: mad o0.xyz, r0.yyyy, r2.xyzx, cb12[46].xyzx  
  48: ge r0.y, r0.w, cb12[48].y  
  49: if_nz r0.y  
  50:  mad r0.y, r0.z, cb12[22].z, cb12[0].z  
  51:  mul r0.z, r0.w, r0.z  
  52:  mul r0.z, r0.z, l(0.062500)  
  53:  mul r1.x, r0.w, cb12[43].x  
  54:  mul r1.x, r1.x, l(0.062500)  
  55:  add r0.x, r0.x, cb12[42].x  
  56:  add r2.x, cb12[42].x, l(1.000000)  
  57:  div_sat r0.x, r0.x, r2.x  
  58:  add r2.x, -cb12[43].z, cb12[43].y  
  59:  mad r0.x, r0.x, r2.x, cb12[43].z  
  60:  add r0.y, r0.y, cb12[42].y  
  61:  mul r2.x, r0.x, r0.y  
  62:  mul r0.z, r0.x, r0.z  
  63:  mad r3.xyzw, r0.zzzz, l(16.000000, 15.000000, 14.000000, 13.000000), r2.xxxx  
  64:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  65:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  67:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  68:  mul r2.y, r3.y, r3.x  
  69:  mul r2.y, r3.z, r2.y  
  70:  mul r2.y, r3.w, r2.y  
  71:  mad r3.xyzw, r0.zzzz, l(12.000000, 11.000000, 10.000000, 9.000000), r2.xxxx  
  72:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  73:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  74:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  75:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  76:  mul r2.y, r2.y, r3.x  
  77:  mul r2.y, r3.y, r2.y  
  78:  mul r2.y, r3.z, r2.y  
  79:  mul r2.y, r3.w, r2.y  
  80:  mad r3.xyzw, r0.zzzz, l(8.000000, 7.000000, 6.000000, 5.000000), r2.xxxx  
  81:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  82:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  83:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  84:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  85:  mul r2.y, r2.y, r3.x  
  86:  mul r2.y, r3.y, r2.y  
  87:  mul r2.y, r3.z, r2.y  
  88:  mul r2.y, r3.w, r2.y  
  89:  mad r2.zw, r0.zzzz, l(0.000000, 0.000000, 4.000000, 3.000000), r2.xxxx  
  90:  max r2.zw, r2.zzzw, l(0, 0, 0, 0)  
  91:  add r2.zw, r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  92:  div_sat r2.zw, r1.xxxx, r2.zzzw  
  93:  add r2.zw, -r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  94:  mul r2.y, r2.z, r2.y  
  95:  mul r2.y, r2.w, r2.y  
  96:  mad r2.x, r0.z, l(2.000000), r2.x  
  97:  max r2.x, r2.x, l(0)  
  98:  add r2.x, r2.x, l(1.000000)  
  99:  div_sat r2.x, r1.x, r2.x  
  100:  add r2.x, -r2.x, l(1.000000)  
  101:  mul r2.x, r2.x, r2.y  
  102:  mad r0.x, r0.y, r0.x, r0.z  
  103:  max r0.x, r0.x, l(0)  
  104:  add r0.x, r0.x, l(1.000000)  
  105:  div_sat r0.x, r1.x, r0.x  
  106:  add r0.x, -r0.x, l(1.000000)  
  107:  mad r0.x, -r2.x, r0.x, l(1.000000)  
  108:  add r0.y, r0.w, -cb12[48].y  
  109:  mul_sat r0.y, r0.y, cb12[48].z  
  110: else  
  111:  mov r0.xy, l(1.000000, 0.000000, 0.000000, 0.000000)  
  112: endif  
  113: log r0.x, r0.x  
  114: mul r0.z, r0.x, cb12[42].w  
  115: exp r0.z, r0.z  
  116: mul r0.z, r0.z, r0.y  
  117: mul r0.x, r0.x, cb12[48].x  
  118: exp r0.x, r0.x  
  119: mul o0.w, r0.x, r0.y  
  120: mad_sat r0.xy, r0.zzzz, cb12[189].xzxx, cb12[189].ywyy  
  121: add r2.xyz, -r1.yzwy, cb12[188].xyzx  
  122: mad r2.xyz, r0.xxxx, r2.xyzx, r1.yzwy  
  123: add r0.x, cb12[188].w, l(-1.000000)  
  124: mad r0.x, r0.y, r0.x, l(1.000000)  
  125: mul_sat r2.w, r0.x, r0.z  
  126: lt r0.x, l(0), cb12[192].x  
  127: if_nz r0.x  
  128:  mad_sat r0.xy, r0.zzzz, cb12[191].xzxx, cb12[191].ywyy  
  129:  add r3.xyz, -r1.yzwy, cb12[190].xyzx  
  130:  mad r1.xyz, r0.xxxx, r3.xyzx, r1.yzwy  
  131:  add r0.x, cb12[190].w, l(-1.000000)  
  132:  mad r0.x, r0.y, r0.x, l(1.000000)  
  133:  mul_sat r1.w, r0.x, r0.z  
  134:  add r0.xyzw, -r2.xyzw, r1.xyzw  
  135:  mad o1.xyzw, cb12[192].xxxx, r0.xyzw, r2.xyzw  
  136: else  
  137:  mov o1.xyzw, r2.xyzw  
  138: endif  
  139: mov o3.xyzw, v1.xyzw  
  140: mov o2.xy, v4.yxyy  
  141: ret

Hier kann die Berechnung des Nebels sofort Aufmerksamkeit erregen (Zeilen 30-138). Die Berechnung der Nebelspitze ist aus Leistungsgründen sinnvoll. Außerdem brauchen wir keine solche Genauigkeit des Nebels - Meteoroiden fliegen normalerweise über Geralts Kopf und erreichen den Horizont nicht.

Die atmosphärischen Parameter (rgb = Farbe, a = Einfluss) werden in o0.xyzw und die Nebelparameter in o1.xyzw gespeichert.

o2.xy (Zeile 140) ist nur Texcoords.
o3.xyzw (Zeile 139) ist irrelevant.

Lassen Sie uns nun ein paar Worte zur Berechnung einer Position in der Welt sagen. Vertex-Shader führen Billboarding durch . Zuallererst kommen eingehende Daten für Werbetafeln aus dem Scheitelpunktpuffer - schauen wir sie uns an.

Die ersten Daten sind Position:


Wie oben erwähnt, haben wir hier 2 Quad-a: 8 Eckpunkte, 12 Indizes.

Aber warum ist die Position für jedes Quad gleich? Ganz einfach - das ist die Position der Mitte des Quad.

Ferner hat jeder Scheitelpunkt einen Versatz von der Mitte zum Rand des Quadrats:


Dies bedeutet, dass jeder Shooting Star eine Größe von (400, 3) Einheiten im Weltraum hat. (In der XY-Ebene in Hexer 3 ist die Z-Achse nach oben gerichtet.)

Das letzte Element, das jeder Scheitelpunkt hat, ist ein Einheitsrichtungsvektor im Weltraum, der die Bewegung eines Sternschnuppen steuert:


Da die Daten von der CPU stammen, ist es schwierig zu verstehen, wie sie berechnet werden.

Fahren wir nun mit dem Billboarding-Code fort. Die Idee ist ganz einfach: Zuerst erhalten Sie einen Einheitsvektor von der Mitte des Quad zur Kamera:

   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx

Dann erhalten wir einen einzelnen Tangentenvektor, der die Bewegung des Sternschnuppen steuert.

Da dieser Vektor auf der CPU-Seite bereits normalisiert ist, ist diese Normalisierung redundant.

  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx

Wenn zwei Vektoren vorhanden sind, wird ein Vektorprodukt verwendet, um den Bi-Tangentenvektor senkrecht zu beiden eingehenden Vektoren zu bestimmen.

  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx

Jetzt haben wir die Vektoren Tangente (r3.xyz) und Bitangente (r2.xyz) normalisiert.

Lassen Sie uns Xsize und Ysize einführen , die dem eingehenden Element TEXCOORD1 entsprechen, also zum Beispiel (-200, 1,50).

Die endgültige Berechnung der Position im Weltraum erfolgt wie folgt:

  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000) 

Da r0.x, r0.y und r0.z gleich 1,0 sind, wird die endgültige Berechnung vereinfacht:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

Der letzte Teil ist eine einfache Multiplikation einer Position im Weltraum mit einer Ansichtsprojektionsmatrix, um SV_Position zu erhalten:

  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  

3. Pixel Shader


Wie im Abschnitt "Allgemeine Übersicht" angegeben, wird der folgende Mischstatus verwendet: Dabei sind SrcColor und SrcAlpha die .rgb- und .a-Komponenten des Pixel- Shaders und DestColor die .rgb- Farbe, die sich derzeit im Rendertarget befindet. Der Hauptindikator, der die Transparenz steuert, ist SrcAlpha . Viele proaktive Game-Shader berechnen es als Deckkraft und wenden es am Ende wie folgt an: Der Falling-Star-Shader war keine Ausnahme. Nach diesem Muster betrachten wir drei Fälle, in denen die Opazität 1,0, 0,1 und 0,0 beträgt.

FinalColor = SrcColor * One + DestColor * (1.0 - SrcAlpha) =
FinalColor = SrcColor + DestColor * (1.0 - SrcAlpha)






return float4( color * opacity, opacity )



a) opacity = 1.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = color = SrcColor



b) opacity = 0.1

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = 0.1 * color + 0.9 * DestColor



c) opacity = 0.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = DestColor



Die Grundidee dieses Shaders besteht darin, die Deckkraftfunktion (x) zu modellieren und zu verwenden , die die Deckkraft eines Pixels entlang eines Sternschnuppen steuert. Die Hauptanforderung ist, dass die Opazität am Ende des Sterns (seines „Körpers“) Maximalwerte erreicht und sanft auf 0,0 (zu seinem „Schwanz“) verblasst.

Wenn wir beginnen, den Assembler-Code des Pixel-Shaders zu verstehen, wird dies offensichtlich:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[2], immediateIndexed  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.y  
    dcl_input_ps linear v3.w  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mov_sat r0.x, v2.y  
   1: ge r0.y, r0.x, l(0.052579)  
   2: ge r0.z, l(0.965679), r0.x  
   3: and r0.y, r0.z, r0.y  
   4: if_nz r0.y  
   5:  ge r0.y, l(0.878136), r0.x  
   6:  add r0.z, r0.x, l(-0.052579)  
   7:  mul r1.w, r0.z, l(1.211303)  
   8:  mov_sat r0.z, r1.w  
   9:  mad r0.w, r0.z, l(-2.000000), l(3.000000)  
  10:  mul r0.z, r0.z, r0.z  
  11:  mul r0.z, r0.z, r0.w  
  12:  mul r2.x, r0.z, l(0.084642)  
  13:  mov r1.yz, l(0.000000, 0.000000, 0.084642, 0.000000)  
  14:  movc r2.yzw, r0.yyyy, r1.yyzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  15:  not r0.z, r0.y  
  16:  if_z r0.y  
  17:   ge r0.y, l(0.924339), r0.x  
  18:   add r0.w, r0.x, l(-0.878136)  
  19:   mul r1.w, r0.w, l(21.643608)  
  20:   mov_sat r0.w, r1.w  
  21:   mad r3.x, r0.w, l(-2.000000), l(3.000000)  
  22:   mul r0.w, r0.w, r0.w  
  23:   mul r0.w, r0.w, r3.x  
  24:   mad r1.x, r0.w, l(0.889658), l(0.084642)  
  25:   mov r1.yz, l(0.000000, 0.084642, 0.974300, 0.000000)  
  26:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  27:  else  
  28:   mov r2.y, l(0)  
  29:   mov r0.y, l(-1)  
  30:  endif  
  31:  not r0.w, r0.y  
  32:  and r0.z, r0.w, r0.z  
  33:  if_nz r0.z  
  34:   ge r0.y, r0.x, l(0.924339)  
  35:   add r0.x, r0.x, l(-0.924339)  
  36:   mul r1.w, r0.x, l(24.189651)  
  37:   mov_sat r0.x, r1.w  
  38:   mad r0.z, r0.x, l(-2.000000), l(3.000000)  
  39:   mul r0.x, r0.x, r0.x  
  40:   mul r0.x, r0.x, r0.z  
  41:   mad r1.x, r0.x, l(-0.974300), l(0.974300)  
  42:   mov r1.yz, l(0.000000, 0.974300, 0.000000, 0.000000)  
  43:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  44:  endif  
  45: else  
  46:  mov r2.yzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  47:  mov r0.y, l(0)  
  48: endif  
  49: mov_sat r2.w, r2.w  
  50: mad r0.x, r2.w, l(-2.000000), l(3.000000)  
  51: mul r0.z, r2.w, r2.w  
  52: mul r0.x, r0.z, r0.x  
  53: add r0.z, -r2.y, r2.z  
  54: mad r0.x, r0.x, r0.z, r2.y  
  55: movc r0.x, r0.y, r2.x, r0.x  
  56: mad r0.y, cb4[1].x, -cb0[9].w, l(1.000000)  
  57: mul_sat r0.y, r0.y, v3.w  
  58: mul r0.x, r0.y, r0.x  
  59: mul r0.yzw, cb2[2].xxyz, cb4[0].xxxx  
  60: mul r0.x, r0.x, cb2[2].w  
  61: dp3 r1.x, l(0.333000, 0.555000, 0.222000, 0.000000), r0.yzwy  
  62: mad r1.xyz, r1.xxxx, v0.xyzx, -r0.yzwy  
  63: mad r0.yzw, v0.wwww, r1.xxyz, r0.yyzw  
  64: add r1.xyz, -r0.yzwy, v1.xyzx  
  65: mad r0.yzw, v1.wwww, r1.xxyz, r0.yyzw  
  66: mul o0.xyz, r0.xxxx, r0.yzwy  
  67: mov o0.w, r0.x  
  68: ret

Im Allgemeinen ist der Shader etwas überkompliziert und es fiel mir schwer herauszufinden, was darin vor sich ging. Woher stammen beispielsweise alle Werte wie 1.211303, 21.643608 und 24.189651?

Wenn wir über die Opazitätsfunktion sprechen, benötigen wir einen Eingabewert. Damit ist es ganz einfach - Texcoord im Bereich von [0,1] (Zeile 0) ist hier nützlich, damit wir die Funktion auf die gesamte Länge des Meteoriten anwenden können.

Die Opazitätsfunktion hat drei Segmente / Intervalle, die durch vier Kontrollpunkte definiert sind:

   // current status: no idea how these are generated  
   const float controlPoint0 = 0.052579;  
   const float controlPoint1 = 0.878136;  
   const float controlPoint2 = 0.924339;  
   const float controlPoint3 = 0.965679;

Ich habe keine Ahnung, wie sie ausgewählt / berechnet wurden.

Wie aus dem Assembler-Code ersichtlich ist, besteht die erste Bedingung lediglich darin, zu überprüfen, ob der Eingabewert im Bereich [controlPoint0 - controlPoint3] liegt. Wenn nicht, beträgt die Deckkraft nur 0,0.

   // Input for the opacity function
   float y = saturate(Input.Texcoords.y);  // r0.x
     
   // Value of opacity function.  
   // 0 - no change  
   // 1 - full color  
   float opacity = 0.0;  
     
   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
      ...

Die Entschlüsselung des folgenden Assembler-Codes ist erforderlich, wenn wir verstehen möchten, wie die Opazitätsfunktion funktioniert:

   6: add r0.z, r0.x, l(-0.052579)   
   7: mul r1.w, r0.z, l(1.211303)   
   8: mov_sat r0.z, r1.w   
   9: mad r0.w, r0.z, l(-2.000000), l(3.000000)   
  10: mul r0.z, r0.z, r0.z   
  11: mul r0.z, r0.z, r0.w   
  12: mul r2.x, r0.z, l(0.084642)

Zeile 9 hat die Koeffizienten '-2.0' und '3.0', was auf die Verwendung der Smoothstep- Funktion hinweist . Ja, das ist eine gute Vermutung.

Die HLSL-Smoothstep-Funktion mit Prototyp: Ret Smoothstep (min, max, x) begrenzt x immer auf [ min-max ]. Aus Assembler-Sicht subtrahiert dies min vom Eingabewert (d. H. Von r0.z in Zeile 9), aber es gibt nichts Vergleichbares im Code. Für max bedeutet dies eine Multiplikation des Eingabewerts, aber der Code enthält nichts Vergleichbares wie 'mul_sat'. Stattdessen gibt es 'mov_sat'. Dies sagt uns , dass die min und max Funktionen von smoothstep sind 0 und 1.

Jetzt wissen wir , dass xmuss im Intervall [0, 1] liegen. Wie oben angegeben, gibt es drei Segmente in der Opazitätsfunktion. Dies deutet eindeutig darauf hin, dass der Code sucht, wo wir uns im Intervall [segmentStart-segmentEnd] befinden.

Die Antwort ist die Linstep-Funktion!

 float linstep(float min, float max, float v)  
 {  
   return ( (v-min) / (max-min) );  
 }

Nehmen wir zum Beispiel das erste Segment: [0.052579 - 0.878136]. Die Subtraktion erfolgt in Zeile 6. Wenn wir die Division durch Multiplikation ersetzen -> 1,0 / (0,878136 - 0,052579) = 1,0 / 0,825557 = ~ 1,211303.

Das Ergebnis von Smoothstep liegt im Bereich [0, 1]. Die Multiplikation in Zeile 12 ist das Gewicht des Segments. Jedes Segment hat sein eigenes Gewicht, sodass Sie die maximale Deckkraft dieses bestimmten Segments steuern können.

Dies bedeutet, dass für das erste Segment [0,052579 - 0,878136] die Opazität im Bereich [0 - 0,084642] liegt.

Eine HLSL-Funktion, die die Opazität für ein beliebiges Segment berechnet, kann wie folgt geschrieben werden:

 float getOpacityFunctionValue(float x, float cpLeft, float cpRight, float weight)  
 {  
   float val = smoothstep( 0, 1, linstep(cpLeft, cpRight, x) );  
   return val * weight;  
 }

Der springende Punkt ist also, diese Funktion einfach für das entsprechende Segment aufzurufen.

Schauen Sie sich die Gewichte an:

   const float weight0 = 0.084642;  
   const float weight1 = 0.889658;  
   const float weight2 = 0.974300; // note: weight0+weight1 = weight2

Gemäß dem Assembler-Code wird die Opazitätsfunktion (x) wie folgt berechnet:

   float opacity = 0.0;

   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
     // Range of v: [0, weight0]  
     float v = getOpacityFunctionValue(y, controlPoint0, controlPoint1, weight0);  
     opacity = v;  
     
     [branch]  
     if ( y >= controlPoint1 )  
     {  
       // Range of v: [0, weight1]  
       float v = getOpacityFunctionValue(y, controlPoint1, controlPoint2, weight1);  
       opacity = weight0 + v;  
   
       [branch]  
       if (y >= controlPoint2)  
       {  
         // Range of v: [0, weight2]  
         float v = getOpacityFunctionValue(y, controlPoint2, controlPoint3, weight2);
         opacity = weight2 - v;          
       }  
     }  
   }

Hier ist ein Diagramm der Opazitätsfunktion. Sie können leicht einen starken Anstieg der Deckkraft erkennen, der auf den Beginn des Körpers eines Sternschnuppen hinweist:


Grafikopazitätsfunktion.

Roter Kanal - Opazitätswert.
Grüner Kanal - Kontrollpunkte.
Blauer Kanal - Gewichte.


Nach der Berechnung der Opazität ist alles andere nur der letzte Schliff. Dann gibt es zusätzliche Multiplikationen: die Opazität der Sterne, die Farbe des Sternschnuppen und den Einfluss des Nebels. Wie bei TW3-Shadern üblich, finden Sie hier auch redundante Multiplikationen mit 1,0:

   // cb4_v1.x = 1.0  
   float starsOpacity = 1.0 - cb0_v9.w * cb4_v1.x;    
   opacity *= starsOpacity;  

   // Calculate color of a shooting star  
   // cb4_v0.x = 10.0
   // cb2_v2.rgb = (1.0, 1.0, 1.0)
   float3 color = cb2_v2.rgb * cb4_v0.x;
     
   // cb2_v2.w = 1  
   opacity *= cb2_v2.w;
     
   FogResult fr = { Input.FogParams, Input.AerialParams };  
   color = ApplyFog(fr, color);
     
   return float4( color*opacity, opacity);  
 }

4. Zusammenfassung


Die Hauptschwierigkeit liegt im Teil mit der Opazitätsfunktion. Nach dem Entschlüsseln ist alles andere recht einfach zu verstehen.

Ich habe oben gesagt, dass der Pixel-Shader etwas überkompliziert ist. Tatsächlich kümmern wir uns nur um den Wert der Opazitätsfunktion (x) , die in r2.x gespeichert ist (beginnend mit Zeile 49). Die Deckkraftfunktion im Assembler-Code erstellt jedoch drei zusätzliche Variablen: minRange (r2.y), maxRange (r2.z) und value (r2.w). Alle von ihnen sind Parameter, die zur Berechnung der Opazität verwendet werden, wenn die Opazität (x) nicht verwendet wird:

lerp( minRange, maxRange, smoothstep(0, 1, value) );

Tatsächlich wird der endgültige Opazitätswert im bedingten Zweig in Zeile 55 erhalten - wenn der Eingabewert x istliegt im Bereich [controlPoint0 - controlPoint3], bedeutet dies, dass die Opazitätsfunktion verwendet wird, sodass r2.x ausgewählt ist. Andernfalls wird, wenn x außerhalb des Intervalls liegt, die Opazität aus r0.x berechnet, dh gemäß der obigen Gleichung.

Ich habe einige Pixel außerhalb des Intervalls [controlPoint0 - controlPoint3] debuggt, und die endgültige Deckkraft stellte sich immer als Null heraus.

Das ist alles für heute. Und wie immer vielen Dank fürs Lesen.

All Articles