L'ingénierie inverse du rendu de The Witcher 3

La première partie de la traduction est ici . Dans cette partie, nous parlerons de l'effet de la netteté, de la luminosité moyenne, des phases de la lune et des phénomènes atmosphériques sous la pluie.

Partie 6. Aiguiser


Dans cette partie, nous allons examiner de plus près un autre effet de post-traitement de The Witcher 3 - Sharpen.

La netteté rend l'image de sortie un peu plus nette. Cet effet nous est connu de Photoshop et d'autres éditeurs graphiques.

Dans The Witcher 3, la netteté a deux options: basse et haute. Je vais parler de la différence entre eux ci-dessous, mais pour l'instant, regardons les captures d'écran:

image

Option «Low» - jusqu'à

image

Option «Low» - après


Option élevée - jusqu'à


Option "High" - après

Si vous souhaitez jeter un œil à des comparaisons plus détaillées (interactives), consultez la section du Guide de performances de The Witcher 3 de Nvidia . Comme vous pouvez le voir, l'effet est particulièrement visible sur l'herbe et le feuillage.

Dans cette partie du post, nous étudierons le cadre depuis le tout début du jeu: je l'ai choisi intentionnellement, car ici on voit le relief (longue distance de dessin) et le dôme du ciel.


En termes d'entrée, la netteté nécessite un tampon de couleur t0 (LDR après correction de tonalité et reflets de l'objectif) et un tampon de profondeur t1 .

Examinons le code assembleur pour le pixel shader:

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb3[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_input_ps_siv v0.xy, position
dcl_output o0.xyzw
dcl_temps 7
0: ftoi r0.xy, v0.xyxx
1: mov r0.zw, l(0, 0, 0, 0)
2: ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t1.xyzw
3: mad r0.x, r0.x, cb12[22].x, cb12[22].y
4: mad r0.y, r0.x, cb12[21].x, cb12[21].y
5: max r0.y, r0.y, l(0.000100)
6: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
7: mad_sat r0.y, r0.y, cb3[1].z, cb3[1].w
8: add r0.z, -cb3[1].x, cb3[1].y
9: mad r0.y, r0.y, r0.z, cb3[1].x
10: add r0.y, r0.y, l(1.000000)
11: ge r0.x, r0.x, l(1.000000)
12: movc r0.x, r0.x, l(0), l(1.000000)
13: mul r0.z, r0.x, r0.y
14: round_z r1.xy, v0.xyxx
15: add r1.xy, r1.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)
16: div r1.xy, r1.xyxx, cb3[0].zwzz
17: sample_l(texture2d)(float,float,float,float) r2.xyz, r1.xyxx, t0.xyzw, s0, l(0)
18: lt r0.z, l(0), r0.z
19: if_nz r0.z
20: div r3.xy, l(0.500000, 0.500000, 0.000000, 0.000000), cb3[0].zwzz
21: add r0.zw, r1.xxxy, -r3.xxxy
22: sample_l(texture2d)(float,float,float,float) r4.xyz, r0.zwzz, t0.xyzw, s0, l(0)
23: mov r3.zw, -r3.xxxy
24: add r5.xyzw, r1.xyxy, r3.zyxw
25: sample_l(texture2d)(float,float,float,float) r6.xyz, r5.xyxx, t0.xyzw, s0, l(0)
26: add r4.xyz, r4.xyzx, r6.xyzx
27: sample_l(texture2d)(float,float,float,float) r5.xyz, r5.zwzz, t0.xyzw, s0, l(0)
28: add r4.xyz, r4.xyzx, r5.xyzx
29: add r0.zw, r1.xxxy, r3.xxxy
30: sample_l(texture2d)(float,float,float,float) r1.xyz, r0.zwzz, t0.xyzw, s0, l(0)
31: add r1.xyz, r1.xyzx, r4.xyzx
32: mul r3.xyz, r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000)
33: mad r1.xyz, -r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000), r2.xyzx
34: max r0.z, abs(r1.z), abs(r1.y)
35: max r0.z, r0.z, abs(r1.x)
36: mad_sat r0.z, r0.z, cb3[2].x, cb3[2].y
37: mad r0.x, r0.y, r0.x, l(-1.000000)
38: mad r0.x, r0.z, r0.x, l(1.000000)
39: dp3 r0.y, l(0.212600, 0.715200, 0.072200, 0.000000), r2.xyzx
40: dp3 r0.z, l(0.212600, 0.715200, 0.072200, 0.000000), r3.xyzx
41: max r0.w, r0.y, l(0.000100)
42: div r1.xyz, r2.xyzx, r0.wwww
43: add r0.y, -r0.z, r0.y
44: mad r0.x, r0.x, r0.y, r0.z
45: max r0.x, r0.x, l(0)
46: mul r2.xyz, r0.xxxx, r1.xyzx
47: endif
48: mov o0.xyz, r2.xyzx
49: mov o0.w, l(1.000000)
50: ret


50 lignes de code assembleur semblent être une tâche réalisable. Passons à la résolution.

Accentuer la génération de valeur


La première étape consiste à charger le tampon de profondeur (ligne 1). Il est à noter que le «The Witcher 3» utilise une profondeur inversée (1.0 - proche, 0.0 - éloigné). Comme vous le savez peut-être, la profondeur du matériel est liée de manière non linéaire (voir cet article pour plus de détails ).

Les lignes 3 à 6 fournissent un moyen très intéressant d'associer cette profondeur matérielle [1.0 - 0.0] à des valeurs [presque éloignées] (nous les avons définies à l'étape MatrixPerspectiveFov). Considérez les valeurs du tampon constant:


Ayant la valeur «close» de 0,2 et la valeur de «far» 5000, nous pouvons calculer les valeurs de cb12_v21.xy comme suit:

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


Ce morceau de code est assez courant dans les shaders TW3, donc je pense que c'est juste une fonction.

Après avoir obtenu la «profondeur de la pyramide de visibilité», la ligne 7 utilise l'échelle / distorsion pour créer le coefficient d'interpolation (ici on utilise saturé pour limiter les valeurs à l'intervalle [0-1]).


cb3_v1.xy et cb3_v2.xy - il s'agit de la luminosité de l'effet de netteté à courte et longue distance. Appelons-les sharpenNear et sharpenFar. Et c'est la seule différence entre les options «Low» et «High» de cet effet dans The Witcher 3.

Il est maintenant temps d'utiliser le rapport résultant. Les lignes 8-9 ne font que lerp(sharpenNear, sharpenFar, interpolationCoeff) . À quoi ça sert? Grâce à cela, nous obtenons une luminosité différente près de Geralt et loin de lui. Jetez un oeil:


Peut-être que cela est à peine perceptible, mais ici, nous avons interpolé en fonction de la distance la netteté de la luminosité à côté du lecteur (2.177151) et la luminosité de l'effet est très loin (1.91303). Après ce calcul, nous ajoutons 1,0 à la luminosité (ligne 10). Pourquoi est-ce nécessaire? Supposons que l'opération lerp indiquée ci-dessus nous donne 0,0. Après avoir ajouté 1.0, nous obtenons naturellement 1.0, et c'est une valeur qui n'affectera pas le pixel lors de la netteté. En savoir plus à ce sujet ci-dessous.

Pendant l’affûtage, nous ne voulons pas affecter le ciel. Ceci peut être réalisé en ajoutant une simple vérification conditionnelle:

// sharpen
float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;


Dans The Witcher 3, la valeur de la profondeur de pixel du ciel est de 1,0, nous l'utilisons donc pour obtenir une sorte de «filtre binaire» (fait intéressant: dans ce cas, l' étape ne fonctionnera pas correctement).

Nous pouvons maintenant multiplier la luminosité interpolée par un «filtre ciel»:


Cette multiplication est effectuée sur la ligne 13.

Exemple de code de shader:

// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;


Centre d'échantillonnage des pixels


SV_Position a un aspect qui sera important ici: un décalage d'un demi-pixel . Il s'avère que ce pixel dans le coin supérieur gauche (0, 0) a des coordonnées non pas (0, 0) en termes de SV_Position.xy, mais (0,5, 0,5). Ouah!

Ici, nous voulons prendre un échantillon au centre du pixel, alors regardons les lignes 14-16. Vous pouvez les écrire en HLSL:

// .
// "" SV_Position.xy.
float2 uvCenter = trunc( Input.Position.xy );

// ,
uvCenter += float2(0.5, 0.5);
uvCenter /= g_Viewport.xy


Et plus tard, nous échantillonnons la texture de couleur d'entrée de texcoords «uvCenter». Ne vous inquiétez pas, le résultat de l'échantillonnage sera le même qu'avec la méthode «normale» (SV_Position.xy / ViewportSize.xy).

Aiguiser ou ne pas aiguiser


La décision d'utiliser ou non la netteté dépend de fSharpenAmount.

//
float3 colorCenter = TexColorBuffer.SampleLevel( samplerLinearClamp, uvCenter, 0 ).rgb;

//
float3 finalColor = colorCenter;

if ( fSharpenAmount > 0 )
{
// sharpening...
}

return float4( finalColor, 1 );


Aiguiser


Il est temps de jeter un œil à l'intérieur de l'algorithme lui-même.

Essentiellement, il effectue les actions suivantes:

- échantillonne quatre fois la texture de couleur d'entrée aux coins du pixel,

- ajoute des échantillons et calcule la valeur moyenne,

- calcule la différence entre "center" et "cornerAverage",

- trouve la composante absolue maximale de la différence,

- corrige max. abs. composante utilisant des valeurs d'échelle + biais,

- détermine l'ampleur de l'effet en utilisant max. abs. composant

- calcule la valeur de luminosité (luma) pour "centerColor" et "averageColor",

- divise colorCenter en sa luma,

- calcule une nouvelle valeur de luma interpolée en fonction de l'ampleur de l'effet,

- Multiplie colorCenter par la nouvelle valeur de luma.

Beaucoup de travail, et c'était difficile pour moi de le comprendre, car je n'avais jamais expérimenté avec des filtres plus nets.

Commençons par le modèle d'échantillonnage. Comme vous pouvez le voir dans le code assembleur, quatre lectures de texture sont effectuées.

Cela sera mieux illustré à l'aide d'un exemple d'image pixel (le niveau de compétence de l'artiste est un expert ):


Toutes les lectures du shader utilisent l'échantillonnage bilinéaire (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).

