L'ingénierie inverse du rendu de The Witcher 3: divers effets du ciel

image

[Parties précédentes de l'analyse: première , deuxième et troisième .]

Partie 1. Nuages ​​de Cirrus


Lorsque le jeu se déroule dans des espaces ouverts, l'un des facteurs déterminant la crédibilité du monde est le ciel. Pensez-y - la plupart du temps, le ciel occupe littéralement environ 40 à 50% de tout l'écran. Le ciel est bien plus qu'un beau dégradé. Il a des étoiles, le soleil, la lune et enfin des nuages.

Bien que les tendances actuelles semblent consister en un rendu volumétrique des nuages ​​à l'aide du raymarching (voir cet article ), les nuages ​​dans The Witcher 3 sont entièrement basés sur la texture. Je les ai déjà examinés auparavant, mais il s'est avéré qu'avec eux, tout est plus compliqué que ce à quoi je m'attendais à l'origine. Si vous avez suivi ma série d'articles, vous savez qu'il y a une différence entre le DLC Blood and Wine et le reste du jeu. Et, comme vous pouvez le deviner, il y a quelques changements dans le travail avec les nuages ​​dans le DLC.

Le Witcher 3 a plusieurs couches de nuages. Selon la météo, il ne peut s'agir que de cirrus , de cumulus élevés , éventuellement de quelques nuages ​​de la famille des nuages ​​en couches (par exemple lors d'une tempête). En fin de compte, il peut ne pas y avoir de nuages ​​du tout.

Certains calques diffèrent en termes de textures et de shaders utilisés pour les rendre. De toute évidence, cela affecte la complexité et la longueur du code assembleur pour le pixel shader.

Malgré toute cette diversité, il existe certains modèles communs qui peuvent être observés lors du rendu des nuages ​​dans Witcher 3. Tout d'abord, ils sont tous rendus dans une passe proactive, et c'est le choix parfait. Tous utilisent le mélange (voir ci-dessous). Cela rend beaucoup plus facile de contrôler la façon dont un calque séparé recouvre le ciel - cela est affecté par la valeur alpha du pixel shader.


Plus intéressant, certains calques sont rendus deux fois avec les mêmes paramètres.

Après avoir regardé le code, j'ai choisi le shader le plus court afin (1) d'effectuer probablement son reverse engineering complet, (2) de comprendre tous ses aspects.

J'ai regardé de plus près les nuages ​​de cirrus de Witcher 3: Blood and Wine.

Voici un exemple de cadre:


Avant le rendu


Après la première passe de rendu


Après la deuxième passe de rendu

Dans ce cadre particulier, les nuages ​​de cirrus sont la première couche du rendu. Comme vous pouvez le voir, il est rendu deux fois, ce qui augmente sa luminosité.

Shader géométrique et vertex


Avant le pixel shader, nous parlerons brièvement des shaders géométriques et vertex utilisés. Le maillage pour l'affichage des nuages ​​est un peu comme un dôme de ciel ordinaire:


Tous les sommets sont dans l'intervalle [0-1], donc pour centrer le maillage sur le point (0,0,0), la mise à l'échelle et la déviation sont utilisées avant la conversion en worldViewProj (nous connaissons déjà ce modèle des parties précédentes de la série). Dans le cas des nuages, le maillage s'étire fortement le long du plan XY (l'axe Z pointe vers le haut) pour couvrir plus d'espace que la pyramide de visibilité. Le résultat est le suivant:


De plus, le maillage a des vecteurs normaux et tangents. Le vertex shader calcule également le vecteur bi-tangent par le produit vectoriel - les trois sont affichés sous une forme normalisée. Il y a aussi un calcul supérieur du brouillard (sa couleur et sa luminosité).

Pixel shader


Le code d'assemblage du pixel shader ressemble à ceci:

 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 

Deux textures sans couture sont entrées. L'un d'eux contient une carte normale (canaux xyz ) et une forme de nuage (canal a ). Le second est le bruit qui déforme la forme.


Carte normale, propriété CD Projekt Red


