Partie 1. Fermetures à glissière
Dans cette partie, nous verrons le processus de rendu de la foudre dans Witcher 3: Wild Hunt.
Le rendu de l'éclair est effectué un peu plus tard que l'effet de
rideau de pluie , mais se produit toujours dans la passe de rendu direct. La foudre peut être vue dans cette vidéo:
Ils disparaissent très rapidement, il est donc préférable de regarder la vidéo à une vitesse de 0,25.
Vous pouvez voir que ce ne sont pas des images statiques; au fil du temps, leur luminosité change légèrement.
Du point de vue du rendu des nuances, il existe de nombreuses similitudes avec le dessin d'un rideau de pluie au loin, par exemple, le même état de mélange (mélange additif) et de profondeur (la vérification est activée, l'enregistrement de la profondeur n'est pas effectué).
Scène sans foudreScène de foudreEn termes de géométrie de foudre, The Witcher 3 est un maillage en forme d'arbre. Cet exemple de foudre est représenté par le maillage suivant:
Il a des coordonnées UV et des vecteurs normaux. Tout cela est utile au stade du vertex shader.
Vertex shader
Jetons un coup d'œil au code de vertex shader assemblé:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret
Il existe de nombreuses similitudes avec le rideau de pluie vertex shader, donc je ne répéterai pas. Je veux vous montrer la différence importante qui se trouve dans les lignes 11-18:
11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw
Premièrement, cb1 [8] .xyz est la position de la caméra, et r2.xyz est la position dans l'espace mondial, c'est-à-dire que la ligne 11 calcule le vecteur de la caméra à la position dans le monde. Ensuite, les lignes 12 à 15 calculent la
longueur (worldPos - cameraPos) * 0,000001.v2.xyz est le vecteur normal de la géométrie entrante. La ligne 16 le prolonge de l'intervalle [0-1] à l'intervalle [-1; 1].
Ensuite, la position finale dans le monde est calculée:
finalWorldPos = worldPos + longueur (worldPos - cameraPos) * 0.000001 * normalVectorL'extrait de code HLSL pour cette opération ressemblera à ceci:
...
Cette opération se traduit par une petite "rafale" du maillage (en direction du vecteur normal). J'ai expérimenté en remplaçant 0,000001 par plusieurs autres valeurs. Voici les résultats:
0,0000020,0000050,000010,000025Pixel shader
Eh bien, nous avons compris le vertex shader, maintenant il est temps de passer au code assembleur pour le pixel shader!
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret
Bonne nouvelle: le code n'est pas si long.
Mauvaise nouvelle:
3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff)
... de quoi s'agit-il?
Honnêtement, ce n'est pas la première fois que je vois un tel morceau ... de code assembleur dans les shaders de Witcher 3. Mais quand je l'ai rencontré pour la première fois, j'ai pensé: "Qu'est-ce que c'est que ça?"
Quelque chose de similaire peut être trouvé dans certains autres shaders TW3. Je ne décrirai pas mes aventures avec ce fragment, et je dirai simplement que la réponse réside dans le
bruit entier :
Comme vous pouvez le voir, dans le pixel shader, il est appelé deux fois. En utilisant les guides de ce site Web, nous pouvons comprendre comment le bruit doux est correctement mis en œuvre. J'y reviendrai dans une minute.
Regardez la ligne 0 - ici, nous animons sur la base de la formule suivante:
animation = elapsedTime * animationSpeed + TextureUV.xCes valeurs, après avoir été arrondies vers le bas (
étage ) (instruction
round_ni ),
deviennent à l'avenir des points d'entrée pour le bruit entier. Habituellement, nous calculons la valeur du bruit pour deux entiers, puis nous calculons la valeur finale interpolée entre eux (voir le site Web de libnoise pour plus de détails).
Eh bien, c'est
un bruit
entier , mais après tout, toutes les valeurs mentionnées précédemment (également arrondies vers le bas) sont flottantes!
Notez qu'il n'y a pas d'instructions
ftoi ici . Je suppose que les programmeurs de CD Projekt Red ont utilisé
ici la fonction interne HLSL
asint , qui effectue la conversion des valeurs à virgule flottante «reinterpret_cast» et les traite comme un modèle entier.
Le poids d'interpolation pour les deux valeurs est calculé sur les lignes 10-11.
interpolationWeight = 1.0 - frac (animation);Cette approche nous permet d'interpoler entre les valeurs dans le temps.
Pour créer un bruit doux, cet interpolateur est passé à la fonction
SCurve :
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
Fonction Smoothstep [libnoise.sourceforge.net]Cette fonctionnalité est connue sous le nom de "smoothstep". Mais comme vous pouvez le voir dans le code assembleur, ce n'est
pas une fonction de
lissage interne de HLSL. Une fonction interne applique des restrictions afin que les valeurs soient vraies. Mais comme nous savons que
interpolationWeight sera toujours dans la plage [0-1], ces vérifications peuvent être ignorées en toute sécurité.
Lors du calcul de la valeur finale, plusieurs opérations de multiplication sont utilisées. Découvrez comment la sortie alpha finale peut changer en fonction de la valeur du bruit. C'est pratique car cela affectera l'opacité de la foudre rendue, comme dans la vraie vie.
Pixel shader prêt:
cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; };
Pour résumer
Dans cette partie, j'ai décrit un moyen de rendre la foudre dans The Witcher 3.
Je suis très heureux que le code assembleur qui est sorti de mon shader corresponde complètement à l'original!
Partie 2. Astuces idiotes de ciel
Cette partie sera légèrement différente des précédentes. Dans ce document, je veux vous montrer certains aspects du Sky Shader Witcher 3.
Pourquoi des "trucs idiots" et pas tout le shader? Eh bien, il y a plusieurs raisons. Tout d'abord, le sky shader de Witcher 3 est une bête plutôt complexe. Le pixel shader de la version 2015 contient 267 lignes de code assembleur, et le shader du Blood and Wine DLC contient 385 lignes.
De plus, ils reçoivent beaucoup d'entrées, ce qui n'est pas très propice à la rétro-ingénierie du code HLSL complet (et lisible!).
Par conséquent, j'ai décidé de ne montrer qu'une partie des astuces de ces shaders. Si je trouve quelque chose de nouveau, je compléterai le post.
Les différences entre la version 2015 et le DLC (2016) sont très visibles. En particulier, ils incluent des différences dans le calcul des étoiles et leur scintillement, une approche différente du rendu du soleil ...
Le shader
Blood and Wine calcule même la Voie lactée la nuit.
Je vais commencer par les bases puis parler de trucs stupides.
Les bases
Comme la plupart des jeux modernes, Witcher 3 utilise skydome pour modéliser le ciel. Regardez l'hémisphère utilisé pour cela dans Witcher 3 (2015). Remarque: dans ce cas, la boîte englobante de ce maillage est dans la plage de [0,0,0] à [1,1,1] (Z est l'axe pointant vers le haut) et a des UV distribués en douceur. Plus tard, nous les utilisons.
L'idée derrière skydome est similaire à l'idée de
skybox (la seule différence est le maillage utilisé). Au stade du vertex shader, nous transformons le skydome par rapport à l'observateur (généralement en fonction de la position de la caméra), ce qui crée l'illusion que le ciel est en fait très loin - nous n'y arriverons jamais.
Si vous lisez les parties précédentes de cette série d'articles, vous savez que «The Witcher 3» utilise la profondeur inverse, c'est-à-dire que le plan éloigné est 0,0f, et le plus proche est 1,0f. Afin de terminer la sortie du skydome sur le plan lointain, dans les paramètres de la fenêtre de navigation, nous avons défini
MinDepth sur la même valeur que
MaxDepth :
Pour savoir comment les
champs MinDepth et
MaxDepth sont utilisés lors de la conversion de la fenêtre de navigation, cliquez
ici (docs.microsoft.com).
Vertex shader
Commençons par le vertex shader. Dans Witcher 3 (2015), le code de shader d'assembleur est le suivant:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret
Dans ce cas, le vertex shader transfère uniquement les texcoords et une position dans l'espace mondial vers la sortie. Dans
Blood and Wine, il affiche également un vecteur normal normalisé. Je considérerai la version 2015 car elle est plus simple.
Regardez le tampon constant désigné comme
cb2 :
Nous avons ici une matrice du monde (mise à l'échelle uniforme par 100 et transfert par rapport à la position de la caméra). Rien de compliqué. cb2_v4 et cb2_v5 sont les facteurs d'échelle / écart utilisés pour convertir les positions des sommets de l'intervalle [0-1] à l'intervalle [-1; 1]. Mais ici, ces coefficients «compressent» l'axe Z (vers le haut).
Dans les parties précédentes de la série, nous avions des vertex shaders similaires. L'algorithme général consiste à transférer davantage les texcoords, puis la
position est calculée en tenant compte des coefficients d'échelle / déviation, puis la
position W
est calculée dans l'espace mondial, puis la position finale de l'espace de détourage est calculée en multipliant
matWorld et
matViewProj -> leur produit est utilisé pour se multiplier par
position pour obtenir la position finale de SV_Position .
Par conséquent, le HLSL de ce vertex shader devrait ressembler à ceci:
struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0;
Comparaison de mon shader (à gauche) et de l'original (à droite):
Une excellente propriété de
RenderDoc est qu'il nous permet d'injecter notre propre shader au lieu de l'original, et ces changements affecteront le pipeline jusqu'à la fin du cadre. Comme vous pouvez le voir dans le code HLSL, j'ai fourni plusieurs options pour zoomer et transformer la géométrie finale. Vous pouvez les expérimenter et obtenir des résultats très amusants:
Vertex Shader Optimization
Avez-vous remarqué le problème du vertex shader d'origine? La multiplication des sommets d'une matrice par une matrice est complètement redondante! J'ai trouvé cela dans au moins quelques vertex shaders (par exemple, dans le shader un
rideau de pluie au loin ). Nous pouvons l'optimiser en multipliant immédiatement
PositionW par
matViewProj !
Ainsi, nous pouvons remplacer ce code par HLSL:
comme suit:
La version optimisée nous donne le code d'assemblage suivant:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret
Comme vous pouvez le voir, nous avons réduit le nombre d'instructions de 26 à 12 - un changement assez important. Je ne sais pas à quel point ce problème est répandu dans le jeu, mais pour l'amour de Dieu, CD Projekt Red, peut-être publier un patch? :)
Et je ne plaisante pas. Vous pouvez insérer mon shader optimisé au lieu du RenderDoc d'origine et vous verrez que cette optimisation n'affecte visuellement rien. Honnêtement, je ne comprends pas pourquoi CD Projekt Red a décidé d'effectuer la multiplication des sommets d'une matrice par une matrice ...
Le soleil
Dans The Witcher 3 (2015), le calcul de la diffusion atmosphérique et du Soleil consiste en deux appels de dessin distincts:
Witcher 3 (2015) - Jusqu'àWitcher 3 (2015) - avec le cielWitcher 3 (2015) - avec ciel + soleilLe rendu du Soleil dans la version 2015 est très similaire au
rendu de la Lune en termes de géométrie et d'états de mélange / profondeur.
En revanche, dans
"Blood and Wine" le ciel avec le Soleil est rendu en un seul passage:
The Witcher 3: Blood and Wine (2016) - Au paradisThe Witcher 3: Blood and Wine (2016) - avec le ciel et le soleilPeu importe la façon dont vous effectuez le rendu du soleil, à un certain stade, vous avez toujours besoin de la direction (normalisée) de la lumière du soleil. La façon la plus logique d'obtenir ce vecteur est d'utiliser
des coordonnées sphériques . En fait, nous n'avons besoin que de deux valeurs qui indiquent deux angles (en radians!):
Phi et
thêta . Après les avoir reçus, nous pouvons supposer que
r = 1 , le réduisant ainsi. Ensuite, pour les coordonnées cartésiennes avec l'axe Y pointant vers le haut, vous pouvez écrire le code suivant en HLSL:
float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir);
En règle générale, la direction de la lumière solaire est calculée dans l'application, puis transmise au tampon constant pour une utilisation future.
Ayant reçu la direction de la lumière du soleil, nous pouvons approfondir le code assembleur du pixel shader
"Blood and Wine" ...
... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ...
Donc, premièrement,
cb12 [0] .xyz est la position de la caméra, et dans
r0.xyz nous stockons la position du sommet (c'est la sortie du vertex shader). Par conséquent, la ligne 100 calcule le vecteur
worldToCamera . Mais jetez un œil aux lignes 105-107. Nous pouvons les écrire comme
normaliser (-worldToCamera) , c'est-à-dire que nous calculons le vecteur
cameraToWorld normalisé.
120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx
Ensuite, nous calculons le produit scalaire des
vecteurs cameraToWorld et
sunDirection ! N'oubliez pas qu'ils doivent être normalisés. Nous saturons également cette expression complète pour la limiter à l'intervalle [0-1].
Super! Ce produit scalaire est stocké dans r1.x. Voyons où cela s'applique ensuite ...
152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x
La trinité «log, mul, exp» est l'exponentiation. Comme vous pouvez le voir, nous augmentons notre cosinus (le produit scalaire des vecteurs normalisés) dans une certaine mesure. Vous pouvez demander pourquoi. De cette façon, nous pouvons créer un dégradé qui imite le soleil. (Et la ligne 155 affecte l'opacité de ce dégradé, de sorte que nous, par exemple, le réinitialisons pour masquer complètement le Soleil). Voici quelques exemples:
exposant = 54exposant = 2400Ayant ce gradient, nous l'utilisons pour interpoler entre
skyColor et
sunColor ! Pour éviter les artefacts, vous devez saturer la valeur de la ligne 120.
Il convient de noter que cette astuce peut être utilisée pour simuler les
couronnes de la lune (à de faibles valeurs d'exposant). Pour ce faire, nous avons besoin du vecteur
moonDirection , qui peut facilement être calculé à l'aide de coordonnées sphériques.
Le code HLSL prêt à l'emploi peut ressembler à l'extrait de code suivant:
float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient );
Mouvement des étoiles
Si vous faites un time-lapse du ciel nocturne clair de Witcher 3, vous pouvez voir que les étoiles ne sont pas statiques - elles se déplacent un peu à travers le ciel! J'ai remarqué cela presque par accident et je voulais savoir comment cela a été mis en œuvre.
Commençons par le fait que les étoiles dans Witcher 3 sont présentées comme une carte cubique de taille 1024x1024x6. Si vous y réfléchissez, vous pouvez comprendre qu'il s'agit d'une solution très pratique qui vous permet de prendre facilement des directions pour échantillonner une carte cubique.
Regardons le code assembleur suivant:
159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0
Pour calculer le vecteur d'échantillonnage final (ligne 173), nous commençons par calculer le vecteur
worldToCamera normalisé (lignes 159-162).
Ensuite, nous calculons deux produits vectoriels (163-164, 165-166) avec
moonDirection , puis nous calculons trois produits scalaires pour obtenir le vecteur d'échantillonnage final. Code HLSL:
float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb;
Remarque à moi-même: c'est un code très bien conçu, et je devrais l'examiner plus en détail.
Note aux lecteurs: si vous en savez plus sur cette opération, alors dites-moi!
Étoiles scintillantes
Un autre truc intéressant que j'aimerais explorer plus en détail est le scintillement des étoiles.
Par exemple, si vous vous promenez dans Novigrad par temps clair, vous remarquerez que les étoiles scintillent.J'étais curieux de savoir comment cela a été mis en œuvre. Il s'est avéré que la différence entre la version 2015 et «Blood and Wine» est assez grande. Par souci de simplicité, je considérerai la version 2015.Donc, nous commençons juste après avoir échantillonné starsColor de la section précédente: 174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx
Hm.
Jetons un coup d'œil à la fin de ce code d'assemblage assez long.Après avoir échantillonné starsColor à la ligne 173, nous calculons une sorte de valeur de décalage . Ce décalage est utilisé pour déformer la première direction d'échantillonnage (r2.xyz, ligne 235), puis à nouveau nous échantillonnons la carte cubique des étoiles, effectuons une correction gamma de ces deux valeurs (237-242) et les multiplions (243).C'est simple, non? Enfin, pas vraiment. Réfléchissons un peu à ce décalage . Cette valeur devrait être différente tout au long du skydome - des étoiles tout aussi scintillantes sembleraient très irréalistes.Pour compenserétait aussi diversifiée que possible, nous allons profiter du fait que les UV sont étirés pour skydome (v0.xy) et appliquer le temps écoulé stocké dans le tampon constant (cb [0] .x).Si vous n'êtes pas familier avec ces ishr / xor / effrayants et, dans la partie sur l'effet de la foudre, lisez le bruit entier.Comme vous pouvez le voir, le bruit entier est causé quatre fois ici, mais il diffère de celui utilisé pour la foudre. Pour rendre les résultats encore plus aléatoires, l'entier d'entrée pour le bruit est la somme ( iadd ) et les bits sont inversés avec lui ( bits inverses de fonction interne ; instruction bfrev ).Alors, ralentissez maintenant. Commençons par le début.Nous avons 4 «itérations» de bruit entier. J'ai analysé le code assembleur, les calculs des 4 itérations ressemblent à ceci: int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); }
La sortie finale des 4 itérations (suivez les instructions de itof pour les trouver ):Itération 1 - r5.x,Itération 2 - r4.w,Itération 3 - r1.w,Itération 4 - r5.yAprès la dernière itof (ligne 216) ) nous avons: 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z
Ces lignes calculent les valeurs de la courbe en S pour l'équilibre sur la base de la partie fractionnelle des UV, comme dans le cas de la foudre. Donc:
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
Comme vous pouvez vous y attendre, ces coefficients sont utilisés pour interpoler en douceur le bruit et générer le décalage final pour les coordonnées d'échantillonnage: 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb;
Voici une petite visualisation du décalage calculé :Après avoir calculé starsColorDisturbed, la partie la plus difficile est terminée. Hourra!
L'étape suivante consiste à effectuer une correction gamma pour starsColor et starsColorDisturbed , après quoi elles sont multipliées: starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed;
Stars - la touche finale
Nous avons starsFinal dans r1.xyz. À la fin du traitement en étoile, les événements suivants se produisent: 256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000)
C'est beaucoup plus facile par rapport aux étoiles scintillantes et mobiles.Donc, nous commençons par élever starsFinal à une puissance de 2,5 - cela nous permet de contrôler la densité des étoiles. Assez intelligent. Ensuite, nous faisons la couleur maximale des étoiles égale float3 (1, 1, 1).cb0 [9] .w est utilisé pour contrôler la visibilité globale des étoiles. Par conséquent, nous pouvons nous attendre à ce que pendant la journée, cette valeur soit de 1,0 (ce qui donne une multiplication par zéro), et la nuit - 0,0.En fin de compte, nous augmentons la visibilité des étoiles de 10. Et c'est tout!Partie 3. The Witcher Flair (objets et carte de luminosité)
Presque tous les effets et techniques décrits précédemment n'étaient pas vraiment liés à Witcher 3. Des choses comme la correction de tonalité, le vignettage ou le calcul de la luminosité moyenne sont présentes dans presque tous les jeux modernes. Même l'effet de l'intoxication est assez répandu.C'est pourquoi j'ai décidé d'examiner de plus près la mécanique de rendu de «l'instinct de sorceleur». Geralt est un sorceleur, et donc ses sentiments sont beaucoup plus aigus que ceux d'une personne ordinaire. Par conséquent, il peut voir et entendre plus que les autres, ce qui l'aide grandement dans ses enquêtes. La mécanique de flair du sorceleur permet au joueur de visualiser ces traces.Voici une démonstration de l'effet:Et un de plus, avec un meilleur éclairage:Comme vous pouvez le voir, il existe deux types d'objets: ceux avec lesquels Geralt peut interagir (contour jaune) et les traces associées à l'enquête (contour rouge). Après que Geralt a examiné la piste rouge, elle peut devenir jaune (première vidéo). Notez que tout l'écran devient gris et un effet fish-eye (deuxième vidéo) est ajouté.Cet effet est assez compliqué, j'ai donc décidé de diviser ses recherches en trois parties.Dans le premier, je parlerai de la sélection des objets, dans le second - de la génération du contour, et dans le troisième - de l'unification finale de tout cela en un tout.Sélectionner des objets
Comme je l'ai dit, il existe deux types d'objets et nous devons les distinguer. Dans Witcher 3, cela est implémenté à l'aide d'un tampon de gabarit. Lors de la génération de maillages GBuffer qui doivent être marqués comme «traces» (rouges), ils sont rendus avec stencil = 8. Les maillages marqués en jaune comme objets «intéressants» sont rendus avec stencil = 4.Par exemple, les deux textures suivantes montrent un exemple de cadre avec instinct de sorceleur visible et le tampon de pochoir correspondant:Slip Buffer Brief
Le tampon de pochoir est souvent utilisé dans les jeux pour marquer les maillages. Certaines catégories de mailles reçoivent le même ID.L'idée est d'utiliser la fonction Toujours avec l'opérateur Remplacer si le test du gabarit réussit et avec l'opérateur Conserver dans tous les autres cas.Voici comment il est implémenté à l'aide de D3D11: D3D11_DEPTH_STENCIL_DESC depthstencilState;
La valeur de stensil à écrire dans le tampon est transmise en tant que StencilRef dans l'appel d'API:
Rendu de la luminosité
Dans ce passage, du point de vue de la mise en œuvre, il existe une texture plein écran au format R11G11B10_FLOAT, dans laquelle les objets et traces intéressants sont stockés dans les canaux R et G.Pourquoi en avons-nous besoin en termes de luminosité? Il se trouve que l'instinct de Geralt a un rayon limité, donc les objets n'obtiennent des contours que lorsque le joueur est assez proche d'eux.Regardez cet aspect en action:Nous commençons par nettoyer la texture de luminosité, en la remplissant de noir.Ensuite, deux appels de dessin en plein écran sont effectués: le premier pour la trace, le second pour les objets intéressants:Le premier appel de tirage est effectué pour les traces - le canal vert:Le deuxième appel est fait pour des objets intéressants - le canal rouge:Eh bien, mais comment déterminer les pixels à considérer? Nous devrons utiliser le tampon de pochoir!Pour chacun de ces appels, un test au pochoir est effectué et seuls les pixels qui étaient précédemment marqués comme «8» (premier appel de tirage) ou «4» sont acceptés.Visualisation du test au pochoir pour les traces:... et pour les objets intéressants:Comment le test est-il effectué dans ce cas? Vous pouvez en apprendre davantage sur les bases du test de pochoir dans un bon article . En général, la formule de test du pochoir a la forme suivante: if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel
où:StencilRef est la valeur transmise par l'appel de l'API,StencilReadMask est le masque utilisé pour lire la valeur du stensil (notez qu'il est présent à gauche et à droite),OP est l'opérateur de comparaison, défini via l'API,StencilValue est la valeur de tampon du pochoir dans le pixel en cours de traitement.Il est important de comprendre que nous utilisons des ET binaires pour calculer les opérandes.Après avoir pris connaissance des bases, voyons comment ces paramètres sont utilisés dans ces appels de dessin:Condition du pochoir pour les tracesÉtat du pochoir pour les objets intéressantsHa! Comme nous pouvons le voir, la seule différence est ReadMask. Voyons ça! Remplacez ces valeurs dans l'équation de test du gabarit: Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE
Astucieusement. Comme vous pouvez le voir, dans ce cas, nous ne comparons pas la valeur de stensil, mais vérifions si un certain bit du tampon de gabarit est défini. Chaque pixel du tampon de gabarit a le format uint8, donc l'intervalle de valeurs est [0-255].Remarque: tous les appels DrawIndexed (36) sont liés au rendu des empreintes en tant qu'empreintes, donc dans ce cadre particulier, la carte de luminosité a la forme finale suivante:Mais avant le test au pochoir, il y a un pixel shader. 28738 et 28748 utilisent le même pixel shader: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret
Ce pixel shader écrit sur une seule cible de rendu, donc les lignes 24-27 sont redondantes.La première chose qui se produit ici est l'échantillonnage en profondeur (avec un échantillonneur ponctuel avec une valeur limite), ligne 1. Cette valeur est utilisée pour recréer une position dans le monde en la multipliant par une matrice spéciale, suivie d'une division en perspective (lignes 2-6).En prenant la position de Geralt (cb3 [7] .xyz - notez que ce n'est pas la position de la caméra!), Nous calculons la distance de Geralt à ce point particulier (lignes 7-9).L'entrée suivante est importante dans ce shader:- cb3 [0] .rgb - couleur de sortie. Il peut avoir le format float3 (0, 1, 0) (traces) ou float3 (1, 0, 0) (objets intéressants),- cb3 [6] .y - facteur d'échelle de distance. Affecte directement le rayon et la luminosité de la sortie finale.Plus tard, nous avons des formules assez délicates pour calculer la luminosité en fonction de la distance entre Geralt et l'objet. Je peux supposer que tous les coefficients sont sélectionnés expérimentalement.La sortie finale est l' intensité de la couleur * . Le code HLSL ressemblera à ceci: struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) {
Une petite comparaison du code shader assembleur d'origine (à gauche) et de mon (à droite).Ce fut la première étape de l'effet de flair de la sorcière . En fait, c'est le plus simple.Partie 4. The Witcher Flair (carte muette)
Encore une fois, jetez un œil à la scène que nous explorons:Dans la première partie de l'analyse de l'effet de l'instinct de la sorcière, j'ai montré comment la "carte de luminosité" est générée.Nous avons une texture plein écran au format R11G11B10_FLOAT, qui pourrait ressembler à ceci:Le canal vert signifie «empreintes», le rouge - des objets intéressants avec lesquels Geralt peut interagir.Après avoir reçu cette texture, nous pouvons passer à l'étape suivante - je l'ai appelée la «carte de contour».C'est une texture un peu bizarre du format 512x512 R16G16_FLOAT. Il est important qu'il soit implémenté dans le style du «ping-pong». La carte de contour de l'image précédente est les données d'entrée (avec la carte de luminosité) pour générer une nouvelle carte de contour dans l'image actuelle.Les tampons de ping-pong peuvent être implémentés de plusieurs façons, mais j'aime surtout le (pseudo-code) suivant:
Cette approche, où l'entrée est toujours [m_outlineIndex] et la sortie est toujours [! M_outlineIndex] , offre une flexibilité pour l'utilisation d'autres post-effets.Jetons un coup d'œil au 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_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret
Comme vous pouvez le voir, la carte de contour de sortie est divisée en quatre carrés égaux, et c'est la première chose que nous devons étudier: 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0)
Nous commençons par calculer le plancher (TextureUV * 2.0), ce qui nous donne ce qui suit:Pour déterminer les carrés individuels, une petite fonction est utilisée: float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; }
Notez que la fonction renvoie 1.0 avec entrée float2 (0.0, 0.0).Ce cas se produit dans le coin supérieur gauche. Pour obtenir la même situation dans le coin supérieur droit, soustrayez float2 (1, 0) des texcoords arrondis, soustrayez float2 (0, 1) pour le carré vert et float2 (1.0, 1.0) pour le carré jaune.Donc:
float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 );
Chacun des composants du masque est soit zéro soit un, et est responsable d'un carré de la texture. Par exemple, mask.r et mask.w :mask.rmask.wNous avons un masque , passons. La ligne 15 échantillonne la carte de luminance. Notez que la texture de luminance est au format R11G11B10_FLOAT, bien que nous échantillonnions tous les composants rgba. Dans cette situation, on suppose que .a est 1.0f.Les Texcoords utilisés pour cette opération peuvent être calculés en frac (TextureUV * 2.0) . Par conséquent, le résultat de cette opération peut, par exemple, ressembler à ceci:Vous voyez la ressemblance?La prochaine étape est très intelligente - le produit scalaire à quatre composants (dp4) est effectué: 16: dp4 r1.x, r1.xyzw, r2.xyzw
Ainsi, seul le canal rouge (c'est-à-dire uniquement les objets intéressants) reste dans le coin supérieur gauche, seul le canal vert dans le coin supérieur droit (uniquement les traces) et tout dans le coin inférieur droit (car la composante de luminance .w est indirectement définie sur 1.0). Excellente idée. Le résultat du produit scalaire ressemble à ceci:Ayant reçu ce masterFilter , nous sommes prêts à déterminer les contours des objets. Ce n'est pas aussi difficile que cela puisse paraître. L'algorithme est très similaire à celui utilisé pour obtenir la netteté - nous devons obtenir la différence absolue maximale de valeurs.Voici ce qui se passe: nous échantillonnons quatre texels à côté du texel en cours de traitement (important: dans ce cas, la taille du texel est 1.0 / 256.0!) Et calculons les différences absolues maximales pour les canaux rouge et vert: float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference);
Maintenant, si nous multiplions le filtre par maxAbsDifference ...Très simple et efficace.Après avoir reçu les contours, nous échantillonnons la carte de contour de l'image précédente.Ensuite, afin d'obtenir un effet «fantomatique», nous prenons une partie des paramètres calculés sur la passe actuelle et les valeurs de la carte de contour.Dites bonjour à notre vieil ami - bruit entier. Il est présent ici. Les paramètres d'animation (cb3 [0] .zw) sont extraits du tampon constant et changent au fil du temps. float2 outlines = masterFilter * maxAbsDifference;
Remarque: si vous souhaitez implémenter vous-même l'instinct de la sorcière, je recommande de limiter le bruit entier à l'intervalle [-1; 1] (comme indiqué sur son site Internet). Il n'y avait aucune restriction dans le shader TW3 d'origine, mais sans lui, j'ai eu de terribles artefacts et la carte d'ensemble était instable.Ensuite, nous échantillonnons la carte de contour de la même manière que la carte de luminosité précédente (cette fois, le texel a une taille de 1,0 / 512,0), et calculons la valeur moyenne de la composante .x:
Ensuite, à en juger par le code assembleur, la différence entre la moyenne et la valeur de ce pixel particulier est calculée, après quoi une distorsion par bruit entier est effectuée:
L'étape suivante consiste à déformer la valeur de la «vieille» carte de contour à l'aide du bruit - c'est la ligne principale qui donne à la texture de sortie une impression de bloc.Il y a ensuite d'autres calculs, après lesquels, à la toute fin, «l'atténuation» est calculée.
Voici une courte vidéo montrant une carte muette en action:Si vous êtes intéressé par le pixel shader complet, alors il est disponible ici . Shader est compatible avec RenderDoc.Il est intéressant (et pour être honnête, légèrement ennuyeux) que malgré l'identité du code assembleur avec le shader original de Witcher 3, l'apparence finale de la carte de contour dans RenderDoc change!Remarque: dans le dernier passage (voir la partie suivante), vous verrez que seul le canal .r de la carte de contour est utilisé. Pourquoi alors avons-nous besoin du canal .g? Je pense que c'est une sorte de tampon de ping-pong dans une texture - notez que .r contient le canal .g + une nouvelle valeur.Partie 5: The Witcher Flair (Fisheye et le résultat final)
Nous allons brièvement énumérer ce que nous avons déjà: dans la première partie, dédiée à l'instinct du sorceleur, une carte de luminosité en plein écran est générée qui indique à quel point l'effet devrait être perceptible en fonction de la distance. Dans la deuxième partie, j'ai exploré plus en détail la map de contour, qui est responsable des contours et de l'animation de l'effet fini.Nous sommes arrivés à la dernière étape. Tout cela doit être combiné! La dernière passe est un quad en plein écran. Entrées: tampon de couleurs, carte de contour et carte de luminance.À:Après:
Encore une fois, je vais montrer la vidéo avec l'effet appliqué:Comme vous pouvez le voir, en plus d'appliquer des contours à des objets que Geralt peut voir ou entendre, l'effet fish-eye est appliqué à l'ensemble de l'écran, et l'ensemble de l'écran (en particulier les coins) devient grisâtre pour transmettre la sensation d'un véritable chasseur de monstres.Code de pixel shader entièrement assemblé: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret
82 lignes - nous avons donc beaucoup de travail à faire!Tout d'abord, jetez un œil aux données d'entrée:
La valeur principale responsable de l'ampleur de l'effet est fisheyeAmount . Je pense qu'il passe progressivement de 0,0 à 1,0 lorsque Geralt commence à utiliser son instinct. Les autres valeurs ne changent guère, mais je soupçonne que certaines d'entre elles seraient différentes si l'utilisateur avait désactivé l'effet fisheye dans les options (je n'ai pas vérifié cela).La première chose qui se passe ici est que le shader calcule le masque responsable des angles de gris: 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000)
En HLSL, nous pouvons écrire ceci comme suit:
Tout d'abord, l'intervalle [-1; 1] UV et leurs valeurs absolues. Ensuite, il y a une «pression» délicate. Le masque fini est le suivant:Je reviendrai sur ce masque plus tard.Maintenant, je vais intentionnellement sauter quelques lignes de code et étudier attentivement le code responsable de l'effet de zoom. 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw
Tout d'abord, les coordonnées de texture «doublées» sont calculées et la soustraction float2 (1, 1) est effectuée: float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0);
Un tel texcoord peut être visualisé comme suit:Ensuite, le point de produit scalaire (uv4, uv4) est calculé , ce qui nous donne le masque:qui est utilisé pour multiplier par les texcoords ci-dessus:Important: dans le coin supérieur gauche (pixels noirs) les valeurs sont négatives. Ils sont affichés en noir (0,0) en raison de la précision limitée du format R11G11B10_FLOAT. Il n'a pas de bit de signe, donc les valeurs négatives ne peuvent pas y être stockées.Ensuite, le coefficient d'atténuation est calculé (comme je l'ai dit ci-dessus, fisheyeAmount varie de 0,0 à 1,0). float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation;
Ensuite, la restriction (max / min) et une multiplication sont effectuées.Ainsi, le décalage est calculé. Pour calculer le uv final, qui sera utilisé pour échantillonner la texture de couleur, nous effectuons simplement la soustraction:float2 colorUV = mainUv - offset;En échantillonnant la texture de couleur de la couleur d'entrée UV , nous obtenons une image déformée près des coins:Contours
L'étape suivante consiste à échantillonner la carte de contour pour trouver les contours. C'est assez simple, on trouve d'abord des texcoords pour échantillonner les contours d'objets intéressants, puis on fait de même pour les pistes:
Objets intéressants de la carte des contoursTraces de la carte de contourIl convient de noter que nous échantillonnons uniquement le canal .x de la carte de contour et ne prenons en compte que les carrés supérieurs.Mouvement
Pour implémenter le mouvement des pistes, presque la même astuce est utilisée que pour l'effet d'intoxication. Un cercle de taille unitaire est ajouté et nous échantillonnons 8 fois la carte de contour pour les objets et traces intéressants, ainsi que la texture des couleurs.Notez que nous n'avons divisé les chemins trouvés que par 8.0.Puisque nous sommes dans l'espace des coordonnées de texture [0-1] 2 , la présence d'un cercle de rayon 1 pour encercler un seul pixel créera des artefacts inacceptables:Par conséquent, avant de poursuivre, découvrons comment ce rayon est calculé. Pour ce faire, nous devons revenir aux lignes manquantes 15-21. Un problème mineur avec le calcul de ce rayon est que son calcul est dispersé autour du shader (probablement en raison des optimisations du shader par le compilateur). Voici donc la première partie (15-21) et la seconde (41-42): 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000)
Comme vous pouvez le voir, nous considérons uniquement les texels de [0,00 - 0,03] à côté de chaque surface, résumons leurs valeurs, multiplions 20 et saturons. Voici à quoi ils ressemblent après les lignes 15-21:Et voici comment après la ligne 41:À la ligne 42, nous multiplions cela par 0,03, cette valeur est le rayon du cercle pour tout l'écran. Comme vous pouvez le voir, plus près des bords de l'écran, le rayon devient plus petit.Maintenant, nous pouvons regarder le code assembleur responsable du mouvement: 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop
Restons ici une minute. Sur la ligne 40, nous obtenons le coefficient de temps - juste elapsedTime * 0,1 . À la ligne 43, nous avons un tampon pour la texture de couleur obtenue à l'intérieur de la boucle.r0.x (lignes 41-42) est, comme nous le savons maintenant, le rayon du cercle. r4.x (ligne 44) est le contour des objets intéressants, r4.y (ligne 45) est le contour des pistes (précédemment divisé par 8!), et r4.z (ligne 46) est le compteur de boucle.Comme vous pouvez vous y attendre, la boucle a 8 itérations. Nous commençons par calculer l'angle en radians i * PI_4 , ce qui nous donne 2 * PI - un cercle complet. L'angle est déformé au fil du temps.À l'aide de sincos, nous déterminons le point d'échantillonnage (cercle unité) et modifions le rayon en utilisant la multiplication (ligne 54).Après cela, nous faisons le tour du pixel dans un cercle et échantillonnons les contours et la couleur. Après le cycle, nous obtenons les valeurs moyennes (dues à la division par 8) des contours et des couleurs. float timeParam = time * 0.1;
L'échantillonnage des couleurs sera effectué de la même manière, mais nous ajouterons un décalage multiplié par un cercle «unique» à la couleur de base UV .Luminosité
Après le cycle, nous échantillonnons la carte de luminosité et modifions les valeurs de luminosité finales (car la carte de luminosité ne sait rien des contours): 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx
Code HLSL:
Coins gris et l'unification finale de tout
La couleur grise plus proche des coins est calculée à l'aide du produit scalaire (ligne d'assemblage 69):
Ensuite, deux interpolations s'ensuivent. Le premier combine le gris avec la «couleur dans le cercle» en utilisant le premier masque que j'ai décrit, de sorte que les coins deviennent gris. De plus, il existe un coefficient de 0,6, ce qui réduit la saturation de l'image finale:La seconde combine la première couleur avec la précédente en utilisant fisheyeAmount . Cela signifie que l'écran devient progressivement plus sombre (en raison de la multiplication par 0,6) et gris dans les coins! Ingénieux.HLSL:
Nous pouvons maintenant passer à l'ajout des contours des objets.Les couleurs (rouge et jaune) sont extraites du tampon constant.
Fuh! Nous sommes presque à la ligne d'arrivée!Nous avons la couleur finale, il y a la couleur de l'instinct de la sorcière ... il reste à les combiner en quelque sorte!Et pour cela, un simple ajout ne convient pas. Nous calculons d'abord le produit scalaire: 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) );
qui ressemble à ceci:Et ces valeurs à la toute fin sont utilisées pour interpoler entre la couleur et le flair (saturé) de la sorcière: 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting;
Et c'est tout.Le shader complet est disponible ici .Comparaison de mes shaders (gauche) et originaux (droite):J'espère que cet article vous a plu! Il y a beaucoup d'idées brillantes dans la mécanique de "l'instinct de sorceleur", et le résultat final est très plausible.[Parties précédentes de l'analyse: première et deuxième .]