Le décalage entre le centre et chacun des angles est de (± 0,5, ± 0,5), selon l'angle.

Voyez comment cela peut être mis en œuvre sur HLSL? Voyons voir:

float2 uvCorner;
float2 uvOffset = float2( 0.5, 0.5 ) / g_Viewport.xy; // remember about division!

float3 colorCorners = 0;

//
// -0,5, -0.5
uvCorner = uvCenter - uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// +0.5, -0.5
uvCorner = uvCenter + float2(uvOffset.x, -uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// -0.5, +0.5
uvCorner = uvCenter + float2(-uvOffset.x, uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;

//
// +0.5, +0.5
uvCorner = uvCenter + uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;


Donc, maintenant, les quatre échantillons sont résumés dans la variable «colorCorners». Suivons ces étapes:

//
float3 averageColorCorners = colorCorners / 4.0;

//
float3 diffColor = colorCenter - averageColorCorners;

// . . RGB-
float fDiffColorMaxComponent = max( abs(diffColor.x), max( abs(diffColor.y), abs(diffColor.z) ) );

//
float fDiffColorMaxComponentScaled = saturate( fDiffColorMaxComponent * sharpenLumScale + sharpenLumBias );

// .
// "1.0" - fSharpenIntensity 1.0.
float fPixelSharpenAmount = lerp(1.0, fSharpenAmount, fDiffColorMaxComponentScaled);

// "" .
float lumaCenter = dot( LUMINANCE_RGB, finalColor );
float lumaCornersAverage = dot( LUMINANCE_RGB, averageColorCorners );

// "centerColor"
float3 fColorBalanced = colorCenter / max( lumaCenter, 1e-4 );

//
float fPixelLuminance = lerp(lumaCornersAverage, lumaCenter, fPixelSharpenAmount);

//
finalColor = fColorBalanced * max(fPixelLuminance, 0.0);
}

return float4(finalColor, 1.0);


La reconnaissance des contours est effectuée en calculant max. abs. composante de différence. Déménagement intelligent! Découvrez sa visualisation:


Visualisation de la composante absolue maximale de la différence.

Super. Le shader HLSL fini est disponible ici . Désolé pour le formatage assez médiocre. Vous pouvez utiliser mon programme HLSLexplorer et expérimenter avec le code.

Je peux dire avec plaisir que le code ci-dessus crée le même code assembleur que dans le jeu!

Pour résumer: Le shader de netteté de Witcher 3 est très bien écrit (notez que fPixelSharpenAmount est supérieur à 1.0! C'est intéressant ...). De plus, le principal moyen de modifier la luminosité de l'effet est la luminosité des objets proches / éloignés. Dans ce jeu, ce ne sont pas des constantes; J'ai compilé plusieurs exemples de valeurs:

Skellige:

aiguisersharpenFarsharpenDistanceScalesharpenDistanceBiassharpenLumScalesharpenLumBias
faible
haut2.01,80,025
-0,25
-13,33333
1,33333

Kaer Morhen:

aiguiser
sharpenFar
sharpenDistanceScale
sharpenDistanceBias
sharpenLumScale
sharpenLumBias
faible
0,57751
0,31303
0,06665
-0.33256
-1,0
2.0
haut
2.17751
1,91303
0,06665
-0.33256
-1,0
2.0

Partie 7. Luminosité moyenne


L'opération de calcul de la luminosité moyenne de l'image actuelle peut être trouvée dans presque tous les jeux vidéo modernes. Cette valeur est souvent utilisée plus tard pour l'effet de l'adaptation oculaire et de la correction tonale (voir dans la partie précédente de l' article). Dans les solutions simples, le calcul de la luminosité est utilisé, par exemple, pour la texture 512 2 , puis le calcul de ses niveaux de mip et l'application de cette dernière. Cela fonctionne généralement, mais limite considérablement les possibilités. Des solutions plus complexes utilisent des shaders de calcul qui effectuent, par exemple, une réduction parallèle .

Voyons comment l'équipe de CD Projekt Red a résolu ce problème dans The Witcher 3. Dans la partie précédente, j'ai déjà examiné la correction tonale et l'adaptation de l'œil, donc la seule pièce restante du puzzle était la luminosité moyenne.

Pour commencer, le calcul de la luminosité moyenne de The Witcher 3 se compose de deux passes. Pour plus de clarté, j'ai décidé de les diviser en parties distinctes, et nous examinons d'abord le premier passage - «distribution de la luminosité» (calcul de l'histogramme de la luminosité).

Répartition de la luminosité


Ces deux passes sont assez faciles à trouver dans n'importe quel analyseur de trame. Ce sont les appels Dispatch dans l'ordre juste avant d'effectuer l'adaptation oculaire:


Regardons l'entrée pour ce pass. Il a besoin de deux textures:

1) Tampon couleur HDR, dont l'échelle est réduite à 1/4 x 1/4 (par exemple, de 1920x1080 à 480x270),

2) Tampon de profondeur plein écran


Tampon couleur 1/4 x 1/4 HDR. Notez l'astuce délicate - ce tampon fait partie d'un plus grand tampon. La réutilisation des tampons est une bonne pratique.


Tampon de profondeur plein écran

Pourquoi faire un zoom arrière sur le tampon de couleur? Je pense que c'est une question de performance.

Quant à la sortie de cette passe, c'est un buffer structuré. 256 éléments de 4 octets chacun.

Les shaders n'ont pas d'informations de débogage ici, alors supposez que ce n'est qu'un tampon de valeurs int non signées.

Important: la première étape du calcul de la luminosité moyenne appelle ClearUnorderedAccessViewUint pour remettre à zéro tous les éléments du tampon structuré.

Étudions le code assembleur du shader de calcul (c'est le premier shader de calcul de toute notre analyse!)

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[3], immediateIndexed
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_uav_structured u0, 4
dcl_input vThreadGroupID.x
dcl_input vThreadIDInGroup.x
dcl_temps 6
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: store_structured g0.x, vThreadIDInGroup.x, l(0), l(0)
1: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
2: store_structured g0.x, r0.x, l(0), l(0)
3: store_structured g0.x, r0.y, l(0), l(0)
4: store_structured g0.x, r0.z, l(0), l(0)
5: sync_g_t
6: ftoi r1.x, cb0[2].z
7: mov r2.y, vThreadGroupID.x
8: mov r2.zw, l(0, 0, 0, 0)
9: mov r3.zw, l(0, 0, 0, 0)
10: mov r4.yw, l(0, 0, 0, 0)
11: mov r1.y, l(0)
12: loop
13: utof r1.z, r1.y
14: ge r1.z, r1.z, cb0[0].x
15: breakc_nz r1.z
16: iadd r2.x, r1.y, vThreadIDInGroup.x
17: utof r1.z, r2.x
18: lt r1.z, r1.z, cb0[0].x
19: if_nz r1.z
20: ld_indexable(texture2d)(float,float,float,float) r5.xyz, r2.xyzw, t0.xyzw
21: dp3 r1.z, r5.xyzx, l(0.212600, 0.715200, 0.072200, 0.000000)
22: imul null, r3.xy, r1.xxxx, r2.xyxx
23: ld_indexable(texture2d)(float,float,float,float) r1.w, r3.xyzw, t1.yzwx
24: eq r1.w, r1.w, cb0[2].w
25: and r1.w, r1.w, cb0[2].y
26: add r2.x, -r1.z, cb0[2].x
27: mad r1.z, r1.w, r2.x, r1.z
28: add r1.z, r1.z, l(1.000000)
29: log r1.z, r1.z
30: mul r1.z, r1.z, l(88.722839)
31: ftou r1.z, r1.z
32: umin r4.x, r1.z, l(255)
33: atomic_iadd g0, r4.xyxx, l(1)
34: endif
35: iadd r1.y, r1.y, l(64)
36: endloop
37: sync_g_t
38: ld_structured r1.x, vThreadIDInGroup.x, l(0), g0.xxxx
39: mov r4.z, vThreadIDInGroup.x
40: atomic_iadd u0, r4.zwzz, r1.x
41: ld_structured r1.x, r0.x, l(0), g0.xxxx
42: mov r0.w, l(0)
43: atomic_iadd u0, r0.xwxx, r1.x
44: ld_structured r0.x, r0.y, l(0), g0.xxxx
45: atomic_iadd u0, r0.ywyy, r0.x
46: ld_structured r0.x, r0.z, l(0), g0.xxxx
47: atomic_iadd u0, r0.zwzz, r0.x
48: ret


Et un tampon constant:


Nous savons déjà que la première entrée est un tampon couleur HDR. Avec FullHD, sa résolution est de 480x270. Regardons l'appel Dispatch.

Dispatch (270, 1, 1) - cela signifie que nous exécutons 270 groupes de threads. Autrement dit, nous exécutons un groupe de threads par ligne du tampon de couleur.


Chaque groupe de threads exécute une ligne de mémoire tampon couleur HDR

Maintenant que nous avons ce contexte, essayons de comprendre ce que fait le shader.

Chaque groupe de threads a 64 threads dans la direction X (dcl_thread_group 64, 1, 1), ainsi qu'une mémoire partagée, 256 éléments de 4 octets chacun (dcl_tgsm_structured g0, 4, 256).

Notez que dans le shader, nous utilisons SV_GroupThreadID (vThreadIDInGroup.x) [0-63] et SV_GroupID (vThreadGroupID.x) [0-269].

1) Nous commençons par affecter à tous les éléments de la mémoire partagée des valeurs nulles. Étant donné que la mémoire totale contient 256 éléments et 64 threads par groupe, cela peut commodément être fait avec une simple boucle:

// - .
// 64 , 4 .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = 0;
}


2) Après cela, nous avons défini la barrière à l'aide de GroupMemoryBarrierWithGroupSync (sync_g_t). Nous faisons cela pour nous assurer que tous les threads dans la mémoire partagée des groupes sont remis à zéro avant de passer à l'étape suivante.

3) Maintenant, nous exécutons une boucle, qui peut être grossièrement écrite comme ceci:

// cb0_v0.x - . 1920x1080 1920/4 = 480;
float ViewportSizeX = cb0_v0.x;
[loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )
{
...


Il s'agit d'une simple boucle for avec un incrément de 64 (avez-vous déjà compris pourquoi?).

L'étape suivante consiste à calculer la position du pixel chargé.

Réfléchissons-y.

Pour la coordonnée Y, nous pouvons utiliser SV_GroupID.x car nous avons lancé 270 groupes de threads.

Pour la coordonnée X, nous ... pouvons profiter du flux de groupe actuel! Essayons de le faire.

Puisqu'il y a 64 threads dans chaque groupe, une telle solution contournera tous les pixels.

Considérez le groupe de threads (0, 0, 0).

- Le flux (0, 0, 0) traitera les pixels (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0), (384, 0), (448,0).

- Le flux (1, 0, 0) traitera les pixels (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0), (449, 0) ...

- Le flux (63, 0, 0) traitera les pixels (63, 0), (127, 0), (191, 0), (255, 0), (319, 0), (383, 0), (447, 0)

Ainsi, tous les pixels seront traités.

Nous devons également nous assurer que nous ne chargeons pas les pixels de l'extérieur du tampon de couleur:

// X. Y GroupID.
uint CurrentPixelPositionX = PositionX + threadID;
uint CurrentPixelPositionY = groupID;
if ( CurrentPixelPositionX < ViewportSizeX )
{
// HDR- .
// HDR- , .
uint2 colorPos = uint2(CurrentPixelPositionX, CurrentPixelPositionY);
float3 color = texture0.Load( int3(colorPos, 0) ).rgb;
float luma = dot(color, LUMA_RGB);


Tu vois? C'est assez simple!

J'ai également calculé la luminosité (ligne 21 du code assembleur).

Très bien, nous avons déjà calculé la luminosité à partir d'un pixel de couleur. L'étape suivante consiste à charger (et non à échantillonner!) La valeur de profondeur correspondante.

Mais ici, nous avons un problème, car nous avons connecté le tampon de profondeurs de pleine résolution. Que faire à ce sujet?

C'est étonnamment simple - multipliez simplement colorPos par une constante (cb0_v2.z). Nous avons réduit le tampon de couleur HDR à quatre reprises. donc la valeur est 4!

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


Jusqu'ici tout va bien! Mais ... nous sommes arrivés aux lignes 24-25 ...

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


Alors Nous avons d'abord une comparaison de l'égalité en virgule flottante, son résultat est écrit en r2.x, et juste après ça va ... quoi? Au niveau du bit et ?? Vraiment? Pour une valeur en virgule flottante? Que diable ???

Le problème 'eq + et'

Permettez-moi de dire que pour moi, c'était la partie la plus difficile du shader. J'ai même essayé d'étranges combinaisons asint / asfloat ...

Et si vous utilisez une approche légèrement différente? Faisons juste la comparaison flottante-flottante habituelle dans HLSL.

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


Et voici la sortie en code assembleur:

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


Intéressant, non? Je ne m'attendais pas à voir "et" ici.

0x3f800000 est juste 1.0f ... C'est logique parce que nous obtenons 1.0 et 0.0 sinon si la comparaison réussit.

Mais que se passe-t-il si nous «remplaçons» 1.0 par une autre valeur? Par exemple, comme ceci:

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


On obtient le résultat suivant:

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


Ha! Ça a marché. Ce n'est que la magie du compilateur HLSL. Remarque: si vous remplacez 0.0 par autre chose, vous obtenez simplement movc.

Revenons au shader de calcul. L'étape suivante consiste à vérifier que la profondeur est égale à cb0_v2.w. Il est toujours égal à 0,0 - en d'autres termes, nous vérifions si un pixel est sur un plan lointain (dans le ciel). Si c'est le cas, alors nous attribuons à ce coefficient une valeur, environ 0,5 (j'ai vérifié sur plusieurs images).

Ce coefficient calculé est utilisé pour interpoler entre la luminosité de la couleur et la luminosité du «ciel» (valeur cb0_v2.x, qui est souvent approximativement égale à 0,0). Je suppose que cela est nécessaire pour contrôler l'importance du ciel dans le calcul de la luminosité moyenne. Habituellement, l'importance est réduite. Idée très intelligente.

// , ( ). , ,
// .
float value = (depth == cb0_v2.w) ? cb0_v2.y : 0.0;

// 'value' 0.0, lerp 'luma'. 'value'
// ( 0.50), luma . (cb0_v2.x 0.0).
float lumaOk = lerp( luma, cb0_v2.x, value );


Puisque nous avons lumaOk, l'étape suivante consiste à calculer son logarithme naturel pour créer une bonne distribution. Mais attendez, disons que lumaOk est de 0,0. Nous savons que la valeur de log (0) n'est pas définie, nous ajoutons donc 1,0 car log (1) = 0,0.

Après cela, nous modifions le logarithme calculé à 128 pour le distribuer dans 256 cellules. Très intelligent!

Et c'est d'ici que cette valeur 88.722839 est prise. Il s'agit d'un 128 * (2) .

C'est juste la façon dont HLSL calcule les logarithmes.

Il n'y a qu'une seule fonction dans le code assembleur HLSL qui calcule les logarithmes: log , et elle a une base de 2.

// , lumaOk 0.0.
// log(0) undefined
// log(1) = 0.
//
lumaOk = log(lumaOk + 1.0);

// 128
lumaOk *= 128;


Enfin, nous calculons l'indice de la cellule à partir de la luminosité distribuée logarithmiquement et ajoutons 1 à la cellule correspondante dans la mémoire partagée.

// . Uint, 256 ,
// , .
uint uLuma = (uint) lumaOk;
uLuma = min(uLuma, 255);

// 1 .
InterlockedAdd( shared_data[uLuma], 1 );


L'étape suivante consistera à nouveau à définir une barrière pour garantir que tous les pixels de la ligne ont été traités.

Et la dernière étape consiste à ajouter des valeurs de la mémoire partagée au tampon structuré. Cela se fait de la même manière, à travers une simple boucle:

// ,
GroupMemoryBarrierWithGroupSync();

// .
[unroll] for (uint idx = 0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;

uint data = shared_data[offset];
InterlockedAdd( g_buffer[offset], data );
}


Une fois que les 64 threads du groupe de threads ont rempli les données communes, chaque thread ajoute 4 valeurs au tampon de sortie.

Considérez le tampon de sortie. Réfléchissons-y. La somme de toutes les valeurs dans le tampon est égale au nombre total de pixels! (à 480x270 = 129 600). Autrement dit, nous savons combien de pixels ont une valeur de luminosité spécifique.

Si vous êtes mal familiarisé avec les shaders informatiques (comme moi), alors au début, cela peut ne pas être clair, alors lisez le post plusieurs fois, prenez du papier et un crayon et essayez de comprendre les concepts sur lesquels cette technique est construite.

C'est tout! C'est ainsi que The Witcher 3 calcule un histogramme de luminosité. Personnellement, j'ai beaucoup appris en écrivant cette partie. Félicitations aux gars de CD Projekt Red pour leur excellent travail!

Si vous êtes intéressé par un shader HLSL complet, alors il est disponible ici . Je m'efforce toujours d'obtenir le code d'assemblage le plus près possible du jeu et je suis très heureux d'avoir réussi à nouveau!

Calcul de la luminosité moyenne


Il s'agit de la deuxième partie de l'analyse des calculs de luminosité moyenne dans "The Witcher 3: Wild Hunt".

Avant de nous lancer dans la bataille avec un autre shader de calcul, répétons brièvement ce qui s'est passé dans la dernière partie: nous avons travaillé avec un tampon de couleur HDR avec une échelle jusqu'à 1 / 4x1 / 4. Après la première passe, nous avons obtenu un histogramme de luminosité (tampon structuré de 256 valeurs entières non signées). Nous avons calculé le logarithme de la luminosité de chaque pixel, l'avons réparti sur 256 cellules et augmenté la valeur correspondante du tampon structuré de 1 par pixel. Pour cette raison, la somme totale de toutes les valeurs dans ces 256 cellules est égale au nombre de pixels.


Un exemple de la sortie de la première passe. Il y a 256 éléments.

Par exemple, notre tampon plein écran a une taille de 1920x1080. Après un zoom arrière, la première passe a utilisé un tampon 480x270. La somme des 256 valeurs du tampon sera égale à 480 * 270 = 129 600.

Après cette brève introduction, nous sommes prêts à passer à l'étape suivante: l'informatique.

Cette fois, un seul groupe de threads est utilisé (Dispatch (1, 1, 1)).

Regardons le code assembleur du shader de calcul:

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_uav_structured u0, 4
dcl_uav_typed_texture2d (float,float,float,float) u1
dcl_input vThreadIDInGroup.x
dcl_temps 4
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, vThreadIDInGroup.x, l(0), u0.xxxx
1: store_structured g0.x, vThreadIDInGroup.x, l(0), r0.x
2: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
3: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, r0.x, l(0), u0.xxxx
4: store_structured g0.x, r0.x, l(0), r0.w
5: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.y, l(0), u0.xxxx
6: store_structured g0.x, r0.y, l(0), r0.x
7: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.z, l(0), u0.xxxx
8: store_structured g0.x, r0.z, l(0), r0.x
9: sync_g_t
10: if_z vThreadIDInGroup.x
11: mul r0.x, cb0[0].y, cb0[0].x
12: ftou r0.x, r0.x
13: utof r0.y, r0.x
14: mul r0.yz, r0.yyyy, cb0[0].zzwz
15: ftoi r0.yz, r0.yyzy
16: iadd r0.x, r0.x, l(-1)
17: imax r0.y, r0.y, l(0)
18: imin r0.y, r0.x, r0.y
19: imax r0.z, r0.y, r0.z
20: imin r0.x, r0.x, r0.z
21: mov r1.z, l(-1)
22: mov r2.xyz, l(0, 0, 0, 0)
23: loop
24: breakc_nz r2.x
25: ld_structured r0.z, r2.z, l(0), g0.xxxx
26: iadd r3.x, r0.z, r2.y
27: ilt r0.z, r0.y, r3.x
28: iadd r3.y, r2.z, l(1)
29: mov r1.xy, r2.yzyy
30: mov r3.z, r2.x
31: movc r2.xyz, r0.zzzz, r1.zxyz, r3.zxyz
32: endloop
33: mov r0.w, l(-1)
34: mov r1.yz, r2.yyzy
35: mov r1.xw, l(0, 0, 0, 0)
36: loop
37: breakc_nz r1.x
38: ld_structured r2.x, r1.z, l(0), g0.xxxx
39: iadd r1.y, r1.y, r2.x
40: utof r2.x, r2.x
41: utof r2.w, r1.z
42: add r2.w, r2.w, l(0.500000)
43: mul r2.w, r2.w, l(0.011271)
44: exp r2.w, r2.w
45: add r2.w, r2.w, l(-1.000000)
46: mad r3.z, r2.x, r2.w, r1.w
47: ilt r2.x, r0.x, r1.y
48: iadd r2.w, -r2.y, r1.y
49: itof r2.w, r2.w
50: div r0.z, r3.z, r2.w
51: iadd r3.y, r1.z, l(1)
52: mov r0.y, r1.z
53: mov r3.w, r1.x
54: movc r1.xzw, r2.xxxx, r0.wwyz, r3.wwyz
55: endloop
56: store_uav_typed u1.xyzw, l(0, 0, 0, 0), r1.wwww
57: endif
58: ret


Il y a un tampon constant:


Jetons un coup d'œil au code assembleur: deux drones sont attachés (u0: tampon d'entrée de la première partie et u1: texture de sortie du format 1x1 R32_FLOAT). Nous voyons également qu'il y a 64 threads par groupe et 256 éléments de mémoire de groupe partagée sur 4 octets.