Forme de nuage, propriété CD Projekt Red


Texture de bruit, propriété de CD Projekt Red

Le tampon principal des constantes avec paramètres de nuage est cb4. Pour ce cadre, il a les significations suivantes:


De plus, d'autres valeurs provenant d'autres tampons sont utilisées. Ne vous inquiétez pas, nous les considérerons également.

Lumière du soleil inversée direction Z


La première chose qui se produit dans le shader est le calcul de la direction normalisée de la lumière solaire inversée le long de l'axe Z:

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

Comme mentionné précédemment, l'axe Z est dirigé vers le haut et cb0 [9] est la direction de la lumière solaire. Ce vecteur est destiné au soleil - c'est important! Vous pouvez le vérifier en écrivant un shader de calcul simple qui exécute un NdotL simple et en l'insérant dans la passe de shader différée.

Échantillonnage de la texture des nuages


L'étape suivante consiste à calculer des texcoords pour échantillonner la texture du nuage, décompresser le vecteur normal et le normaliser.

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

Traitons-le progressivement.

Pour obtenir le mouvement des nuages, nous avons besoin du temps écoulé en secondes ( cb [0] .x ) multiplié par le coefficient de vitesse, ce qui affecte la vitesse à laquelle les nuages ​​se déplacent dans le ciel ( cb4 [5] .xy ).

Comme je l'ai dit plus tôt, les UV sont étirés le long de la géométrie du dôme du ciel, et nous avons également besoin de facteurs d'échelle de texture qui affectent la taille des nuages ​​( cb4 [4] .xy ).

La formule finale est:

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

Après avoir échantillonné les 4 canaux, nous avons une carte normale (canaux RVB) et une forme de nuage (canal a).

Pour décompresser la carte normale de l'intervalle [0; 1] dans l'intervalle [-1; 1] nous utilisons la formule suivante:

unpackedNormal = (packedNormal - 0.5) * 2.0;

Vous pouvez également utiliser ceci:

unpackedNormal = packedNormal * 2.0 - 1.0;

Enfin, nous normalisons le vecteur normal décompressé.

Superposer les normales


Ayant les vecteurs normaux, les vecteurs tangents et bi-tangents du vertex shader et le vecteur normal de la carte normale, nous cartographions normalement les normales.

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

Luminosité (1)


Dans l'étape suivante, le calcul NdotL est appliqué et cela affecte la quantité d'éclairage d'un pixel spécifique.

Considérez le code assembleur suivant:

  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  

Voici la visualisation de NdotL sur la trame en question:


Ce produit scalaire (avec saturation) est utilisé pour interpoler entre minIntensity et maxIntensity. Grâce à cela, les parties des nuages ​​éclairées par la lumière du soleil seront plus lumineuses.

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

Luminosité (2)


Un autre facteur affecte la luminosité des nuages.

Les nuages ​​situés dans la partie du ciel où se trouve le soleil devraient être davantage mis en évidence. Pour ce faire, nous calculons le gradient sur la base du plan XY.

Ce gradient est utilisé pour calculer l'interpolation linéaire entre les valeurs min / max, similaire à ce qui se passe dans la partie (1).

Autrement dit, nous pouvons théoriquement demander d'assombrir les nuages ​​situés de l'autre côté du soleil, mais cela ne se produit pas dans ce cadre particulier, car param2Min et param2Max ( cb4 [0] .x et cb4 [1] .x ) sont définis sur 1.0f.

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

À la toute fin, nous multiplions les deux luminosités et élevons le résultat à une puissance de 2,2.

Couleur des nuages


Le calcul de la couleur des nuages ​​commence par l'obtention des constantes tampons de deux valeurs indiquant la couleur des nuages ​​près du soleil et des nuages ​​de l'autre côté du ciel. Entre eux, une interpolation linéaire est effectuée sur la base de l' optionSkySection .

Le résultat est ensuite multiplié par finalIntensity .

Et au final, le résultat est mélangé au brouillard (pour des raisons de performances, il a été calculé par le vertex shader).

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

