Récemment, j'ai commencé à m'occuper du rendu de The Witcher 3. Ce jeu a des techniques de rendu incroyables. De plus, elle est magnifique en termes d'intrigue / musique / gameplay.
Dans cet article, je parlerai des solutions utilisées pour rendre The Witcher 3. Elle ne sera pas aussi complète que l'analyse des graphiques
GTA V par Adrian Correger, du moins pour l'instant.
Nous allons commencer par l'ingénierie inverse de la correction de tonalité.
Partie 1: correction de tonalité
Dans la plupart des jeux AAA modernes, l'une des étapes de rendu est nécessairement la correction de tonalité.
Permettez-moi de vous rappeler que dans la vie réelle, il existe une plage de luminosité assez large, tandis que sur les écrans d'ordinateur, elle est très limitée (8 bits par pixel, ce qui nous donne 0-255). C'est là que le tonemapping vient à la rescousse, vous permettant d'adapter une gamme plus large dans un intervalle d'éclairage limité. En règle générale, il existe deux sources de données dans ce processus: une image HDR à virgule flottante, dont les valeurs de couleur dépassent 1,0, et l'illumination moyenne de la scène (cette dernière peut être calculée de plusieurs manières, même en tenant compte de l'adaptation de l'œil pour simuler le comportement des yeux humains, mais cela n'a pas d'importance ici).
L'étape suivante (et dernière) consiste à obtenir la vitesse d'obturation, à calculer la couleur avec la vitesse d'obturation et à la traiter à l'aide de la courbe de correction des tons. Et ici, tout devient assez déroutant, car de nouveaux concepts apparaissent, comme le «point blanc» (point blanc) et le «gris moyen» (gris moyen). Il existe au moins quelques courbes populaires, et certaines d'entre elles sont décrites dans
A Closer Look at Tone Mapping de Matt Pettineo.
Honnêtement, j'ai toujours eu des problèmes avec la mise en œuvre correcte de la correction de tonalité dans mon propre code. Il y a au moins
quelques exemples différents en ligne qui m'ont été utiles ... dans une certaine mesure. Certains d'entre eux prennent en compte la luminosité HDR / le point blanc / le gris moyen, d'autres non - donc ils n'aident pas vraiment. Je voulais trouver une implémentation «testée au combat».
Nous travaillerons dans RenderDoc avec la capture de ce cadre d'une des quêtes principales de Novigrad. Tous les paramètres sont au maximum:
Après avoir cherché un peu, j'ai trouvé un appel nul pour la correction de tonalité! Comme je l'ai mentionné ci-dessus, il existe un tampon de couleurs HDR (texture numéro 0, pleine résolution) et la luminosité moyenne de la scène (texture numéro 1, 1x1, virgule flottante, calculée plus tôt par le shader de calcul).
Jetons un coup d'œil au code assembleur du pixel shader:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret
Il y a plusieurs points à noter. Premièrement, la luminosité chargée ne doit pas être égale à celle utilisée, car elle est limitée (appels max / min) dans les valeurs choisies par les artistes (à partir du tampon constant). Ceci est pratique car il vous permet d'éviter une vitesse d'obturation trop élevée ou trop basse de la scène. Cette décision semble assez courante, mais je ne l'ai jamais fait auparavant. Deuxièmement, une personne familière avec les courbes de correction de tonalité reconnaîtra instantanément cette valeur «11,2», car il s’agit en fait de la valeur du point blanc de la courbe de
correction de ton Uncharted2 de John Hable.
Les paramètres AF sont chargés à partir de cbuffer.
Nous avons donc trois autres paramètres: cb3_v16.x, cb3_v16.y, cb3_v16.z. Nous pouvons examiner leurs significations:
Mon intuition:
Je crois que «x» est une sorte de «échelle blanche» ou gris moyen, car il est multiplié par 11,2 (ligne 4), et ensuite il est utilisé comme numérateur dans le calcul du réglage de la vitesse d'obturation (ligne 10).
«Y» - je l'ai appelé le «facteur numérateur u2», et bientôt vous comprendrez pourquoi.
«Z» est le «paramètre d'exponentiation», car il est utilisé dans le triple log / mul / exp (en fait, dans l'exponentiation).
Mais traitez ces noms de variables avec un certain scepticisme!
Aussi:
cb3_v4.yz - valeurs min / max de luminosité admissible,
cb3_v7.xyz - Paramètres AC de la courbe Uncharted2,
cb3_v8.xyz - Paramètres DF de la courbe Uncharted2.
Passons maintenant à la partie difficile - nous allons écrire un shader HLSL qui nous donnera exactement le même code assembleur.
Cela peut être très difficile, et plus le shader est long, plus la tâche est difficile. Heureusement, il y a quelque temps, j'ai écrit un outil pour parcourir rapidement hlsl-> asm.
Mesdames et messieurs ... bienvenue D3DShaderDisassembler!
Après avoir expérimenté le code, j'ai obtenu la
correction tonale HLSL prête à l'emploi
The Witcher 3 :
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); }
Une capture d'écran de mon utilitaire pour le confirmer:
Voila!
Je pense que c'est une implémentation assez précise de la correction de tonalité TW3, au moins en termes de code assembleur. Je l'ai déjà appliqué dans mon framework et ça marche très bien!
J'ai dit «assez» parce que je
n'ai aucune idée pourquoi le dénominateur dans ToneMapU2Func devient maximum à zéro. Lorsque vous divisez par 0, vous devriez être indéfini?
Cela pourrait être fini, mais presque par accident, j'ai trouvé dans ce cadre une autre version du shader TW3, utilisé pour un magnifique coucher de soleil (il est intéressant de noter qu'il est utilisé avec des paramètres graphiques minimaux!)
Voyons ça. Tout d'abord, le code assembleur du shader:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret
Au début, le code peut sembler intimidant, mais en fait, tout n'est pas si mauvais. Après une brève analyse, vous remarquerez qu'il y a deux appels à la fonction Uncharted2 avec différents ensembles de données d'entrée (AF, luminosité min / max ...). Je n'ai jamais vu une telle décision auparavant.
Et HLSL:
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); }
Autrement dit, nous avons deux ensembles de paramètres de contrôle, nous calculons deux couleurs avec correction de tonalité et à la fin nous les interpolons. Décision intelligente!
Partie 2: adaptation oculaire
La deuxième partie sera beaucoup plus simple.
Dans la première partie, j'ai montré comment la correction tonale est effectuée dans TW3. Expliquant le contexte théorique, j'ai brièvement mentionné l'adaptation de l'œil. Et tu sais quoi? Dans cette partie, je parlerai de la façon dont cette adaptation de l'œil est réalisée.
Mais attendez, qu'est-ce que l'adaptation oculaire et pourquoi en avons-nous besoin?
Wikipédia sait tout à ce sujet, mais je vais vous expliquer: imaginez que vous êtes dans une pièce sombre (rappelez-vous que Life is Strange) ou dans une grotte, et allez dehors où il fait jour. Par exemple, la principale source d'éclairage peut être le soleil.
Dans l'obscurité, nos pupilles sont dilatées afin que plus de lumière pénètre dans la rétine à travers elles. Quand il devient léger, nos pupilles diminuent et parfois nous fermons les yeux car cela «fait mal».
Ce changement ne se produit pas instantanément. L'œil doit s'adapter aux changements de luminosité. C'est pourquoi nous effectuons l'adaptation des yeux dans le rendu en temps réel.
Le
HDRToneMappingCS11 du DirectX SDK est un bon exemple d'un manque d'adaptation oculaire. Les changements brusques de luminosité moyenne sont plutôt désagréables et contre nature.
Commençons! Par souci de cohérence, nous analyserons le même cadre de Novigrad.
Nous allons maintenant approfondir le programme de capture d'images RenderDoc. L'adaptation de l'œil est généralement effectuée juste avant la correction tonale, et The Witcher 3 ne fait pas exception.
Regardons l'état du pixel shader:
Nous avons deux sources d'entrée - 2 textures, R32_FLOAT, 1x1 (un pixel). texture0 contient la luminosité moyenne de la scène de l'image précédente. texture1 contient la luminosité moyenne de la scène de l'image actuelle (calculée immédiatement avant ce shader de calcul - je l'ai marquée en bleu).
Il est prévu qu'il y ait une sortie - R32_FLOAT, 1x1. Regardons le pixel shader.
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret
Wow, comme c'est simple! Seulement 7 lignes de code assembleur. Que se passe-t-il ici? Je vais expliquer chaque ligne:
0) Obtenez la luminosité moyenne de l'image actuelle.
1) Obtenez la luminosité moyenne de l'image précédente.
2) Effectuez une vérification: la luminosité actuelle est-elle inférieure ou égale à la luminosité de l'image précédente?
Si oui, alors la luminosité diminue, sinon, la luminosité augmente.
3) Calculez la différence:
difference = currentLum - previousLum.4) Ce transfert conditionnel (movc) attribue un facteur de vitesse à partir du tampon constant. Deux valeurs différentes peuvent être attribuées à partir de la ligne 2, selon le résultat du contrôle. C'est une décision intelligente, car de cette manière, vous pouvez obtenir différentes vitesses d'adaptation pour réduire et augmenter la luminosité. Mais dans le cadre étudié, les deux valeurs sont les mêmes et varient de 0,11 à 0,3.
5) Le calcul final de la luminosité adaptée:
adaptéLuminance = speedFactor * différence + précédenteLuminance.6) La fin du shader
Ceci est implémenté dans HLSL tout simplement:
// The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; }
Ces lignes nous donnent le même code assembleur. Je suggérerais seulement de remplacer le type de sortie par
float4 par
float . Pas besoin de gaspiller de bande passante. C'est ainsi que Witcher 3 met en œuvre l'adaptation oculaire. Assez simple, non?
PS. Un grand merci à Baldur Karlsson (Twitter:
@baldurk ) pour RenderDoc. Le programme est tout simplement génial.
Partie 3: aberration chromatique
L'aberration chromatique est un effet que l'on retrouve principalement dans les verres bon marché. Cela se produit parce que les lentilles ont des indices de réfraction différents pour différentes longueurs de lumière visible. En conséquence, une distorsion visible apparaît. Cependant, tout le monde n'aime pas ça. Heureusement, dans Witcher 3, cet effet est très subtil, et donc pas gênant dans le gameplay (moi du moins). Mais vous pouvez le désactiver si vous le souhaitez.
Examinons de près un exemple de scène avec aberration chromatique et sans elle:
Aberration chromatique incluseAberration chromatique désactivéeAvez-vous remarqué des différences près des bords? Moi non plus. Essayons une autre scène:
L'aberration chromatique est incluse. Remarquez la légère distorsion «rouge» dans la zone indiquée.Ouais, bien mieux! Ici, le contraste entre les zones sombres et claires est plus fort, et dans le coin, nous voyons une légère distorsion. Comme vous pouvez le voir, cet effet est très faible. Cependant, je me demandais comment il est mis en œuvre. Passons à la partie la plus curieuse: le code!
ImplémentationLa première chose à faire est de trouver le bon appel de dessin avec un pixel shader. En fait, l'aberration chromatique fait partie du grand shader de pixel «post-traitement», qui consiste en l'aberration chromatique, le vignettage et la correction gamma. Tout cela est à l'intérieur d'un shader à pixel unique. Examinons de plus près le code assembleur du pixel shader:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ...
Et aux valeurs cbuffer:
Essayons donc de comprendre ce qui se passe ici. En fait, cb3_v17.xy est le centre de l'aberration chromatique, donc les premières lignes calculent le vecteur 2d à partir des coordonnées texel (cb3_v17.zw = l'inverse de la taille de la fenêtre d'affichage) au «centre de l'aberration chromatique» et de sa longueur, puis effectue d'autres calculs, vérification et branchement . Lors de l'application de l'aberration chromatique, nous calculons les déplacements en utilisant certaines valeurs du tampon constant et déformons les canaux R et G. En général, plus les bords de l'écran sont proches, plus l'effet est fort. La ligne 10 est assez intéressante car elle rapproche les pixels, surtout quand on exagère l'aberration. Je partagerai avec plaisir ma réalisation de l'effet. Comme d'habitude, prenez des noms de variables avec une part (solide) de scepticisme. Et notez que l'effet est appliqué
avant la correction gamma.
void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } }
J'ai ajouté «fChromaticAberrationIntensity» pour augmenter la taille du décalage, et donc la force de l'effet, comme son nom l'indique (TW3 = 1.0). Intensité = 40:
C'est tout! J'espère que vous avez apprécié cette partie.
Partie 4: vignettage
Le vignettage est l'un des effets de post-traitement les plus couramment utilisés dans les jeux. Il est également populaire en photographie. Les coins légèrement ombragés peuvent créer un bel effet. Il existe plusieurs types de vignettage. Par exemple,
Unreal Engine 4 utilise du naturel. Mais revenons à The Witcher 3.
Cliquez ici pour voir une comparaison interactive des images avec et sans vignettage. La comparaison est tirée du
guide de performances NVIDIA pour The Witcher 3 .
Capture d'écran de "The Witcher 3" avec vignettage activé.Notez que le coin supérieur gauche (ciel) n'est pas aussi ombragé que les autres parties de l'image. Plus tard, nous y reviendrons.
Détails d'implémentationPremièrement, il existe une légère différence entre le vignettage utilisé dans la version originale de The Witcher 3 (qui a été publiée le 19 mai 2015) et dans The Witcher 3: Blood and Wine. Dans le premier, le «gradient inverse» est calculé à l'intérieur du pixel shader, et dans le second, il est pré-calculé en une texture 2D 256x256:
Texture 256x256, utilisée comme "gradient inverse" dans le complément "Sang et vin".
J'utiliserai le shader Blood and Wine (grand jeu, au fait). Comme dans la plupart des autres jeux, le vignettage Witcher 3 est calculé dans le pixel shader du post-traitement final. Jetez un œil au code assembleur:
... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ...
Intéressant! Il semble que le gamma (ligne 46) et les espaces linéaires (ligne 51) soient utilisés pour calculer le vignettage. À la ligne 48, nous échantillonnons la texture du «gradient inverse». cb3 [9] .xyz n'est pas lié au vignettage. Dans chaque image cochée, on lui attribue la valeur float3 (1.0, 1.0, 1.0), c'est-à-dire qu'il s'agit probablement du dernier filtre utilisé dans les effets de fondu d'entrée / de sortie. Il existe trois paramètres principaux pour le vignettage dans TW3:
- Opacité (cb3 [6] .w) - affecte la force du vignettage. 0 - pas de vignettage, 1 - vignettage maximum. Selon mes observations, dans la base The Witcher 3, il est d'environ 1,0, tandis que dans Blood and Wine, il oscille autour de 0,15.
- Couleur (cb3 [7] .xyz) - une excellente caractéristique du vignettage TW3 est la possibilité de changer sa couleur. Il n'a pas besoin d'être noir, mais en pratique ... Habituellement, il a les valeurs float3 (3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0) et ainsi de suite - dans le cas général, ce sont des multiples de 0.00392156 = 1.0 / 255.0
- Les poids (cb3 [6] .xyz) est un paramètre très intéressant. J'ai toujours vu des vignettes «plates», par exemple:
Masque de vignettage typiqueMais en utilisant des poids (ligne 52), vous pouvez obtenir des résultats très intéressants:
Masque de vignettage TW3 calculé à l'aide de poidsLes poids sont proches de 1,0. Regardez les constantes données tampons pour une image de Blood and Wine (un monde magique avec un arc-en-ciel): c'est pourquoi le vignettage n'a pas affecté les pixels lumineux du ciel au-dessus.
CodeVoici mon implémentation du vignettage TW3 sur HLSL.
GammaToLinear = pow (couleur, 2.2)
float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; }
J'espère que ça vous a plu. Vous pouvez également essayer mon HLSLexplorer , qui m'a beaucoup aidé à comprendre le code assembleur HLSL.Comme précédemment, prenez les noms des variables avec un certain scepticisme - les shaders TW3 sont traités par D3DStripShader, donc, en fait, je ne sais presque rien à leur sujet, je ne peux que deviner. De plus, je ne porte aucune responsabilité pour les dommages causés à votre équipement par ce shader;)Bonus: calcul du gradientDans The Witcher 3, sorti en 2015, le gradient inverse a été calculé dans le pixel shader, plutôt que d'échantillonner une texture pré-calculée. Jetez un œil au code assembleur: 35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000)
Heureusement pour nous, c'est assez simple. Sur HLSL, cela ressemblera à ceci: float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; }
Autrement dit, nous calculons simplement la distance du centre au textile, faisons de la magie avec lui (multiplication, saturer ...), puis ... calculons le polynôme! Génial.Partie 5: l'effet de l'intoxication
Voyons comment le jeu "The Witcher 3: Wild Hunt" implémente l'effet de l'intoxication. Si vous ne l'avez pas encore joué, alors lâchez tout, achetez et jouez, regardez une vidéo:Soirée:Nuit:Tout d'abord, nous voyons une image double et tourbillonnante, qui apparaît souvent lorsque vous buvez dans la vraie vie. Plus un pixel est éloigné du centre de l'image, plus l'effet de rotation est fort. J'ai intentionnellement posté la deuxième vidéo avec la nuit, car vous pouvez clairement voir cette rotation sur les étoiles (voir 8 points distincts?)La deuxième partie de l'effet d'intoxication, qui peut ne pas être immédiatement perceptible, est un léger changement de zoom. Il est visible près du centre.Il est probablement évident que cet effet est un post-traitement typique (pixel shader). Cependant, son emplacement dans le pipeline de rendu peut ne pas être aussi évident. Il s'avère que l'effet d'intoxication est appliqué immédiatement après la correction tonale et juste avant le flou de mouvement (l'image «ivre» est l'entrée pour le flou de mouvement).Commençons les jeux avec le code assembleur: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
Deux tampons constants distincts sont utilisés ici. Vérifions leurs valeurs:Nous nous intéressons à certains d'entre eux:cb0_v0.x -> temps écoulé (en secondes)cb0_v1.xyzw - taille de la fenêtre d'affichage et l'inverse de la taille de la fenêtre d'affichage (aka "taille du pixel") cb3_v0.x - rotation autour d'un pixel, a toujours une valeur de 1,0.cb3_v0.y - l'ampleur de l'effet de l'intoxication. Une fois allumé, il ne fonctionne pas à pleine puissance, mais augmente progressivement de 0,0 à 1,0.cv3_v1.xy - décalages de pixels (plus de détails ci-dessous). Il s'agit d'une paire sin / cos, vous pouvez donc utiliser des sincos (temps) dans le shader si vous le souhaitez.cb3_v2.xy est le centre de l'effet, généralement float2 (0,5, 0,5).Ici, nous voulons nous concentrer sur la compréhension de ce qui se passe, et pas seulement réécrire aveuglément le shader.Commençons par les premières lignes: ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w
J'appelle la ligne 0 «taux de zoom» et vous comprendrez bientôt pourquoi. Immédiatement après (ligne 1), nous calculons le «déplacement en rotation». Il s'agit simplement d'une paire de données sin / cos d'entrée multipliée par 0,05.Lignes 2-4: Tout d'abord, nous calculons le vecteur du centre de l'effet aux coordonnées UV de la texture. Ensuite, nous calculons le carré de la distance (3) et la distance simple (4) (du centre au texel)Zoom sur les coordonnées de texture
Regardons le code assembleur suivant: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy
Puisqu'ils sont emballés de cette façon, nous ne pouvons analyser qu'une seule paire de flotteurs.Pour commencer, r0.yz sont des «décalages de rotation», r1.z est la distance du centre au texel, r1.xy est le vecteur du centre au texel, r0.x est le «facteur de zoom».Pour comprendre cela, supposons pour l'instant que zoomFactor = 1.0, c'est-à-dire que vous pouvez écrire ce qui suit: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center;
Mais zoomFactor = 1.0: r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter;
De même pour r3.xy: 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center
Mais zoomFactor = 1.0: Super. Autrement dit, pour le moment, nous avons essentiellement l'offset TextureUV (texel) ± rotation actuel, mais qu'en est-il de zoomFactor? Regardez la ligne 0. En fait, zoomFactor = 1.0 - 0.1 * drunkAmount. Pour le maximum drunkAmount, la valeur de zoomFactor doit être de 0,9 et les coordonnées de texture avec zoom sont désormais calculées comme suit:r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter
baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter
Une telle explication serait peut-être plus intuitive: il s'agit simplement d'une interpolation linéaire par un facteur entre les coordonnées de texture normalisées et le centre. Il s'agit d'une image de «zoom avant». Pour comprendre cela, il est préférable d'expérimenter avec les valeurs. Voici un lien vers Shadertoy, où vous pouvez voir l'effet en action.Offset de texture
Le fragment entier dans le code assembleur: 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset
crée un certain gradient, appelons-le le "masque d'intensité de déplacement". En fait, cela donne deux sens. Le premier est en r0.w (nous l'utiliserons plus tard) et le second est 5 fois plus fort en r0.x (ligne 15). Ce dernier sert en fait de facteur pour la taille du texel, il affecte donc la force de polarisation.Échantillonnage lié à la rotationEnsuite, une série d'échantillonnage de texture est effectuée. En fait, 2 séries de 8 échantillons sont utilisées, une pour chaque «côté». Dans HLSL, vous pouvez écrire ceci comme suit: static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB;
L'astuce consiste à ajouter à baseTexcoordsA / B un décalage supplémentaire se trouvant sur un cercle unitaire, multiplié par l '«intensité de décalage des coordonnées de texture» mentionnée précédemment. Plus le pixel est éloigné du centre, plus le rayon du cercle autour du pixel est grand - nous l'échantillons 8 fois, ce qui est clairement visible sur les étoiles. Valeurs PointsAroundPixel (multiples de 45 degrés):Cercle uniqueÉchantillonnage lié au zoomLa deuxième partie de l'effet d'ivresse dans The Witcher 3 est le zoom avec zoom avant et zoom arrière. Regardons le code assembleur qui effectue cette tâche: 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw
Nous voyons qu'il y a trois appels de texture distincts, c'est-à-dire trois coordonnées de texture différentes. Analysons comment les coordonnées de texture sont calculées à partir de celles-ci. Mais d'abord, nous montrons l'entrée pour cette partie: float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60
Quelques mots sur l'entrée. Nous calculons le décalage en texels (par exemple, 8,0 * taille de texel), qui est ensuite ajouté aux coordonnées uv de base. L'amplitude se situe simplement entre 0,98 et 1,02 pour donner une impression de zoom, tout comme zoomFactor dans la pièce effectuant la rotation.Commençons par la première paire - r1.xy (ligne 61) r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude);
Soit:
float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude)
Vérifions la deuxième paire - r3.xy (ligne 62) r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV
Soit:
float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates;
Vérifions la troisième paire - r0.xy (lignes 63-64) r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV
Soit:
float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates
Les trois requêtes de texture sont additionnées et le résultat est stocké dans le registre r1. Il convient de noter que ce pixel shader utilise un échantillonneur d'adressage limité.Assembler le toutDonc, pour le moment, nous avons le résultat de la rotation dans le registre r2 et de trois demandes de zoom pliées dans le registre r1. Regardons les dernières lignes du code assembleur: 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
À propos de l'entrée supplémentaire: r0.w est tiré de la ligne 7, c'est notre masque d'intensité, et cb3 [0] .y est la magnitude de l'effet d'intoxication.Voyons comment cela fonctionne. Ma première approche a été la force brute: float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor;
Mais bon sang, personne n'écrit des shaders comme ça . J'ai pris un crayon avec du papier et j'ai écrit cette formule: finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart
Où t = effectAmount * intensitéMaskAinsi, nous obtenons: finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t )
Et nous arrivons à ce qui suit: finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity);
Oui, cette partie de l'article s'est avérée très détaillée, mais nous avons finalement terminé! Personnellement, j'ai appris quelque chose en train d'écrire, j'espère que vous aussi!Si vous êtes intéressé, les sources HLSL complètes sont disponibles ici . Je les ai testés avec mon HLSLexplorer , et bien qu'il n'y ait pas de correspondance directe avec le shader d'origine, les différences sont si petites (une ligne de moins) que je peux dire avec confiance que cela fonctionne. Merci d'avoir lu!