Nous commençons par remplir la mémoire partagée avec les données du tampon d'entrée. Nous avons 64 threads, vous devrez donc faire presque la même chose qu'avant.

Pour être absolument sûr que toutes les données ont été chargées pour un traitement ultérieur, nous mettons ensuite une barrière.

// - .
// 64 , 4
// .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = g_buffer[offset];
}
// , ,
// .
GroupMemoryBarrierWithGroupSync();


Tous les calculs sont effectués dans un seul thread, tous les autres sont simplement utilisés pour charger des valeurs du tampon dans la mémoire partagée.

Le flux "informatique" a un indice de 0. Pourquoi? Théoriquement, nous pouvons utiliser n'importe quel flux de l'intervalle [0-63], mais grâce à une comparaison avec 0, nous pouvons éviter des comparaisons entiers-entiers supplémentaires (instructions ieq ).

L'algorithme est basé sur l'indication de l'intervalle de pixels qui sera pris en compte dans l'opération.

À la ligne 11, nous multiplions largeur * hauteur pour obtenir le nombre total de pixels et multiplions-les par deux nombres de l'intervalle [0,0f-1,0f], indiquant le début et la fin de l'intervalle. D'autres restrictions sont utilisées pour garantir que 0 <= Start <= End <= totalPixels - 1 :

// 0.
[branch] if (threadID == 0)
{
//
uint totalPixels = cb0_v0.x * cb0_v0.y;

// (, , ),
// .
int pixelsToConsiderStart = totalPixels * cb0_v0.z;
int pixelsToConsiderEnd = totalPixels * cb0_v0.w;

int pixelsMinusOne = totalPixels - 1;

pixelsToConsiderStart = clamp( pixelsToConsiderStart, 0, pixelsMinusOne );
pixelsToConsiderEnd = clamp( pixelsToConsiderEnd, pixelsToConsiderStart, pixelsMinusOne );


Comme vous pouvez le voir, il y a deux cycles ci-dessous. Le problème avec eux (ou avec leur code assembleur) est qu'il y a d'étranges transitions conditionnelles aux extrémités des boucles. Il m'a été très difficile de les recréer. Jetez également un œil à la ligne 21. Pourquoi y a-t-il "-1"? Je vais l'expliquer un peu plus bas.

La tâche du premier cycle est d'éliminer pixelsToConsiderStart et de nous donner l'index de la cellule tampon dans laquelle pixelsToConsiderStart +1 est présent (ainsi que le nombre de tous les pixels dans les cellules précédentes).

Disons que pixelsToConsiderStart est approximativement égal à 30 000, et dans le tampon, il y a 37 000 pixels dans la cellule «zéro» (cela se produit dans le jeu la nuit). Par conséquent, nous voulons commencer l'analyse de la luminosité avec approximativement le pixel 30001, qui est présent dans la cellule "zéro". Dans ce cas, nous quittons immédiatement la boucle, obtenant l'index de départ «0» et zéro pixels ignorés.

Jetez un œil au code HLSL:

//
int numProcessedPixels = 0;

// [0-255]
int lumaValue = 0;

//
bool bExitLoop = false;

// - "pixelsToConsiderStart" .
// lumaValue, .
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];

// , lumaValue
int tempSum = numProcessedPixels + numPixels;

// , pixelsToConsiderStart, .
// , lumaValue.
// , pixelsToConsiderStart - "" , , .
[flatten]
if (tempSum > pixelsToConsiderStart)
{
bExitLoop = true;
}
else
{
numProcessedPixels = tempSum;
lumaValue++;
}
}


Le mystérieux nombre "-1" de la ligne 21 du code assembleur est associé à la condition booléenne pour l'exécution de la boucle (je l'ai découvert presque par accident).

Après avoir reçu le nombre de pixels des cellules lumaValue et lumaValue lui-même, nous pouvons passer au deuxième cycle.

La tâche du deuxième cycle est de calculer l'influence des pixels et de la luminosité moyenne.

Nous commençons avec lumaValue calculé dans la première boucle.

float finalAvgLuminance = 0.0f;

//
uint numProcessedPixelStart = numProcessedPixels;

// - .
// , , lumaValue.
// [0-255], , , ,
// pixelsToConsiderEnd.
// .
bExitLoop = false;
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];

//
numProcessedPixels += numPixels;

// , [0-255] (uint)
uint encodedLumaUint = lumaValue;

//
float numberOfPixelsWithCurrentLuma = numPixels;

// , [0-255] (float)
float encodedLumaFloat = encodedLumaUint;


À ce stade, nous avons obtenu la valeur de luminosité codée dans l'intervalle [0,0f-255.f].

Le processus de décodage est assez simple - vous devez inverser les calculs de l'étape de codage.

Une brève répétition du processus de codage:

float luma = dot( hdrPixelColor, float3(0.2126, 0.7152, 0.0722) );
...
float outLuma;

// log(0) undef, log(1) = 0
outLuma = luma + 1.0;

//
outLuma = log( outLuma );

// 128, log(1) * 128 = 0, log(2,71828) * 128 = 128, log(7,38905) * 128 = 256
outLuma = outLuma * 128

// uint
uint outLumaUint = min( (uint) outLuma, 255);


Pour décoder la luminosité, nous inversons le processus d'encodage, par exemple, comme ceci:

// 0.5f ( , )
float fDecodedLuma = encodedLumaFloat + 0.5;

// :

// 128
fDecodedLuma /= 128.0;

// exp(x), log(x)
fDecodedLuma = exp(fDecodedLuma);

// 1.0
fDecodedLuma -= 1.0;


Ensuite, nous calculons la distribution en multipliant le nombre de pixels avec une luminosité donnée par la luminosité décodée, et en les additionnant jusqu'à ce que nous arrivions au traitement des pixels pixelsToConsiderEnd .

Après cela, nous divisons l'effet total sur le nombre de pixels analysés.

Voici le reste de la boucle (et le shader): Le shader complet est disponible ici . Il est entièrement compatible avec mon programme HLSLexplorer , sans lequel je ne serais pas en mesure de recréer efficacement le calcul de la luminosité moyenne dans The Witcher 3 (et tous les autres effets aussi!). En conclusion, quelques réflexions. En termes de calcul de la luminosité moyenne, ce shader était difficile à recréer. Les principales raisons: 1) D'étranges contrôles «en attente» sur l'exécution du cycle, cela a pris beaucoup plus de temps que je ne le pensais auparavant. 2) Problèmes de débogage de ce shader de calcul dans RenderDoc (v. 1.2).

//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;

// () .
float tempTotalContribution = fCurrentLumaContribution + finalAvgLuminance;


[flatten]
if (numProcessedPixels > pixelsToConsiderEnd )
{
//
bExitLoop = true;

// , .
//
int diff = numProcessedPixels - numProcessedPixelStart;

//
finalAvgLuminance = tempTotalContribution / float(diff);
}
else
{
// lumaValue
finalAvgLuminance = tempTotalContribution;
lumaValue++;
}
}

//
g_avgLuminance[uint2(0,0)] = finalAvgLuminance;










Les opérations "ld_structured_indexable" ne sont pas entièrement prises en charge, bien que le résultat de la lecture de l'index 0 donne la valeur correcte, toutes les autres renvoient des zéros, c'est pourquoi les cycles se poursuivent indéfiniment.

Bien que je n'aie pas pu obtenir le même code d'assemblage que dans l'original (voir la capture d'écran ci-dessous pour les différences), en utilisant RenderDoc, j'ai pu injecter ce shader dans le pipeline - et les résultats étaient les mêmes!


Le résultat de la bataille. À gauche, mon shader, à droite, le code assembleur d'origine.

Partie 8. La lune et ses phases