Rendre les nuages ​​de cirrus plus visibles à l'horizon


Ce n'est pas très visible sur le cadre, mais en fait cette couche est plus visible près de l'horizon qu'au-dessus de la tête de Geralt. Voici comment procéder.

Vous pourriez remarquer que lors du calcul de la deuxième luminosité, nous avons calculé la longueur du vecteur worldToCamera :

  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

Trouvons les occurrences suivantes de cette longueur dans le code:

  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, qu'est-ce que c'est avec nous?

cb [7] .x et cb [8] .x ont les valeurs 2000.0 et 7000.0.

Il s'avère que c'est le résultat de l'utilisation de la fonction linstep .

Elle reçoit trois paramètres: min / max - intervalle et v - valeur.

Cela fonctionne comme suit: si v est dans l'intervalle [ min - max ], alors la fonction renvoie une interpolation linéaire dans l'intervalle [0,0 - 1,0]. D'un autre côté, si v est hors limites, alors linstep renvoie 0,0 ou 1,0.

Un exemple simple:

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

Autrement dit, il est assez similaire au smoothstep de HLSL, sauf que dans ce cas, au lieu de l'interpolation hermitienne, linéaire est effectuée.

Linstep n'est pas une fonctionnalité de HLSL, mais il est très utile. Cela vaut la peine de l'avoir dans votre boîte à outils.

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

Revenons à Witcher 3: après avoir calculé cet indicateur, signalé à quelle distance une partie du ciel est de Geralt, nous l'utilisons pour affaiblir la luminosité des nuages:

  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 est le canal .a de la première texture, et closeCloudsHidingFactor est une valeur tampon constante qui contrôle la visibilité des nuages ​​au-dessus de la tête de Geralt. Dans toutes les images que j'ai testées, elle était égale à 0,0, ce qui équivaut à l'absence de nuages. Lorsque l' atténuation de la distance approche de 1,0 (la distance entre la caméra et le dôme du ciel augmente), les nuages ​​deviennent plus visibles.

Échantillonnage de la texture du bruit


Calcul des coordonnées de la texture du bruit d'échantillonnage calculs similaires pour la texture des nuages, sauf que vous utilisez un ensemble différent de textureScale et speedMultiplier .

Bien sûr, un sampler avec le wrap mode d' adressage activé est utilisé pour échantillonner toutes ces textures .

  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;

Mettre tous ensemble


Après avoir reçu la valeur du bruit, nous devons la combiner avec cloudShape.

J'ai eu quelques problèmes pour comprendre ces lignes, où il y a param2.w (qui est toujours 1.0) et noiseMult (a une valeur de 5.0, tirée du tampon constant).

Quoi qu'il en soit, la chose la plus importante ici est la valeur finale de generalCloudsVisibility , qui affecte la visibilité des nuages.

Jetez également un œil à la valeur finale du bruit. La couleur de sortie de cloudsColor est multipliée par le bruit final, qui est également émis vers le canal alpha.

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

Total


Le résultat final semble très crédible.

Vous pouvez comparer. La première image est mon shader, la seconde est le shader du jeu:


Si vous êtes curieux, le shader est disponible ici .

Partie 2. Brouillard


Le brouillard peut être mis en œuvre de différentes manières. Cependant, les moments où nous pouvions appliquer un simple brouillard dépendant de la distance et le supprimer étaient pour toujours dans le passé (très probablement). Vivre dans le monde des shaders programmables a ouvert la porte à de nouvelles solutions folles, mais plus importantes encore, physiquement précises et visuellement réalistes.

Les tendances actuelles du rendu du brouillard sont basées sur des shaders de calcul (pour plus de détails, voir cette présentation de Bart Wronsky).

Malgré le fait que cette présentation soit apparue en 2014 et que The Witcher 3 soit sorti en 2015/2016, le brouillard dans la dernière partie des aventures de Geralt dépend complètement de l'écran et est implémenté comme un post-traitement typique.