Dans la huitième partie de l'article, j'explore le shader de lune de The Witcher 3 (et plus spécifiquement, de l'extension Blood and Wine).

La lune est un élément important du ciel nocturne, et il peut être difficile de la rendre crédible, mais pour moi, marcher la nuit en TW3 était un vrai plaisir.

Regardez cette scène!


Avant de passer au pixel shader, je dirai quelques mots sur les nuances du rendu. D'un point de vue géométrique, la Lune n'est qu'une sphère (voir ci-dessous), qui a des coordonnées de texture, des vecteurs normaux et tangents. Le vertex shader calcule la position dans l'espace mondial, ainsi que les vecteurs normalisés des normales, tangentes et tangentes à deux points (à l'aide d'un produit vectoriel), multipliés par la matrice mondiale.

Pour garantir que la lune se trouve complètement sur un plan éloigné, les champs MinDepth et MaxDepth de la structure D3D11_VIEWPORT se voient attribuer la valeur 0,0 (la même astuce utilisée pour le dôme du ciel). La lune est rendue immédiatement après le ciel.


La sphère utilisée pour dessiner la lune.

Eh bien, tout, je pense, vous pouvez continuer. Jetons un coup d'œil au pixel shader: La principale raison pour laquelle j'ai choisi un shader de Blood and Wine est simple - il est plus court. Tout d'abord, nous calculons le décalage pour échantillonner la texture. cb0 [0] .w est utilisé comme décalage le long de l'axe X. Avec cette astuce simple, nous pouvons simuler la rotation de la lune autour de son axe.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[267], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_input_ps linear v1.w
dcl_input_ps linear v2.xyzw
dcl_input_ps linear v3.xy
dcl_input_ps linear v4.xy
dcl_output o0.xyzw
dcl_temps 3
0: mov r0.x, -cb0[0].w
1: mov r0.y, l(0)
2: add r0.xy, r0.xyxx, v2.xyxx
3: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, t0.xyzw, s0
4: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
5: log r0.w, r0.w
6: mul r0.w, r0.w, l(2.200000)
7: exp r0.w, r0.w
8: add r0.xyz, r0.xyzx, r0.xyzx
9: dp3 r1.x, r0.xyzx, r0.xyzx
10: rsq r1.x, r1.x
11: mul r0.xyz, r0.xyzx, r1.xxxx
12: mul r1.xy, r0.yyyy, v3.xyxx
13: mad r0.xy, v4.xyxx, r0.xxxx, r1.xyxx
14: mad r0.xy, v2.zwzz, r0.zzzz, r0.xyxx
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
17: sincos r1.x, r2.x, r0.z
18: mov r2.y, r1.x
19: dp2_sat r0.x, r0.xyxx, r2.xyxx
20: mul r0.xyz, r0.xxxx, cb12[266].xyzx
21: mul r0.xyz, r0.xyzx, r0.wwww
22: mul r0.xyz, r0.xyzx, cb2[2].xyzx
23: add_sat r0.w, -v1.w, l(1.000000)
24: mul r0.w, r0.w, cb2[2].w
25: mul o0.xyz, r0.wwww, r0.xyzx
26: mov o0.w, l(0)
27: ret









Exemples de valeurs du tampon constant.

Une texture (1024x512) est attachée en entrée. La carte normale est codée dans les canaux RVB et la couleur de la surface de la lune dans le canal alpha. Intelligent!


Le canal alpha d'une texture est la couleur de la surface de la lune.


Les canaux RVB de texture sont une carte normale.

Après avoir reçu les coordonnées de texture correctes, nous échantillonnons les canaux RGBA. Nous devons décompresser la carte normale et effectuer une correction gamma de la couleur de la surface. Actuellement, un shader HLSL peut être écrit comme ceci, par exemple: L'étape suivante consiste à effectuer une liaison normale, mais uniquement dans les composants XY. (Dans The Witcher 3, l'axe Z est vers le haut et le canal Z entier de la texture est 1.0). Nous pouvons le faire de cette façon: il est maintenant temps pour ma partie préférée de ce shader. Regardez encore les lignes 15-16: Qu'est-ce que ce mystérieux 0,033864? Au début, il semble que cela n'a aucun sens, mais si nous calculons la valeur inverse, nous obtenons environ 29,53, ce qui est égal à la durée du mois synodique

float4 MoonPS(in InputStruct IN) : SV_Target0
{
// Texcoords
float2 uvOffsets = float2(-cb0_v0.w, 0.0);

// texcoords
float2 uv = IN.param2.xy + uvOffsets;

//
float4 sampledTexture = texture0.Sample( sampler0, uv);

// - -
float moonColorTex = pow(sampledTexture.a, 2.2 );

// [0,1] [-1,1].
// : sampledTexture.xyz * 2.0 - 1.0
float3 sampledNormal = normalize((sampledTexture.xyz - 0.5) * 2);




//
float3 Tangent = IN.param4.xyz;
float3 Normal = float3(IN.param2.zw, IN.param3.w);
float3 Bitangent = IN.param3.xyz;

// TBN
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);

// XY
// TBN float3x2: 3 , 2
float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;




15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)


en jours! C'est ce que j'attire l'attention aux détails!

Nous pouvons supposer de manière fiable que cb0 [0] .y est le nombre de jours qui se sont écoulés pendant le gameplay. Un écart supplémentaire est utilisé ici, utilisé comme décalage le long de l'axe x de la texture.

Ayant reçu ce coefficient, nous le multiplions par 2 * Pi.

Ensuite, en utilisant sincos, nous calculons un autre vecteur 2D.

En calculant le produit scalaire entre le vecteur normal et le vecteur "lune", une phase de la lune est simulée. Regardez les captures d'écran avec différentes phases de la lune:

// .
// days/29.53 + bias.
float phase = cb0_v0.y * (1.0 / SYNODIC_MONTH_LENGTH) + cb0_v0.w;

// 2*PI. , 29.53
// sin/cos.
phase *= TWOPI;

// .
float outSin = 0.0;
float outCos = 0.0;
sincos(phase, outSin, outCos);

//
float lunarPhase = saturate( dot(vNormal, float2(outCos, outSin)) );






La dernière étape consiste à effectuer une série d'opérations de multiplication pour calculer la couleur finale. Vous ne comprenez probablement pas pourquoi ce shader envoie une valeur alpha de 0,0 à la sortie. En effet, la lune est rendue avec le mélange activé:

// .

// cb12_v266.xyz , .
// (1.54, 2.82, 4.13)
float3 moonSurfaceGlowColor = cb12_v266.xyz;

float3 moonColor = lunarPhase * moonSurfaceGlowColor;
moonColor = moonColorTex * moonColor;

// cb_v2.xyz - , , , (1.0, 1.0, 1.0)
moonColor *= cb2_v2.xyz;

// , , . - .
// , ,
// .
float paramHorizon = saturate(1.0 - IN.param1.w);
paramHorizon *= cb2_v2.w;

moonColor *= paramHorizon;

//
return float4(moonColor, 0.0);





Cette approche vous permet d'obtenir la couleur d'arrière-plan (ciel) si ce shader revient en noir.

Si vous êtes intéressé par un shader complet, vous pouvez le prendre ici . Il a de grands tampons constants et devrait déjà être prêt pour l'injection dans RenderDoc au lieu du shader d'origine (renommez simplement «MoonPS» en «EditedShaderPS»).

Et le dernier: je voulais partager les résultats avec vous: à

gauche est mon shader, à droite est le shader original du jeu.

La différence est minime et n'affecte pas les résultats.


Comme vous pouvez le voir, ce shader était assez facile à recréer.

Partie 9. G-buffer


Dans cette partie, je vais révéler quelques détails du gbuffer dans The Witcher 3.

Nous supposerons que vous connaissez les bases de l'ombrage différé.

Brève répétition: l'idée de reporter n'est pas de calculer tous les éclairages et ombrages finis en une seule fois, mais de diviser les calculs en deux étapes.

Dans la première (passe de géométrie), nous remplissons le GBuffer de données de surface (position, normales, couleur spéculaire, etc. ...), et dans la seconde (passe d'éclairage), nous combinons tout et calculons l'éclairage.

L'ombrage différé est une approche très populaire car il vous permet de calculer en un seul passage en plein écran par des techniques telles que l' ombrage différé des carreaux , ce qui améliore considérablement les performances.

Autrement dit, GBuffer est un ensemble de textures aux propriétés géométriques. Il est très important de créer la bonne structure pour cela. Comme exemple de la vie réelle, vous pouvez étudier la technologie de rendu Crysis 3 .

Après cette brève introduction, regardons un exemple de cadre de The Witcher 3: Blood and Wine:


L'un des nombreux hôtels de Tussent.

Le GBuffer principal se compose de trois cibles de rendu plein écran au format DXGI_FORMAT_R8G8B8A8_UNORM et d'un tampon de profondeur + stencil au format DXGI_FORMAT_D24_UNORM_S8_UINT.

Voici leurs captures d'écran:


Rendu cible 0 - canaux RVB, couleur de surface


Rendu cible 0 - canal alpha. Honnêtement, je n'ai aucune idée de ce que sont ces informations.


Rendu cible 1 - canaux RVB. Les vecteurs normaux dans l'intervalle [0-1] sont enregistrés ici.


Rendu cible 1 - canal alpha. On dirait de la réflectivité!


Rendu cible 2 - canaux RVB. On dirait une couleur spéculaire!

Dans cette scène, le canal alpha est noir (mais plus tard, il est utilisé).


Profondeurs de tampon. Notez que la profondeur inversée est utilisée ici.


Tampon de gabarit utilisé pour marquer un type spécifique de pixel (par exemple, peau, végétation, etc.)

Ce n'est pas le GBuffer entier. La passe d'éclairage utilise également des sondes d'éclairage et d'autres tampons, mais je n'en discuterai pas dans cet article.

Avant de passer à la partie "principale" du poste, je donnerai des observations générales:

Observations générales



1) Le seul tampon à nettoyer est le tampon de profondeur / stencil.

Si vous analysez les textures mentionnées ci-dessus dans un bon analyseur d'images, vous serez un peu surpris, car elles n'utilisent pas l'appel «Clear», à l'exception de Depth / Stencil.

C'est-à-dire, en réalité, RenderTarget1 ressemble à ceci (notez les pixels «flous» sur le plan lointain):


Il s'agit d'une optimisation simple et intelligente.

Une leçon importante: vous devez dépenser des ressources pour les appels ClearRenderTargetView , donc utilisez-les uniquement lorsque cela est nécessaire.

2) la profondeur Inversé - il est cool

dans de nombreux articles déjà écrits sur l'exactitude de la mémoire tampon de profondeur avec virgule flottante. Witcher 3 utilise le z inversé. C'est le choix naturel pour un jeu aussi ouvert avec de longues distances de rendu.

Le passage à DirectX ne sera pas difficile:

a) Nous effaçons le tampon de profondeur en écrivant «0», pas «1».

Dans l'approche traditionnelle, la valeur extrême «1» a été utilisée pour effacer le tampon de profondeur. Après le saut de profondeur, la nouvelle valeur «distante» est devenue 0, vous devez donc tout changer.

b) Inversez les limites proches et éloignées lors du calcul de la matrice de projection

c) Changez la vérification de la profondeur de «moins» à «plus»

Pour OpenGL, un peu plus de travail doit être fait (voir les articles mentionnés ci-dessus), mais ça vaut le coup.

3) Nous ne gardons pas notre position dans le monde

, oui, tout est si simple. Au passage de l'éclairage, nous recréons une position dans le monde à partir des profondeurs.

Pixel shader


Dans cette partie, je voulais montrer exactement le pixel shader qui fournit les données de surface à GBuffer.

Alors maintenant, nous savons déjà comment stocker les couleurs, les normales et les spéculaires.

Bien sûr, tout n'est pas aussi simple qu'on pourrait le penser.

Le problème avec le pixel shader est qu'il a de nombreuses options. Ils diffèrent par le nombre de textures qui leur sont transférées et le nombre de paramètres utilisés à partir du tampon constant (probablement du tampon constant décrivant le matériau).

Pour l'analyse, j'ai décidé d'utiliser ce beau canon:


Notre baril héroïque!

Veuillez accueillir les textures:


Nous avons donc un albédo, une carte normale et une couleur spéculaire. Boîtier assez standard.

Avant de commencer, quelques mots sur l'entrée géométrique: La

géométrie est transmise avec des tampons de position, texcoords, normaux et tangents.

Le vertex shader produit au moins des texcoords, vecteurs tangents / normaux / tangents normalisés sur deux points, précédemment multipliés par la matrice mondiale. Pour les matériaux plus complexes (par exemple, avec deux cartes diffuses ou deux cartes normales), le vertex shader peut produire d'autres données, mais je voulais montrer un exemple simple ici.

Pixel shader dans le code assembleur: un shader se compose de plusieurs étapes. Je décrirai chaque partie principale de ce shader séparément.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[3], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t2
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 3
0: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, v1.xyxx, t1.xyzw, s0
1: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t0.xyzw, s0
2: add r1.w, r1.y, r1.x
3: add r1.w, r1.z, r1.w
4: mul r2.x, r1.w, l(0.333300)
5: add r2.y, l(-1.000000), cb4[1].x
6: mul r2.y, r2.y, l(0.500000)
7: mov_sat r2.z, r2.y
8: mad r1.w, r1.w, l(-0.666600), l(1.000000)
9: mad r1.w, r2.z, r1.w, r2.x
10: mul r2.xzw, r1.xxyz, cb4[0].xxyz
11: mul_sat r2.xzw, r2.xxzw, l(1.500000, 0.000000, 1.500000, 1.500000)
12: mul_sat r1.w, abs(r2.y), r1.w
13: add r2.xyz, -r1.xyzx, r2.xzwx
14: mad r1.xyz, r1.wwww, r2.xyzx, r1.xyzx
15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
21: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r1.xyz, v3.xyzx, r0.xxxx, r1.xyzx
27: mad r0.xyz, v2.xyzx, r0.zzzz, r1.xyzx
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
46: dp3 r0.w, r0.xyzx, r0.xyzx
47: rsq r0.w, r0.w
48: mul r0.xyz, r0.wwww, r0.xyzx
49: max r0.w, abs(r0.y), abs(r0.x)
50: max r0.w, r0.w, abs(r0.z)
51: lt r1.xy, abs(r0.zyzz), r0.wwww
52: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
53: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
54: lt r1.z, r1.y, r1.x
55: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
56: div r1.z, r1.y, r1.x
57: div r0.xyz, r0.xyzx, r0.wwww
58: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
59: mul r0.xyz, r0.wwww, r0.xyzx
60: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
61: mov o0.w, cb4[2].x
62: mov o2.w, l(0)
63: ret




Mais d'abord, comme d'habitude - une capture d'écran avec les valeurs du tampon constant:


Albedo


Nous commencerons par des choses complexes. Ce n'est pas seulement «OutputColor.rgb = Texture.Sample (uv) .rgb»

Après avoir échantillonné la texture de couleur RVB (ligne 1), les 14 lignes suivantes sont ce que j'appelle le «tampon de réduction de saturation». Permettez-moi de vous montrer le code HLSL: pour la plupart des objets, ce code ne fait que renvoyer la couleur d'origine de la texture. Ceci est réalisé par les valeurs correspondantes de «tampon de matériau». cb4_v1.x a une valeur de 1,0, qui renvoie un masque de 0,0 et renvoie la couleur d'entrée de l'instruction lerp . Il existe cependant quelques exceptions. Le plus grand facteur de désaturation que j'ai trouvé est 4,0 (il n'est jamais inférieur à 1,0) et desaturéCouleur

float3 albedoColorFilter( in float3 color, in float desaturationFactor, in float3 desaturationValue )
{
float sumColorComponents = color.r + color.g + color.b;

float averageColorComponentValue = 0.3333 * sumColorComponents;
float oneMinusAverageColorComponentValue = 1.0 - averageColorComponentValue;

float factor = 0.5 * (desaturationFactor - 1.0);

float avgColorComponent = lerp(averageColorComponentValue, oneMinusAverageColorComponentValue, saturate(factor));
float3 desaturatedColor = saturate(color * desaturationValue * 1.5);

float mask = saturate( avgColorComponent * abs(factor) );

float3 finalColor = lerp( color, desaturatedColor, mask );
return finalColor;
}




Dépend du matériau. Cela peut être quelque chose comme (0,2, 0,3, 0,4); Il n'y a pas de règles strictes. Bien sûr, je ne pouvais pas aider mais réaliser cela dans son propre DX11-cadre, et voici les résultats, où toutes les valeurs desaturatedColor égale float3 (0,25, 0,3, 0,45)


desaturationFactor = 1.0 (n'a aucun effet)


desaturationFactor = 2.0


desaturationFactor = 3.0


desaturationFactor = 4.0

Je suis sûr que ce n'est qu'une application de paramètres matériels, mais pas effectuée à la fin de la partie albédo.

Les lignes 15-20 ajoutent la touche finale: v0.z est la sortie du vertex shader, et elles sont nulles. N'oubliez pas, car v0.z sera utilisé plus tard à quelques reprises. Il semble que ce soit une sorte de coefficient, et tout le code ressemble à un petit albédo gradateur, mais comme v0.z vaut 0, la couleur reste inchangée. HLSL: En ce qui concerne RT0.a, comme nous pouvons le voir, il est tiré du tampon de constante matérielle, mais comme le shader n'a pas d'informations de débogage, il est difficile de dire de quoi il s'agit. Peut-être la translucidité? Nous avons terminé avec la première cible de rendu!

15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx






/* ALBEDO */
// (?)
float3 albedoColor = albedoColorFilter( colorTex, cb4_v1.x, cb4_v0.rgb );
float albedoMaxComponent = getMaxComponent( albedoColor );

// ,
// "paramZ" 0
float paramZ = Input.out0.z; // , 0

// , 0.70 0.85
// lerp, .
float param = (albedoMaxComponent > 0.22) ? 0.70 : 0.85;
float mulParam = lerp(1, param, paramZ);

//
pout.RT0.rgb = albedoColor * mulParam;
pout.RT0.a = cb4_v2.x;






Normal


Commençons par déballer la carte normale, puis, comme d'habitude, nous lierons les normales: Jusqu'à présent, rien de surprenant. Regardez les lignes 28-33: Nous pouvons les écrire grossièrement comme suit: Je ne sais pas si c'est correct d'écrire. Si vous savez ce qu'est cette opération mathématique, faites-le moi savoir. Nous voyons que le pixel shader utilise SV_IsFrontFace.

/* */
float3 sampledNormal = ((normalTex.xyz - 0.5) * 2);

// TBN
float3 Tangent = Input.TangentW.xyz;
float3 Normal = Input.NormalW.xyz;
float3 Bitangent;
Bitangent.x = Input.out0.w;
Bitangent.yz = Input.out1.zw;

// ; , , normal-tbn
// 'mad' 'mov'
Bitangent = saturate(Bitangent);

float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
float3 normal = mul( sampledNormal, TBN );






28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif




[branch] if (bIsFrontFace <= 0)
{
float cosTheta = dot(Input.NormalW, normal);
float3 invNormal = cosTheta * Input.NormalW;
normal = normal - 2*invNormal;
}






Qu'est ce que c'estLa documentation vient pour aider (je voulais écrire "msdn", mais ...):

Détermine si le triangle regarde la caméra. Pour les lignes et les points, IsFrontFace est vrai. Une exception est les lignes tracées à partir de triangles (mode filaire), qui définissent IsFrontFace de manière similaire à la pixellisation d'un triangle en mode solide. L'écriture peut être effectuée par un shader de géométrie et sa lecture par un shader de pixels.

Je voulais le vérifier par moi-même. Et en fait, l'effet n'est perceptible qu'en mode filaire. Je crois que ce morceau de code est nécessaire pour le calcul correct des normales (et donc de l'éclairage) en mode filaire.

Voici une comparaison: à la fois les couleurs de l'image de la scène finie avec cette astuce on / off, ainsi que la texture des normales gbuffer [0-1] avec l'astuce on / off:


La couleur de la scène sans truc


Scène de couleur avec cascade


Normal [0-1] pas de truc


Normal [0-1] avec une astuce

Avez-vous remarqué que chaque cible de rendu dans GBuffer a le format R8G8B8A8_UNORM? Cela signifie qu'il existe 256 valeurs possibles par composant. Est-ce suffisant pour stocker des normales?

Le stockage de normales de haute qualité avec suffisamment d'octets dans Gbuffer est un problème connu, mais heureusement, il existe de nombreux matériaux différents à apprendre . Peut-être que certains d'entre vous savent déjà quelle technique est utilisée ici. Je dois dire que dans tout le passage de la géométrie, il y a une texture supplémentaire attachée à la fente 13 ...:





Ha!Le Witcher 3 utilise une technique appelée « Normales Best Fit ». Ici je ne l'expliquerai pas en détail (voir présentation). Il a été inventé vers 2009-2010 par Crytek, et puisque CryEngine a l'open source, BFN est également open source .

BFN donne à la texture des normales un aspect "granuleux".

Après avoir mis à l'échelle les normales à l'aide de BFN, nous les recodons de l'intervalle [-1; 1] à [0, 1].

Spéculaire


Commençons par la ligne 34 et échantillonnons la texture spéculaire: Comme vous pouvez le voir, il existe un filtre de «gradation» que nous connaissons chez Albedo: nous calculons la composante avec max. valeur, puis calculez la couleur «assombrie» et interpolez-la avec la couleur spéculaire d'origine, en prenant le paramètre du vertex shader ... qui est 0, donc à la sortie, nous obtenons la couleur de la texture. HLSL:

34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx








/* SPECULAR */
float3 specularTex = texture2.Sample( samplerAnisoWrap, Texcoords ).rgb;

// , Albedo. . ,
// - "".
// paramZ 0,
// .
float specularMaxComponent = getMaxComponent( specularTex );
float3 specB = (specularMaxComponent > 0.2) ? specularTex : float3(0.12, 0.12, 0.12);
float3 finalSpec = lerp(specularTex, specB, paramZ);
pout.RT2.xyz = finalSpec;