Avant de commencer la prochaine session de rétro-ingénierie, je dois dire qu'au cours de la dernière année, j'ai essayé de comprendre le brouillard de Witcher 3 au moins cinq fois, et chaque fois a échoué. Le code assembleur, comme vous le verrez bientôt, est assez compliqué, ce qui rend le processus de création d'un shader de brouillard lisible sur HLSL presque impossible.

Cependant, j'ai réussi à trouver un shader de brouillard sur Internet qui a immédiatement attiré mon attention en raison de sa similitude avec le brouillard The Witcher 3 en termes de noms de variables et de l'ordre général des instructions. Ce shader n'était pas exactement le même que dans le jeu, j'ai donc dû le retravailler un peu. Je tiens à dire que la partie principale du code HLSL que vous voyez ici, à deux exceptions près, n'a pas été créée / analysée par moi. N'oubliez pas cela.

Voici le code assembleur pour le pixel fog shader - il convient de noter qu'il est le même pour tout le jeu (la partie principale de 2015 et les deux DLC):

 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 

Honnêtement, le shader est assez long. Probablement trop long pour un processus d'ingénierie inverse efficace.

Voici un exemple d'une scène de coucher de soleil avec du brouillard:


Jetons un coup d'œil à l'entrée: en ce

qui concerne les textures, nous avons un tampon de profondeur, une occlusion ambiante et un tampon de couleur HDR.


Tampon de profondeur entrant


Occlusion ambiante entrante


Le tampon de couleur HDR entrant

... et le résultat de l'application du shader de brouillard dans cette scène ressemble à ceci:


Texture HDR après application du brouillard.Le

tampon de profondeur est utilisé pour recréer la position dans le monde. C'est le modèle standard pour les shaders Witcher 3.

Avoir des données d'occlusion ambiante (si activées) nous permet d'obscurcir le brouillard. Une idée très intelligente, peut-être évidente, mais je n'y ai jamais pensé de cette façon. Je reviendrai sur cet aspect plus tard.

Un shader commence par déterminer si un pixel est dans le ciel. Dans le cas où le pixel se trouve dans le ciel (profondeur == 1.0), le shader retourne le noir. Si le pixel est dans la scène (profondeur <1.0), alors nous recréons la position dans le monde en utilisant le tampon de profondeur (lignes 7-11) et continuons à calculer le brouillard.

Le passage du brouillard se produit peu de temps après le processus d'ombrage retardé. Vous remarquerez peut-être que certains éléments liés à la marche avant ne sont pas encore disponibles. Dans cette scène particulière, des volumes d'éclairage différés ont été appliqués, puis nous avons rendu les cheveux / visage / yeux de Geralt.

La première chose que vous devez savoir sur le brouillard dans «The Witcher 3»: il se compose de deux parties - «couleur du brouillard» et «couleur de l'atmosphère».

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

Pour chaque pièce, il existe trois couleurs: avant, milieu et arrière. Autrement dit, dans le tampon constant, il y a des données telles que "FogColorFront", "FogColorMiddle", "AerialColorBack", etc ... Regardons les données entrantes:


   // *** 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;

Avant de calculer les couleurs finales, nous devons calculer les vecteurs et les produits scalaires. Le shader a accès à la position des pixels dans le monde, à la position de la caméra (cb12 [0] .xyz) et à la direction du brouillard / éclairage (cb12 [38] .xyz). Cela nous permet de calculer le produit scalaire du vecteur de la forme et de la direction du brouillard.

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

Pour calculer le gradient de mélange, vous devez utiliser le carré du produit scalaire absolu, puis multiplier à nouveau le résultat par un paramètre qui dépend de la distance:

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

Ce bloc de code nous indique clairement d'où proviennent ces 0,002 et -0,300. Comme on peut le voir, le produit scalaire entre les vecteurs de vue et d'éclairage est responsable du choix entre les couleurs «avant» et «arrière». Intelligent!

Voici une visualisation du gradient final résultant (_dd).


Cependant, le calcul de l'effet de l'atmosphère / du brouillard est beaucoup plus compliqué. Comme vous pouvez le voir, nous avons beaucoup plus d'options que les couleurs RVB. Ils incluent, par exemple, la densité des scènes. Nous utilisons le raymarching (16 étapes, et c'est pourquoi le cycle peut être étendu) pour déterminer la taille du brouillard et le facteur d'échelle:

ayant un vecteur [caméra ---> monde], nous pouvons diviser toutes ses composantes en 16 - ce sera une étape de raymarching. Comme nous le voyons ci-dessous, seule la composante .z (hauteur) ( curr_pos_z_step ) est impliquée dans les calculs .

En savoir plus sur le brouillard mis en œuvre par raymarching, par exemple, ici .

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

La quantité de brouillard dépend évidemment de la hauteur (composants .z), à la fin la quantité de brouillard est élevée au degré de brouillard / atmosphère.

final_exp_fog et final_exp_aerial sont extraits du tampon constant; ils vous permettent de contrôler comment les couleurs du brouillard et de l'atmosphère affectent le monde avec une altitude croissante.

Contournement de brouillard


Le shader que j'ai trouvé n'a pas le fragment de code d'assembly suivant:

  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

À en juger par ce que j'ai pu comprendre, cela revient à redéfinir la couleur et l'effet du brouillard: la

plupart du temps, une seule redéfinition est effectuée (cb12_v192.x est 0,0), mais dans ce cas particulier, sa valeur est ~ 0,22, nous faisons donc le deuxième remplacement.


 #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

Voici notre prix fini sans redéfinition du brouillard (première image), avec une redéfinition (deuxième image) et une double redéfinition (troisième image, résultat final):




Régulation de l'occlusion ambiante


Le shader que j'ai trouvé n'utilisait pas du tout d'occlusion ambiante. Jetons un coup d'œil à la texture d'AO et au code qui nous intéresse:


  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)

Cette scène n'est peut-être pas le meilleur exemple, car on ne voit pas les détails sur une île lointaine. Cependant, jetons un œil au tampon constant, qui est utilisé pour définir la valeur d'occlusion ambiante:


Nous commençons par charger AO à partir de la texture, puis exécutons l'instruction max. Dans cette scène, cb3_v1.x est très élevé (0,96888), ce qui rend l'AO très faible.

La partie suivante du code calcule la distance entre les positions de la caméra et les pixels dans le monde.

Je crois que le code parle parfois de lui-même, alors regardons HLSL, qui fait l'essentiel de cette configuration:

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

La distance calculée entre la caméra et le monde est utilisée pour la fonction Linstep. Nous connaissons déjà cette fonction, elle est apparue dans le shader de nuage de cirrus.

Comme vous pouvez le voir, dans le tampon constant, nous avons les valeurs de distance de début / fin AO. La sortie de linstep affecte la force de l'AO (ainsi que de cbuffer), et la force affecte la sortie de l'AO.

Un bref exemple: le pixel est loin, par exemple, la distance est de 500.

linstep renvoie 1,0;
aoStrength est égal à aoStrengthEnd;

Il en résulte un retour AO, qui représente environ 77% (force finale) de la valeur d'entrée.

L'AO entrant pour cette fonction était auparavant soumis à l'opération max.

Mettre tous ensemble


Après avoir reçu la couleur et l'effet de la couleur du brouillard et de la couleur de l'atmosphère, vous pouvez enfin les combiner.

Nous commençons par atténuer l'effet avec l'AO résultant:

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

Toute la magie opère dans la fonction ApplyFog :

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

Tout d'abord, nous calculons la luminosité des pixels:


Ensuite, nous le multiplions par la couleur de l'atmosphère:


Ensuite, nous combinons la couleur HDR avec la couleur de l'atmosphère:


La dernière étape consiste à combiner le résultat intermédiaire avec la couleur du brouillard:


C'est tout!

Quelques captures d'écran de débogage



Effet atmosphérique


Couleur de l'atmosphère


Effet de brouillard


Couleur de brouillard


Scène terminée sans brouillard


Scène prête à l'emploi avec du brouillard uniquement


La scène terminée n'est que le brouillard principal