Réflectivité


Je n'ai aucune idée si ce nom convient à ce paramètre, car je ne sais pas comment il affecte le passage de l'éclairage. Le fait est que le canal alpha de la carte normale d'entrée contient des données supplémentaires:


Texture du canal alpha "carte normale".

Code assembleur: dites bonjour à notre vieil ami - v0.z! Sa signification est similaire à l'albédo et au spéculaire:

41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w




/* REFLECTIVITY */
float reflectivity = normalTex.a;
float reflectivity2 = (reflectivity < 0.33) ? (reflectivity * 0.95) : 0.33;

float finalReflectivity = lerp(reflectivity, reflectivity2, paramZ);
pout.RT1.a = finalReflectivity;


Super!C'est la fin de l'analyse de la première version du pixel shader.

Voici une comparaison de mon shader (à gauche) avec l'original (à droite):


Ces différences n'affectent pas les calculs, donc mon travail ici est terminé.

Pixel Shader: Albedo + Option normale


J'ai décidé de montrer une option de plus, maintenant uniquement avec l'albédo et les cartes normales, sans texture spéculaire. Le code assembleur est légèrement plus long: La différence entre cette option et les options précédentes est la suivante: a) lignes 1, 19 : le paramètre d'interpolation v0.z est multiplié par cb4 [0] .x à partir du tampon constant, mais ce produit n'est utilisé que pour l'alpédo d'interpolation sur la ligne 19. Pour les autres sorties, la valeur "normale" de v0.z est utilisée. b) lignes 54-55 : o2.w est maintenant réglé à la condition que (cb4 [7] .x> 0.0) Nous reconnaissons déjà ce modèle «une sorte de comparaison - ET» à partir du calcul de l'histogramme de luminosité. Cela peut s'écrire comme suit: c) lignes 34-42 : un calcul spéculaire complètement différent.

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 4
0: mul r0.x, v0.z, cb4[0].x
1: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, v1.xyxx, t1.xyzw, s0
2: sample_indexable(texture2d)(float,float,float,float) r0.yzw, v1.xyxx, t0.wxyz, s0
3: add r2.x, r0.z, r0.y
4: add r2.x, r0.w, r2.x
5: add r2.z, l(-1.000000), cb4[2].x
6: mul r2.yz, r2.xxzx, l(0.000000, 0.333300, 0.500000, 0.000000)
7: mov_sat r2.w, r2.z
8: mad r2.x, r2.x, l(-0.666600), l(1.000000)
9: mad r2.x, r2.w, r2.x, r2.y
10: mul r3.xyz, r0.yzwy, cb4[1].xyzx
11: mul_sat r3.xyz, r3.xyzx, l(1.500000, 1.500000, 1.500000, 0.000000)
12: mul_sat r2.x, abs(r2.z), r2.x
13: add r2.yzw, -r0.yyzw, r3.xxyz
14: mad r0.yzw, r2.xxxx, r2.yyzw, r0.yyzw
15: max r2.x, r0.w, r0.z
16: max r2.x, r0.y, r2.x
17: lt r2.x, l(0.220000), r2.x
18: movc r2.x, r2.x, l(-0.300000), l(-0.150000)
19: mad r0.x, r0.x, r2.x, l(1.000000)
20: mul o0.xyz, r0.xxxx, r0.yzwy
21: add r0.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r0.xyw, v3.xyxz, r0.xxxx, r1.xyxz
27: mad r0.xyz, v2.xyzx, r0.zzzz, r0.xywx
28: uge r0.w, l(0), v4.x
29: if_nz r0.w
30: dp3 r0.w, v2.xyzx, r0.xyzx
31: mul r1.xyz, r0.wwww, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
43: max r0.w, r1.z, r1.y
44: max r0.w, r0.w, r1.x
45: lt r0.w, l(0.200000), r0.w
46: movc r2.xyz, r0.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
47: add r2.xyz, -r1.xyzx, r2.xyzx
48: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
49: lt r0.w, r1.w, l(0.330000)
50: mul r1.x, r1.w, l(0.950000)
51: movc r0.w, r0.w, r1.x, l(0.330000)
52: add r0.w, -r1.w, r0.w
53: mad o1.w, v0.z, r0.w, r1.w
54: lt r0.w, l(0), cb4[7].x
55: and o2.w, r0.w, l(0.064706)
56: dp3 r0.w, r0.xyzx, r0.xyzx
57: rsq r0.w, r0.w
58: mul r0.xyz, r0.wwww, r0.xyzx
59: max r0.w, abs(r0.y), abs(r0.x)
60: max r0.w, r0.w, abs(r0.z)
61: lt r1.xy, abs(r0.zyzz), r0.wwww
62: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
63: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
64: lt r1.z, r1.y, r1.x
65: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
66: div r1.z, r1.y, r1.x
67: div r0.xyz, r0.xyzx, r0.wwww
68: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
69: mul r0.xyz, r0.wwww, r0.xyzx
70: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
71: mov o0.w, cb4[6].x
72: ret










pout.RT2.w = (cb4_v7.x > 0.0) ? (16.5/255.0) : 0.0;



Il n'y a pas de texture spéculaire ici. Regardons le code assembleur responsable de cette partie: Notez que nous avons utilisé ici (1 - capacité réfléchie). Heureusement, écrire ceci en HLSL est assez simple: j'ajouterai que dans cette version, le tampon constant avec les données matérielles est légèrement plus grand. Ici, ces valeurs supplémentaires sont utilisées pour émuler la couleur spéculaire. Le reste du shader est le même que dans la version précédente. 72 lignes de code assembleur sont trop pour être affichées dans WinMerge, alors croyez-moi: mon code s'est avéré être presque le même que dans l'original. Ou vous pouvez télécharger mon HLSLexplorer et voir par vous-même!

34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx




float oneMinusReflectivity = 1.0 - normalTex.a;
float3 specularTex = pow(cb4_v3.rgb, 2.2);
oneMinusReflectivity = oneMinusReflectivity * cb4_v4.x + cb4_v5.x;
specularTex = saturate(specularTex * oneMinusReflectivity);
specularTex = pow(specularTex, 1.0/2.2);

// ...
float specularMaxComponent = getMaxComponent( specularTex );
...








Pour résumer


... et si vous le lisez ici, alors vous voudrez probablement aller un peu plus loin.

Ce qui semble simple dans la vie réelle n'est souvent pas le cas, et le transfert de données vers gbuffer The Witcher 3 n'a pas fait exception. Je ne vous ai montré que les versions les plus simples des pixel shaders responsables, et j'ai également donné des observations générales concernant l'ombrage différé en général.

Pour les plus patients, il existe deux options pour les pixel shaders dans pastebin:

Option 1 - avec texture spéculaire

Option 2 - sans texture spéculaire

Partie 10. Rideaux de pluie au loin


Dans cette partie, nous allons examiner un merveilleux effet atmosphérique que j'aime beaucoup - des rideaux de pluie / lumière lointains près de l'horizon. Dans le jeu, ils sont plus faciles à rencontrer sur les îles Skellig.


Personnellement, j'aime vraiment ce phénomène atmosphérique et j'étais curieux de savoir comment les programmeurs graphiques de CD Projekt Red l'ont implémenté. Voyons ça!

Voici deux captures d'écran avant et après l'application des rideaux de pluie:


Aux rideaux de pluie


Après les rideaux de pluie

Géométrie


Tout d'abord, nous nous concentrerons sur la géométrie. L'idée est d'utiliser un petit cylindre:


Un cylindre dans l'espace local

Du point de vue de sa position dans l'espace local, il est assez petit - sa position est dans la plage (0,0 - 1,0).

Le circuit d'entrée de cet appel de tirage ressemble à ceci ...


Les éléments suivants sont importants pour nous ici: Texcoords et Instance_Transform.

Les Texcoords sont enveloppés tout simplement: U des bases supérieure et inférieure sont dans l'intervalle [0,02777 - 1,02734]. V sur la base inférieure est de 1,0 et sur la partie supérieure - 0,0. Comme vous pouvez le voir, vous pouvez tout simplement créer ce maillage même de manière procédurale.

Ayant reçu ce petit cylindre dans l'espace local, nous le multiplions par la matrice mondiale fournie pour chaque instance de l'élément d'entrée INSTANCE_TRANSFORM. Vérifions les valeurs de cette matrice:




Ça a l'air assez effrayant, non? Mais ne vous inquiétez pas, nous analyserons cette matrice et verrons ce qu'elle cache! Les résultats sont très intéressants: il est important de connaître la position de la caméra dans ce cadre particulier: (-116.5338, 234.8695, 2.09) Comme vous pouvez le voir, nous avons redimensionné le cylindre pour le rendre assez grand dans l'espace mondial (dans TW3, l'axe Z est vers le haut), déplacé par rapport à la position de la caméra , et se tourna. Voici à quoi ressemble le cylindre après la conversion avec le vertex shader:

XMMATRIX mat( -227.7472, 159.8043, 374.0736, -116.4951,
-194.7577, -173.3836, -494.4982, 238.6908,
-14.16466, -185.4743, 784.564, -1.45565,
0.0, 0.0, 0.0, 1.0 );

mat = XMMatrixTranspose( mat );

XMVECTOR vScale;
XMVECTOR vRotateQuat;
XMVECTOR vTranslation;
XMMatrixDecompose( &vScale, &vRotateQuat, &vTranslation, mat );

// ...
XMMATRIX matRotate = XMMatrixRotationQuaternion( vRotateQuat );




vRotateQuat: (0.0924987569, -0.314900011, 0.883411944, -0.334462732)

vScale: (299.999969, 300.000000, 1000.00012)

vTranslation: (-116.495102, 238.690796, -1.45564997)









Cylindre après conversion par vertex shader. Voyez comment il est situé par rapport à la pyramide de visibilité.

Vertex shader


La géométrie d'entrée et le vertex shader dépendent strictement l'un de l'autre.