Scène prête à l'emploi avec tout le brouillard pour faciliter la comparaison

Total


Je pense que vous pouvez comprendre beaucoup de ce qui précède, si vous regardez le shader, il est ici .

Je peux dire avec plaisir que ce shader est exactement le même que celui d'origine - il me fait très plaisir.

En général, le résultat final dépend fortement des valeurs transmises au shader. Ce n'est pas une solution «magique» qui donne des couleurs parfaites pour la sortie, elle nécessite de nombreuses itérations et artistes pour que le résultat final soit décent. Je pense que cela peut être un long processus, mais une fois terminé, le résultat sera très convaincant, tout comme cette scène de coucher de soleil.

Le Witcher 3 Sky Shader utilise également des calculs de brouillard pour créer une transition en douceur des couleurs près de l'horizon. Cependant, un ensemble différent de coefficients de densité est transmis au shader du ciel.

Permettez-moi de vous rappeler - la plupart de ce shader n'a pas été créé / analysé par moi. Tous les remerciements doivent être envoyés à CD PROJEKT RED. Soutenez-les, ils font un excellent travail.

Partie 3. Étoiles filantes


Dans The Witcher 3, il y a un détail petit mais curieux - les étoiles filantes. Fait intéressant, ils ne semblent pas figurer dans le DLC Blood and Wine.

Dans la vidéo, vous pouvez voir à quoi ils ressemblent:


Voyons comment nous avons réussi à obtenir cet effet.

Comme vous pouvez le voir, le corps d'une étoile filante est beaucoup plus lumineux que la queue. Il s'agit d'une propriété importante que nous utiliserons plus tard.

Notre programme est assez familier: je vais d'abord décrire les propriétés générales, puis je parlerai de sujets liés à la géométrie, et à la fin nous passerons au pixel shader, où les choses les plus intéressantes se produisent.

1. Aperçu général


Décrivez brièvement ce qui se passe.

Les étoiles filantes sont dessinées dans un passage proactif, immédiatement après le dôme du ciel, du ciel et de la lune:



DrawIndexed (720) - le dôme du ciel,
DrawIndexed (2160) - la sphère pour le ciel / la lune,
DrawIndexed (36) - n'a pas d'importance, ressemble à un parallélépipède de l'occlusion du soleil (?)
DrawIndexed (12) - l'étoile filante
DrawIndexedInstanced (1116, 1) - nuages ​​cirrus

Comme les nuages ​​cirrus , chaque étoile filante est dessinée deux fois de suite.


Avant le premier appel de tirage


Résultat du premier appel de tirage


Résultat du deuxième appel de tirage

En outre, comme dans de nombreux éléments de la passe préemptive de ce jeu, l'état de mélange suivant est utilisé:


2. Géométrie


En termes de géométrie, la première chose à mentionner est que chaque étoile filante est représentée par un quadrilatère mince avec texcoords: 4 sommets, 6 indices. C'est le quad le plus simple possible.


Quad approximatif d'une étoile filante. Le quadruple approximatif d'une étoile filante est


encore plus proche. Vous pouvez voir l'affichage filaire d'une ligne indiquant deux triangles.

Attendez une minute , mais il y a DrawIndexed (12) ! Est-ce à dire que nous dessinons deux étoiles filantes en même temps?

Oui.


Dans ce cadre, l'une des étoiles filantes est complètement en dehors de la pyramide de visibilité.

Regardons le code assembleur du vertex shader:

 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

Ici, le calcul du brouillard peut immédiatement attirer l'attention (lignes 30-138). Le calcul du sommet du brouillard est logique pour des raisons de performances. De plus, nous n'avons pas besoin d'une telle précision du brouillard - les météorites survolent généralement la tête de Geralt et n'atteignent pas l'horizon.

Les paramètres atmosphériques (rgb = couleur, a = influence) sont stockés dans o0.xyzw, et les paramètres de brouillard dans o1.xyzw.

o2.xy (ligne 140) est juste des texcoords.
o3.xyzw (ligne 139) n'est pas pertinent.