Examinons de plus près le code assembleur pour le vertex shader: avec les Texcoords simples passant (ligne 0) et Instance_LOD_Params (ligne 8), deux autres éléments sont nécessaires pour la sortie: SV_Position (c'est évident) et Hauteur (composant .z) de la position dans le monde. Rappelez-vous que l'espace local est dans la plage [0-1]? Donc, juste avant d'appliquer la matrice mondiale, le vertex shader utilise l'échelle et la déviation pour changer la position locale. Déménagement intelligent! Dans ce cas, scale = float3 (4, 4, 2), et bias = float3 (-2, -2, -1). < Le motif qui se remarque entre les lignes 9 et 28 est la multiplication de deux matrices de rangées principales. Regardons simplement le vertex shader fini sur HLSL:

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












cbuffer cbPerFrame : register (b1)
{
row_major float4x4 g_viewProjMatrix;
row_major float4x4 g_rainShaftsViewProjMatrix;
}

cbuffer cbPerObject : register (b2)
{
float4x4 g_mtxWorld;
float4 g_modelScale;
float4 g_modelBias;
}

struct VS_INPUT
{
float3 PositionW : POSITION;
float2 Texcoord : TEXCOORD;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float4 InstanceTransform0 : INSTANCE_TRANSFORM0;
float4 InstanceTransform1 : INSTANCE_TRANSFORM1;
float4 InstanceTransform2 : INSTANCE_TRANSFORM2;
float4 InstanceLODParams : INSTANCE_LOD_PARAMS;
};

struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;

float4 LODParams : LODParams;
float4 PositionH : SV_Position;
};

VS_OUTPUT RainShaftsVS( VS_INPUT Input )
{
VS_OUTPUT Output = (VS_OUTPUT)0;

//
Output.TexcoordAndZ.xy = Input.Texcoord;
Output.LODParams = Input.InstanceLODParams;

//
float3 meshScale = g_modelScale.xyz; // float3( 4, 4, 2 );
float3 meshBias = g_modelBias.xyz; // float3( -2, -2, -1 );
float3 PositionL = Input.PositionW * meshScale + meshBias;

// instanceWorld float4s:
float4x4 matInstanceWorld = float4x4(Input.InstanceTransform0, Input.InstanceTransform1,
Input.InstanceTransform2 , float4(0, 0, 0, 1) );

// (.z)
float4x4 matWorldInstanceLod = mul( g_rainShaftsViewProjMatrix, matInstanceWorld );
Output.TexcoordAndZ.z = mul( float4(PositionL, 1.0), transpose(matWorldInstanceLod) ).z;

// SV_Posiiton
float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );
Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );

return Output;
}


Comparaison de mon shader (à gauche) et de l'original (à droite):


Les différences n'affectent pas les calculs. J'ai injecté mon shader dans le cadre et tout allait bien!

Pixel shader


Enfin! Pour commencer, je vais vous montrer l'entrée:

Deux textures sont utilisées ici: la texture de bruit et le tampon de profondeur:



Valeurs des tampons constants:





Et le code assembleur pour le pixel shader:

ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[8], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s15, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t15
dcl_input_ps linear v0.xyz
dcl_input_ps linear v1.w
dcl_input_ps_siv v2.xy, position
dcl_output o0.xyzw
dcl_temps 1
0: mul r0.xy, cb0[0].xxxx, cb4[5].xyxx
1: mad r0.xy, v0.xyxx, cb4[4].xyxx, r0.xyxx
2: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t0.xyzw, s0
3: add r0.y, -cb4[2].x, cb4[3].x
4: mad_sat r0.x, r0.x, r0.y, cb4[2].x
5: mul r0.x, r0.x, v0.y
6: mul r0.x, r0.x, v1.w
7: mul r0.x, r0.x, cb4[1].x
8: mul r0.yz, v2.xxyx, cb0[1].zzwz
9: sample_l(texture2d)(float,float,float,float) r0.y, r0.yzyy, t15.yxzw, s15, l(0)
10: mad r0.y, r0.y, cb12[22].x, cb12[22].y
11: mad r0.y, r0.y, cb12[21].x, cb12[21].y
12: max r0.y, r0.y, l(0.000100)
13: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
14: add r0.y, r0.y, -v0.z
15: mul_sat r0.y, r0.y, cb4[6].x
16: mul_sat r0.x, r0.y, r0.x
17: mad r0.y, cb0[7].y, r0.x, -r0.x
18: mad r0.x, cb4[7].x, r0.y, r0.x
19: mul r0.xyz, r0.xxxx, cb4[0].xyzx
20: log r0.xyz, r0.xyzx
21: mul r0.xyz, r0.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
22: exp r0.xyz, r0.xyzx
23: mul r0.xyz, r0.xyzx, cb2[2].xyzx
24: mul o0.xyz, r0.xyzx, cb2[2].wwww
25: mov o0.w, l(0)
26: ret


Ouah!Beaucoup, mais en fait, tout n'est pas si mal.

Que se passe-t-il ici? Tout d'abord, nous calculons les UV animés en utilisant le temps écoulé depuis cbuffer (cb0 [0] .x) et l'échelle / décalages. Ces texcoords sont utilisés pour échantillonner à partir de la texture du bruit (ligne 2).

Après avoir reçu la valeur de bruit de la texture, nous interpolons entre les valeurs min / max (généralement 0 et 1).

Ensuite, nous faisons la multiplication, par exemple, par la coordonnée de la texture V (rappelez-vous que la coordonnée V va de 1 à 0?) - ligne 5.

Ainsi, nous avons calculé le "masque de luminosité" - il ressemble à ceci:


A noter que des objets éloignés (un phare, des montagnes ...) ont disparu. Cela est arrivé parce que le cylindre réussit le test de profondeur - le cylindre n'est pas sur le plan lointain et est dessiné au-dessus de ces objets:


Test de profondeur

Nous voulons simuler que le rideau de pluie est plus éloigné (mais pas nécessairement sur le plan lointain). Pour ce faire, nous calculons un autre masque, le «masque d'objets distants».

Il est calculé par la formule suivante:

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

(0,001 est pris dans le tampon), ce qui nous donne le masque souhaité:


(Dans la partie sur l'effet de netteté, j'ai déjà expliqué superficiellement comment la profondeur de la pyramide de visibilité est extraite du tampon de profondeur.)

Personnellement, il me semble que cet effet pourrait être réalisé moins cher sans calculer la hauteur dans l'espace mondial en multipliant la profondeur de la pyramide de visibilité par un nombre plus petit, par exemple 0,0004.

Lorsque les deux masques sont multipliés, le dernier est obtenu:


Après avoir reçu ce masque final (ligne 16), nous effectuons une autre interpolation, qui ne fait presque rien (au moins dans le cas testé), puis multiplions le masque final par la couleur des rideaux (ligne 19), effectuons une correction gamma (lignes 20 -22) et multiplications finales (23-24).

À la fin, nous retournons une couleur avec une valeur alpha nulle. En effet, le mélange est activé sur cette passe:

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

si vous ne comprenez pas très bien comment fonctionne le mélange, voici une courte explication:

SourceColor est la sortie RVB du pixel shader et DestColor est la couleur RVB actuelle du pixel dans la cible de rendu. . Depuis SourceAlpha toujours égal à 0,0, l'équation ci - dessus se réduit à: FinalColor = SourceColor + DestColor.

Autrement dit, nous effectuons ici un mélange additif. Si le pixel shader revient (0, 0, 0), la couleur restera la même.

Voici le code HLSL fini - je pense qu'après l'explication, il sera beaucoup plus facile à comprendre: je peux dire avec plaisir que mon pixel shader crée le même code assembleur que dans l'original. J'espère que l'article vous a plu. Merci d'avoir lu!

struct VS_OUTPUT
{
float3 TexcoordAndWorldspaceHeight : Texcoord0;
float4 LODParams : LODParams; // float4(1,1,1,1)
float4 PositionH : SV_Position;
};

float getFrustumDepth( in float depth )
{
// from [1-0] to [0-1]
float d = depth * cb12_v22.x + cb12_v22.y;

// special coefficents
d = d * cb12_v21.x + cb12_v21.y;

// return frustum depth
return 1.0 / max(d, 1e-4);
}

float4 EditedShaderPS( in VS_OUTPUT Input ) : SV_Target0
{
// * Input from Vertex Shader
float2 InputUV = Input.TexcoordAndWorldspaceHeight.xy;
float WorldHeight = Input.TexcoordAndWorldspaceHeight.z;
float LODParam = Input.LODParams.w;

// * Inputs
float elapsedTime = cb0_v0.x;
float2 uvAnimation = cb4_v5.xy;
float2 uvScale = cb4_v4.xy;
float minValue = cb4_v2.x; // 0.0
float maxValue = cb4_v3.x; // 1.0
float3 shaftsColor = cb4_v0.rgb; // RGB( 147, 162, 173 )

float3 finalColorFilter = cb2_v2.rgb; // float3( 1.175, 1.296, 1.342 );
float finalEffectIntensity = cb2_v2.w;

float2 invViewportSize = cb0_v1.zw;

float depthScale = cb4_v6.x; // 0.001

// sample noise
float2 uvOffsets = elapsedTime * uvAnimation;
float2 uv = InputUV * uvScale + uvOffsets;
float disturb = texture0.Sample( sampler0, uv ).x;

// * Intensity mask
float intensity = saturate( lerp(minValue, maxValue, disturb) );
intensity *= InputUV.y; // transition from (0, 1)
intensity *= LODParam; // usually 1.0
intensity *= cb4_v1.x; // 1.0

// Sample depth
float2 ScreenUV = Input.PositionH.xy * invViewportSize;
float hardwareDepth = texture15.SampleLevel( sampler15, ScreenUV, 0 ).x;
float frustumDepth = getFrustumDepth( hardwareDepth );


// * Calculate mask covering distant objects behind cylinder.

// Seems that the input really is world-space height (.z component, see vertex shader)
float depth = frustumDepth - WorldHeight;
float distantObjectsMask = saturate( depth * depthScale );

// * calculate final mask
float finalEffectMask = saturate( intensity * distantObjectsMask );

// cb0_v7.y and cb4_v7.x are set to 1.0 so I didn't bother with naming them :)
float paramX = finalEffectMask;
float paramY = cb0_v7.y * finalEffectMask;
float effectAmount = lerp(paramX, paramY, cb4_v7.x);

// color of shafts comes from contant buffer
float3 effectColor = effectAmount * shaftsColor;

// gamma correction
effectColor = pow(effectColor, 2.2);

// final multiplications
effectColor *= finalColorFilter;
effectColor *= finalEffectIntensity;

// return with zero alpha 'cause the blending used here is:
// SourceColor * 1.0 + (1.0 - SrcAlpha) * DestColor
return float4( effectColor, 0.0 );
}




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


All Articles