Maintenant, disons quelques mots sur le calcul d'une position dans le monde. Les vertex shaders effectuent des panneaux d'affichage . Tout d'abord, les données entrantes pour les panneaux d'affichage proviennent du tampon de vertex - jetons-y un œil.

Les premières données sont Position:


Comme mentionné ci-dessus, nous avons ici 2 quad-a: 8 sommets, 12 indices.

Mais pourquoi la position est-elle la même pour chaque quad? Assez simple - c'est la position du centre du quad.

De plus, chaque sommet a un décalage du centre vers le bord du quad:


Cela signifie que chaque étoile filante a une taille de (400, 3) unités dans l'espace mondial. (sur le plan XY, dans Witcher 3, l'axe Z est dirigé vers le haut)

Le dernier élément de chaque sommet est un vecteur de direction unitaire dans l'espace mondial qui contrôle le mouvement d'une étoile filante:


Étant donné que les données proviennent du CPU, il est difficile de comprendre comment elles sont calculées.

Passons maintenant au code d'affichage. L'idée est assez simple - vous obtenez d'abord un vecteur unitaire du centre du quad à la caméra:

   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

Ensuite, nous obtenons un seul vecteur tangent qui contrôle le mouvement de l'étoile filante.

Etant donné que ce vecteur est déjà normalisé côté CPU, cette normalisation est redondante.

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

S'il y a deux vecteurs, un produit vectoriel est utilisé pour déterminer le vecteur bi-tangent perpendiculaire aux deux vecteurs entrants.

  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

Nous avons maintenant des vecteurs normalisés tangents (r3.xyz) et bitangents (r2.xyz).

Introduisons Xsize et Ysize correspondant à l'élément entrant TEXCOORD1, donc par exemple (-200, 1.50).

Le calcul final de la position dans l'espace mondial est effectué comme suit:

  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) 

Étant donné que r0.x, r0.y et r0.z sont égaux à 1,0, le calcul final est simplifié:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

La dernière partie est une simple multiplication d'une position dans l'espace mondial par une matrice de projection de vue pour obtenir SV_Position:

  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


Comme indiqué dans la section Présentation générale, l'état de fusion suivant est utilisé: SrcColor et SrcAlpha sont respectivement les composants .rgb et .a du pixel shader et DestColor est la couleur .rgb actuellement dans le rendu cible. Le principal indicateur qui contrôle la transparence est SrcAlpha . De nombreux shaders de jeu proactifs le calculent comme l'opacité et l'appliquent à la fin comme suit: Le shader d'étoiles filantes ne fait pas exception. En suivant ce modèle, nous considérons trois cas dans lesquels l' opacité est de 1,0, 0,1 et 0,0.

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



L'idée sous-jacente de ce shader est de modéliser et d'utiliser l'opacité de la fonction d' opacité (x) , qui contrôle l'opacité d'un pixel le long d'une étoile filante. La principale exigence est que l'opacité atteigne des valeurs maximales à l'extrémité de l'étoile (son «corps») et passe progressivement à 0,0 (à sa «queue»).

Lorsque nous commençons à comprendre le code assembleur du pixel shader, cela devient évident:

 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

En général, le shader est un peu trop compliqué et il était difficile pour moi de comprendre ce qui s'y passait. Par exemple, d'où viennent toutes les valeurs comme 1.211303, 21.643608 et 24.189651?

Si nous parlons de la fonction d'opacité, nous avons besoin d'une valeur d'entrée. C'est assez simple - texcoord dans la plage de [0,1] (ligne 0) sera utile ici, afin que nous puissions appliquer la fonction à toute la longueur du météoroïde.

La fonction d'opacité a trois segments / intervalles définis par quatre points de contrôle:

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

Je n'ai aucune idée de la façon dont ils ont été sélectionnés / calculés.

Comme nous pouvons le voir dans le code assembleur, la première condition est simplement de vérifier si la valeur d'entrée est dans la plage [controlPoint0 - controlPoint3]. Sinon, l'opacité n'est que de 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)  
   {  
      ...

Le déchiffrement du code assembleur ci-dessous est nécessaire si nous voulons comprendre comment fonctionne la fonction d'opacité:

   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)

La ligne 9 a les coefficients «-2,0» et «3,0», ce qui indique l'utilisation de la fonction smoothstep . Oui, c'est une bonne supposition.

La fonction HLSL smoothstep avec prototype: ret smoothstep (min, max, x) limite toujours x à [ min-max ]. Du point de vue de l'assembleur, cela soustrait min de la valeur d'entrée (c'est-à-dire de r0.z sur la ligne 9), mais il n'y a rien de tel dans le code. Pour max, cela implique une multiplication de la valeur d'entrée, mais il n'y a rien de tel que «mul_sat» dans le code. Au lieu de cela, il y a «mov_sat». Cela nous dit que les fonctions min et max du smoothstep sont 0 et 1.

Nous savons maintenant que xdoit être dans l'intervalle [0, 1]. Comme indiqué ci-dessus, la fonction d'opacité comprend trois segments. Cela indique clairement que le code cherche où nous en sommes dans l'intervalle [segmentStart-segmentEnd].

La réponse est la fonction Linstep!

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

Par exemple, prenons le premier segment: [0,052579 - 0,878136]. La soustraction est sur la ligne 6. Si nous remplaçons la division par la multiplication -> 1,0 / (0,878136 - 0,052579) = 1,0 / 0,825557 = ~ 1,211303.

Le résultat du lissage est dans la plage [0, 1]. La multiplication sur la ligne 12 est le poids du segment. Chaque segment a son propre poids, vous permettant de contrôler l'opacité maximale de ce segment particulier.

Cela signifie que pour le premier segment [0,052579 - 0,878136], l'opacité est dans la plage [0 - 0,084642].

Une fonction HLSL qui calcule l'opacité pour un segment arbitraire peut être écrite comme suit:

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

Donc, le but est simplement d'appeler cette fonction pour le segment correspondant.

Jetez un œil aux poids:

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

Selon le code assembleur, la fonction d' opacité (x) est calculée comme suit:

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

Voici un graphique de la fonction d'opacité. Vous pouvez facilement voir une forte augmentation de l'opacité, indiquant le début du corps d'une étoile filante:


Fonction d'opacité du graphique.

Canal rouge - valeur d'opacité
Canal vert - points de contrôle
Canal bleu - poids


Après le calcul de l'opacité, tout le reste n'est que la touche finale. Il y a ensuite des multiplications supplémentaires: l'opacité des étoiles, la couleur de l'étoile filante et l'influence du brouillard. Comme d'habitude dans les shaders TW3, vous pouvez également trouver des multiplications redondantes par 1.0 ici:

   // 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. Résumé


La principale difficulté réside dans la partie avec la fonction d'opacité. Après l'avoir décodé, tout le reste est assez simple à comprendre.

J'ai dit plus haut que le pixel shader était un peu trop compliqué. En fait, nous nous soucions uniquement de la valeur de la fonction d' opacité (x) , qui est stockée dans r2.x (à partir de la ligne 49). Cependant, la fonction d'opacité dans le code assembleur crée trois variables supplémentaires: minRange (r2.y), maxRange (r2.z) et value (r2.w). Tous sont des paramètres utilisés pour calculer l'opacité lorsque l' opacité (x) n'est pas utilisée:

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

en fait, la valeur finale de l'opacité est obtenue dans la branche conditionnelle de la ligne 55 - si la valeur d'entrée est xest dans la plage [controlPoint0 - controlPoint3], cela signifie que la fonction d'opacité est utilisée, donc r2.x est sélectionné. Sinon, lorsque x est en dehors de l'intervalle, l'opacité est calculée à partir de r0.x, c'est-à-dire selon l'équation ci-dessus.

J'ai débogué quelques pixels en dehors de l'intervalle [controlPoint0 - controlPoint3], et l'opacité finale s'est toujours avérée être nulle.

C'est tout pour aujourd'hui. Et comme toujours, merci d'avoir lu.

All Articles