Cómo se implementa la representación de The Witcher 3: relámpago, estilo de brujo y otros efectos

imagen

Parte 1. Cremalleras


En esta parte, veremos el proceso de renderización de rayos en Witcher 3: Wild Hunt.

La representación del rayo se realiza un poco más tarde que el efecto de cortina de lluvia , pero aún se produce en el pase de representación directa. Se puede ver un rayo en este video:


Desaparecen muy rápidamente, por lo que es mejor mirar el video a una velocidad de 0.25.

Puedes ver que estas no son imágenes estáticas; Con el tiempo, su brillo cambia ligeramente.

En cuanto a los matices de representación, existen muchas similitudes con el dibujo de una cortina de lluvia en la distancia, por ejemplo, los mismos estados de fusión (combinación aditiva) y profundidad (la comprobación está habilitada, no se realiza el registro de profundidad).


Escena sin relámpagos


Escena del rayo

En términos de geometría de rayos, The Witcher 3 es una malla en forma de árbol. Este ejemplo de rayo está representado por la siguiente malla:


Tiene coordenadas UV y vectores normales. Todo esto es útil en la etapa de sombreado de vértices.

Sombreador de vértices


Echemos un vistazo al código de sombreador de vértices ensamblado:

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 

Hay muchas similitudes con la cortina de lluvia vertex shader, por lo que no repetiré. Quiero mostrarle la diferencia importante que hay en las líneas 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 

En primer lugar, cb1 [8] .xyz es la posición de la cámara, y r2.xyz es la posición en el espacio mundial, es decir, la línea 11 calcula el vector desde la cámara hasta la posición en el mundo. Luego, las líneas 12-15 calculan la longitud (worldPos - cameraPos) * 0.000001.

v2.xyz es el vector normal de la geometría entrante. La línea 16 lo extiende desde el intervalo [0-1] al intervalo [-1; 1].

Luego se calcula la posición final en el mundo:

finalWorldPos = worldPos + length (worldPos - cameraPos) * 0.000001 * normalVector
El fragmento de código HLSL para esta operación será algo como esto:

  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output; 

Esta operación da como resultado una pequeña "explosión" de la malla (en la dirección del vector normal). Experimenté reemplazando 0.000001 con varios otros valores. Aquí están los resultados:


0.000002


0.000005


0.00001


0.000025

Sombreador de píxeles


Bueno, descubrimos el sombreador de vértices, ¡ahora es el momento de llegar al código del ensamblador para el sombreador de píxeles!

  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 

Buenas noticias: el código no es tan largo.

Malas noticias:

  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 que se trata todo esto?

Honestamente, esta no es la primera vez que veo una pieza de código de ensamblador en los sombreadores Witcher 3. Pero cuando lo conocí por primera vez, pensé: "¿Qué demonios es esto?"

Algo similar se puede encontrar en algunos otros sombreadores TW3. No describiré mis aventuras con este fragmento, y solo diré que la respuesta está en el ruido entero :

  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } 

Como puede ver, en el sombreador de píxeles se llama dos veces. Usando las guías de este sitio web, podemos entender cómo se implementa correctamente el ruido suave. Volveré a esto en un minuto.

Mire la línea 0: aquí estamos animando en base a la siguiente fórmula:

animation = elapsedTime * animationSpeed ​​+ TextureUV.x
Estos valores, después de redondear hacia el lado inferior ( piso ) (instrucción round_ni ) en el futuro, se convierten en puntos de entrada para el ruido entero. Por lo general, calculamos el valor de ruido para dos enteros y luego calculamos el valor final interpolado entre ellos (consulte el sitio web de libnoise para obtener más detalles).

Bueno, este es un ruido entero , pero después de todo, ¡todos los valores mencionados anteriormente (también redondeados hacia abajo) son flotantes!

Tenga en cuenta que no hay instrucciones ftoi aquí . Supongo que los programadores de CD Projekt Red han utilizado aquí la función interna HLSL asint , que realiza la conversión de valores de punto flotante "reinterpret_cast" y los trata como un patrón entero.

El peso de interpolación para los dos valores se calcula en las líneas 10-11.

interpolationWeight = 1.0 - frac (animación);
Este enfoque nos permite interpolar entre valores a lo largo del tiempo.

Para crear un ruido suave, este interpolador se pasa a la función SCurve :

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } 


Función Smoothstep [libnoise.sourceforge.net]

Esta característica se conoce como "paso suave". Pero como puede ver en el código del ensamblador, esta no es una función de paso suave interna de HLSL. Una función interna aplica restricciones para que los valores sean verdaderos. Pero como sabemos que interpolationWeight siempre estará en el rango [0-1], estas comprobaciones se pueden omitir de forma segura.

Al calcular el valor final, se utilizan varias operaciones de multiplicación. Vea cómo la salida alfa final puede cambiar según el valor del ruido. Esto es conveniente porque afectará la opacidad del rayo renderizado, al igual que en la vida real.

Sombreador de píxeles listo:

  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; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); } 

Para resumir


En esta parte, describí una forma de representar un rayo en The Witcher 3.

¡Estoy muy contento de que el código de ensamblador que salió de mi sombreador coincida completamente con el original!


Parte 2. Trucos tontos del cielo


Esta parte será ligeramente diferente de las anteriores. En él, quiero mostrarte algunos aspectos del sombreador de cielo Witcher 3.

¿Por qué "trucos tontos" y no todo el sombreador? Bueno, hay varias razones. En primer lugar, el sombreador de cielo Witcher 3 es una bestia bastante compleja. El sombreador de píxeles de la versión 2015 contiene 267 líneas de código de ensamblador, y el sombreador del DLC Blood and Wine contiene 385 líneas.

Además, reciben mucha información, lo que no es muy propicio para la ingeniería inversa del código HLSL completo (¡y legible!).

Por lo tanto, decidí mostrar solo una parte de los trucos de estos sombreadores. Si encuentro algo nuevo, complementaré la publicación.

Las diferencias entre la versión 2015 y el DLC (2016) son muy notables. En particular, incluyen diferencias en el cálculo de las estrellas y su parpadeo, un enfoque diferente para representar el Sol ... El sombreador Blood and Wine incluso calcula la Vía Láctea por la noche.

Comenzaré con lo básico y luego hablaré sobre trucos estúpidos.

Los fundamentos


Como la mayoría de los juegos modernos, Witcher 3 usa skydome para modelar el cielo. Mire el hemisferio utilizado para esto en Witcher 3 (2015). Nota: en este caso, el cuadro delimitador de esta malla está en el rango de [0,0,0] a [1,1,1] (Z es el eje que apunta hacia arriba) y tiene UV distribuidos suavemente. Más tarde los usamos.


La idea detrás de skydome es similar a la idea de skybox (la única diferencia es la malla utilizada). En la etapa de sombreado de vértices, transformamos el skydome en relación con el observador (generalmente de acuerdo con la posición de la cámara), lo que crea la ilusión de que el cielo está realmente muy lejos, nunca llegaremos a él.

Si lees las partes anteriores de esta serie de artículos, entonces sabes que “The Witcher 3” usa la profundidad inversa, es decir, el plano lejano es 0.0f, y el más cercano es 1.0f. Para que la salida de skydome se ejecute por completo en el plano lejano, en los parámetros de la ventana de exploración establecemos MinDepth en el mismo valor que MaxDepth :


Para saber cómo se utilizan los campos MinDepth y MaxDepth durante la conversión de la ventana de exploración, haga clic aquí (docs.microsoft.com).

Sombreador de vértices


Comencemos con el sombreador de vértices. En Witcher 3 (2015), el código del sombreador del ensamblador es el siguiente:

  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 

En este caso, el sombreador de vértices transfiere solo los códigos de texto y una posición en el espacio mundial a la salida. En Blood and Wine, también muestra un vector normal normalizado. Consideraré la versión 2015 porque es más simple.

Mire el búfer constante designado como cb2 :


Aquí tenemos una matriz del mundo (escala uniforme en 100 y transferencia relativa a la posición de la cámara). Nada complicado cb2_v4 y cb2_v5 son los factores de escala / desviación utilizados para convertir las posiciones de vértice del intervalo [0-1] al intervalo [-1; 1]. Pero aquí, estos coeficientes "comprimen" el eje Z (hacia arriba).


En las partes anteriores de la serie, teníamos sombreadores de vértices similares. El algoritmo general es transferir aún más los códigos de texto, luego se calcula la posición teniendo en cuenta los coeficientes de escala / desviación, luego se calcula la posición W en el espacio mundial, luego se calcula la posición final del espacio de recorte multiplicando matWorld y matViewProj -> su producto se usa para multiplicar por la posición para obtener la posición final SV_Position .

Por lo tanto, el HLSL de este sombreador de vértices debería ser algo como esto:

  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; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; } 

Comparación de mi sombreador (izquierda) y el original (derecha):


Una excelente propiedad de RenderDoc es que nos permite inyectar nuestro propio sombreador en lugar del original, y estos cambios afectarán la tubería hasta el final del marco. Como puede ver en el código HLSL, he proporcionado varias opciones para hacer zoom y transformar la geometría final. Puedes experimentar con ellos y obtener resultados muy divertidos:


Optimización del sombreador de vértices


¿Notó el problema del sombreador de vértices original? ¡La multiplicación de vértices de una matriz por una matriz es completamente redundante! Encontré esto en al menos algunos sombreadores de vértices (por ejemplo, en el sombreador, una cortina de lluvia en la distancia ). ¡Podemos optimizarlo multiplicando inmediatamente PositionW por matViewProj !

Entonces, podemos reemplazar este código con HLSL:

  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); 

como sigue:

  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW ); 

La versión optimizada nos da el siguiente código de ensamblaje:

  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 

Como puede ver, redujimos el número de instrucciones de 26 a 12, un cambio bastante significativo. No sé qué tan extendido está este problema en el juego, pero por el amor de Dios, CD Projekt Red, ¿quizás lanzar un parche? :)

Y no estoy bromeando. Puede insertar mi sombreador optimizado en lugar del RenderDoc original y verá que esta optimización no afecta visualmente nada. Honestamente, no entiendo por qué CD Projekt Red decidió realizar la multiplicación de vértices de una matriz por una matriz ...

El sol


En The Witcher 3 (2015), el cálculo de la dispersión atmosférica y el Sol consta de dos llamadas separadas:


Witcher 3 (2015) - Hasta


Witcher 3 (2015) - con el cielo


Witcher 3 (2015) - con cielo + sol

La representación del Sol en la versión 2015 es muy similar a la representación de la Luna en términos de geometría y estados de mezcla / profundidad.

Por otro lado, en "Blood and Wine", el cielo con el Sol se representa en una pasada:


The Witcher 3: Blood and Wine (2016) - Al cielo


The Witcher 3: Blood and Wine (2016) - con el cielo y el sol

No importa cómo renderice el Sol, en algún momento aún necesita la dirección (normalizada) de la luz solar. La forma más lógica de obtener este vector es usar coordenadas esféricas . De hecho, solo necesitamos dos valores que indiquen dos ángulos (en radianes): Phi y theta . Habiéndolos recibido, podemos suponer que r = 1 , reduciéndolo así. Luego, para las coordenadas cartesianas con el eje Y apuntando hacia arriba, puede escribir el siguiente código en HLSL:

  float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir); 

Por lo general, la dirección de la luz solar se calcula en la aplicación y luego se pasa al búfer constante para su uso futuro.

Habiendo recibido la dirección de la luz solar, podemos profundizar en el código de ensamblador del sombreador de píxeles "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 ... 

Entonces, en primer lugar, cb12 [0] .xyz es la posición de la cámara, y en r0.xyz almacenamos la posición del vértice (esta es la salida del sombreador de vértices). Por lo tanto, la línea 100 calcula el vector worldToCamera . Pero eche un vistazo a las líneas 105-107. Podemos escribirlos como normalizar (-worldToCamera) , es decir, calculamos el vector cameraToWorld normalizado.

  120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx 

Luego calculamos el producto escalar de los vectores cameraToWorld y sunDirection . Recuerde que deben ser normalizados. También saturamos esta expresión completa para limitarla al intervalo [0-1].

Genial Este producto escalar se almacena en r1.x. Veamos dónde se aplica a continuación ...

  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 trinidad "log, mul, exp" es exponenciación. Como puede ver, elevamos nuestro coseno (el producto escalar de vectores normalizados) hasta cierto punto. Puedes preguntar por qué. De esta manera, podemos crear un gradiente que imite al sol. (Y la línea 155 afecta la opacidad de este gradiente, de modo que, por ejemplo, lo reiniciamos para ocultar completamente el Sol). Aquí hay algunos ejemplos:


exponente = 54


exponente = 2400

¡Teniendo este gradiente, lo usamos para interpolar entre skyColor y sunColor ! Para evitar artefactos, debe saturar el valor en la línea 120.

Vale la pena señalar que este truco se puede utilizar para simular las coronas de la luna (a valores de exponente bajos). Para hacer esto, necesitamos el vector moonDirection , que puede calcularse fácilmente utilizando coordenadas esféricas.

El código HLSL listo para usar puede verse como el siguiente fragmento:

  float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient ); 

Movimiento de estrellas


Si haces un lapso de tiempo del claro cielo nocturno de Witcher 3, puedes ver que las estrellas no son estáticas, ¡se mueven un poco por el cielo! Me di cuenta de esto casi por accidente y quería saber cómo se implementó.

Comencemos con el hecho de que las estrellas en Witcher 3 se presentan como un mapa cúbico de tamaño 1024x1024x6. Si lo piensa, puede comprender que esta es una solución muy conveniente que le permite tomar fácilmente direcciones para muestrear un mapa cúbico.

Veamos el siguiente código de ensamblador:

  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 

Para calcular el vector de muestreo final (línea 173), comenzamos calculando el vector worldToCamera normalizado (líneas 159-162).

Luego calculamos dos productos vectoriales (163-164, 165-166) con moonDirection , y luego calculamos tres productos escalares para obtener el vector de muestreo final. Código 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; 

Nota para mí: este es un código muy bien diseñado, y debería investigarlo con más detalle.

Nota para los lectores: si sabes más sobre esta operación, ¡dímelo!

Estrellas centelleantes


Otro truco interesante que me gustaría explorar con más detalle es el parpadeo de las estrellas.Por ejemplo, si deambulas por Novigrad cuando hace buen tiempo, notarás que las estrellas brillan.

Tenía curiosidad por cómo se implementó esto. Resultó que la diferencia entre la versión 2015 y "Blood and Wine" es bastante grande. Por simplicidad, consideraré la versión 2015.

Entonces, comenzamos justo después de muestrear starsColor de la sección anterior:

  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 

. .

starsColor 173 - offset . offset (r2.xyz, 235), , - (237-242) (243).

, ? , . offset . skydome — .

offset , , UV skydome (v0.xy) , (cb[0].x).

ishr/xor/and, .

, , , . , ( iadd ) ( reversebits ; bfrev ).

, . .

4 «» . , 4 :

  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B ); 

4 ( , itof ):

1 — r5.x,

2 — r4.w,

3 — r1.w,

4 — r5.y

itof ( 216) :

  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 

S- UV, . Entonces

  float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY ); 

, :

  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; 

offset :


Después de calcular starsColorDisturbed, la parte más difícil está completa. ¡Hurra!

El siguiente paso es realizar la corrección gamma tanto para starsColor como para starsColorDisturbed , después de lo cual se multiplican:

  starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed; 

Estrellas: los toques finales


Tenemos starsFinal en r1.xyz. Al final del procesamiento en estrella, ocurre lo siguiente:

  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) 

Esto es mucho más fácil en comparación con las estrellas parpadeantes y en movimiento.

Entonces, comenzamos elevando starsFinal a una potencia de 2.5, esto nos permite controlar la densidad de las estrellas. Bastante inteligente. Luego hacemos que el color máximo de las estrellas sea igual a float3 (1, 1, 1).

cb0 [9] .w se usa para controlar la visibilidad general de las estrellas. Por lo tanto, podemos esperar que durante el día este valor sea 1.0 (que da una multiplicación por cero), y en la noche - 0.0.

Al final, aumentamos la visibilidad de las estrellas en 10. ¡Y eso es todo!

Parte 3. The Witcher Flair (objetos y mapa de brillo)


Witcher 3. , , . .

« ». — , , . , , , . .

:


, :


Como puede ver, hay dos tipos de objetos: aquellos con los que Geralt puede interactuar (contorno amarillo) y los rastros asociados con la investigación (contorno rojo). Después de que Geralt examina el rastro rojo, puede convertirse en amarillo (primer video). Observe que toda la pantalla se vuelve gris y se agrega un efecto ojo de pez (segundo video).

Este efecto es bastante complicado, así que decidí dividir su investigación en tres partes.

En el primero hablaré sobre la selección de objetos, en el segundo, sobre la generación del circuito, y en el tercero, sobre la unificación final de todo esto en uno.

Seleccionar objetos


Como dije, hay dos tipos de objetos, y necesitamos distinguirlos. En Witcher 3, esto se implementa utilizando un búfer de galería de símbolos. Al generar mallas GBuffer que se deben marcar como "trazas" (rojo), se representan con stencil = 8. Las mallas marcadas con amarillo como objetos "interesantes" se representan con stencil = 4.

Por ejemplo, las siguientes dos texturas muestran un marco de ejemplo con instinto de brujo visible y el búfer de plantilla correspondiente:



Stencil Buffer Brief


El búfer de plantilla a menudo se usa en juegos para etiquetar mallas. A ciertas categorías de mallas se les asigna la misma ID.

La idea es utilizar la función Siempre con el operador Reemplazar si la prueba de plantilla es exitosa, y con el operador Mantener en todos los demás casos.

Así es como se implementa usando D3D11:

  D3D11_DEPTH_STENCIL_DESC depthstencilState; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue ); 

El valor stensil que se escribirá en el búfer se pasa como StencilRef en la llamada a la API:

  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... ); 

Brillo de renderizado


R11G11B10_FLOAT, R G.

? , , , .

:



, .

: „“, — :


— :


— :


Bueno, pero ¿cómo determinamos qué píxeles considerar? ¡Tendremos que usar el búfer de la plantilla!

Para cada una de estas llamadas, se realiza una prueba de plantilla y solo se aceptan aquellos píxeles que se marcaron previamente como "8" (primera llamada de sorteo) o "4".

Visualización de la prueba de plantilla para trazas:


... y para objetos interesantes:


¿Cómo se realiza la prueba en este caso? Puede aprender sobre los conceptos básicos de las pruebas de plantilla en una buena publicación . En general, la fórmula de prueba de la plantilla tiene la siguiente forma:

  if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel 

:
StencilRef — , API,

StencilReadMask — , (, , ),

OP — , API,

StencilValue — - .

, AND.

, , :







! , ReadMask. ! -:

  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 

Hábilmente. Como puede ver, en este caso no comparamos el valor de stensil, sino que verificamos si un cierto bit del búfer de la plantilla está configurado. Cada píxel del búfer de plantilla tiene el formato uint8, por lo que el intervalo de valores es [0-255].

Nota: todas las llamadas a DrawIndexed (36) están relacionadas con la representación de huellas como trazas, por lo que en este marco en particular el mapa de brillo tiene la siguiente forma final:


Pero antes de la prueba de plantilla hay un sombreador de píxeles. Tanto 28738 como 28748 usan el mismo sombreador de píxeles:

  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 

render target, 24-27 .

, — ( ), 1. ( 2-6).

(cb3[7].xyz — , !), ( 7-9).

:

— cb3[0].rgb — . float3(0, 1, 0) () float3(1, 0, 0) ( ),
- cb3 [6] .y - factor de escala de distancia. Afecta directamente el radio y el brillo de la salida final.

Más tarde, tenemos fórmulas bastante complicadas para calcular el brillo dependiendo de la distancia entre Geralt y el objeto. Puedo suponer que todos los coeficientes se seleccionan experimentalmente.

El resultado final es el color * intensidad .

El código HLSL se verá así:

  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) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; } 

Una pequeña comparación del código de sombreador del ensamblador original (izquierda) y mi (derecha).


Esta fue la primera etapa del efecto instinto de la bruja . De hecho, es el más simple.

Parte 4. The Witcher Flair (mapa de contorno)


Una vez más, eche un vistazo a la escena que estamos explorando:


En la primera parte del análisis del efecto del instinto de la bruja, mostré cómo se genera el "mapa de brillo".

Tenemos una textura de pantalla completa del formato R11G11B10_FLOAT, que podría verse así:


El canal verde significa "huellas", el rojo - objetos interesantes con los que Geralt puede interactuar.

Una vez recibida esta textura, podemos pasar a la siguiente etapa: la llamé "mapa de contorno".


Esta es una textura un poco extraña del formato 512x512 R16G16_FLOAT. Es importante que se implemente en el estilo de "ping pong". El mapa de contorno del cuadro anterior son los datos de entrada (junto con el mapa de brillo) para generar un nuevo mapa de contorno en el cuadro actual.

Los buffers de ping-pong se pueden implementar de muchas maneras, pero personalmente me gusta más el siguiente (pseudocódigo):

  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; } 

Este enfoque, donde la entrada es siempre [m_outlineIndex] , y la salida es siempre [! M_outlineIndex] , proporciona flexibilidad para el uso de más efectos posteriores.

Echemos un vistazo al sombreador de píxeles:

  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 

Como puede ver, el mapa de contorno de salida se divide en cuatro cuadrados iguales, y esto es lo primero que debemos estudiar:

  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) 

Comenzamos calculando el piso (TextureUV * 2.0), que nos da lo siguiente:


Para determinar los cuadrados individuales, se utiliza una pequeña función:

  float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; } 

Observe que la función devuelve 1.0 con la entrada float2 (0.0, 0.0).

Este caso ocurre en la esquina superior izquierda. Para obtener la misma situación en la esquina superior derecha, reste float2 (1, 0) de los texcoords redondeados, reste float2 (0, 1) para el cuadrado verde y float2 (1.0, 1.0) para el cuadrado amarillo.

Entonces

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

Cada uno de los componentes de la máscara es cero o uno, y es responsable de un cuadrado de la textura. Por ejemplo, mask.r y mask.w :


mask.r


mask.w

Tenemos máscara , sigamos adelante. La línea 15 muestra el mapa de luminancia. Tenga en cuenta que la textura de luminancia está en el formato R11G11B10_FLOAT, aunque muestreamos todos los componentes rgba. En esta situación, se supone que .a es 1.0f.

Los Texcoords utilizados para esta operación se pueden calcular como frac (TextureUV * 2.0) . Por lo tanto, el resultado de esta operación puede, por ejemplo, verse así:


¿Ves el parecido?

El siguiente paso es muy inteligente: se realiza el producto escalar de cuatro componentes (dp4):

  16: dp4 r1.x, r1.xyzw, r2.xyzw 

( ), — ( ), — ( .w 1.0). . :


masterFilter , . , . — .

: (: 1.0/256.0!) :

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

Ahora, si multiplicamos el filtro por maxAbsDifference ...


Muy simple y eficiente.

Habiendo recibido los contornos, tomamos muestras del mapa de contornos del marco anterior.

Luego, para obtener un efecto "fantasmal", tomamos una parte de los parámetros calculados en el pase actual y los valores del mapa de contorno.

Saluda a nuestro viejo amigo: ruido entero. Él está presente aquí. Los parámetros de animación (cb3 [0] .zw) se toman del búfer constante y cambian con el tiempo.

  float2 outlines = masterFilter * maxAbsDifference; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y 

Nota: si desea implementar el instinto de la bruja usted mismo, le recomiendo limitar el ruido entero al intervalo [-1; 1] (como se dice en su sitio web). No había restricción en el sombreador TW3 original, pero sin él obtuve artefactos terribles y todo el mapa de contorno era inestable.

Luego muestreamos el mapa de contorno de la misma manera que el mapa de brillo anterior (esta vez el texel tiene un tamaño de 1.0 / 512.0), y calculamos el valor promedio del componente .x:

  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0; 

Luego, a juzgar por el código del ensamblador, se calcula la diferencia entre el promedio y el valor de este píxel en particular, después de lo cual se realiza la distorsión por ruido entero:

  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0; 

El siguiente paso es distorsionar el valor del mapa de contornos "antiguo" utilizando ruido: esta es la línea principal que le da a la textura de salida una sensación de bloqueo.

Luego hay otros cálculos, después de los cuales, al final, se calcula la "atenuación".

  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0); 

Aquí hay un breve video que muestra un mapa general en acción:


Si está interesado en el sombreador de píxeles completo, está disponible aquí . Shader es compatible con RenderDoc.

Es interesante (y, para ser sincero, un poco molesto) que, a pesar de la identidad del código del ensamblador con el sombreador original de Witcher 3, ¡el aspecto final del mapa de contorno en RenderDoc está cambiando!

Nota: en la última pasada (ver la siguiente parte), verá que solo se utiliza el canal .r del mapa de contorno. ¿Por qué entonces necesitamos el canal .g? Creo que este es algún tipo de búfer de ping-pong en una textura: tenga en cuenta que .r contiene el canal .g + algún valor nuevo.

Parte 5: The Witcher Flair (Ojo de pez y el resultado final)


Enumeraremos brevemente lo que ya tenemos: en la primera parte, dedicada al instinto del brujo, se genera un mapa de brillo de pantalla completa que indica cuán notable debería ser el efecto dependiendo de la distancia. En la segunda parte, exploré el mapa de contorno con más detalle, que es responsable de los contornos y la animación del efecto final.

Hemos llegado a la última etapa. ¡Todo esto necesita ser combinado! El último pase es un quad de pantalla completa. Entradas: búfer de color, mapa de contorno y mapa de luminancia.

Para:



Después:


Una vez más, mostraré el video con el efecto aplicado:


Como puede ver, además de aplicar contornos a los objetos que Geralt puede ver o escuchar, el efecto ojo de pez se aplica a toda la pantalla, y toda la pantalla (especialmente las esquinas) se vuelve grisácea para transmitir la sensación de un verdadero cazador de monstruos.

Código de sombreador de píxeles ensamblado completo:

  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 líneas, ¡así que tenemos mucho trabajo por hacer!

Primero, eche un vistazo a los datos de entrada:

  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x ); 

El valor principal responsable de la magnitud del efecto es fisheyeAmount . Creo que gradualmente aumenta de 0.0 a 1.0 cuando Geralt comienza a usar su instinto. El resto de los valores no cambian mucho, pero sospecho que algunos de ellos serían diferentes si el usuario hubiera deshabilitado el efecto ojo de pez en las opciones (no marqué esto).

Lo primero que sucede aquí es que el sombreador calcula la máscara responsable de los ángulos grises:

  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, podemos escribir esto de la siguiente manera:

  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); } 

Primero, el intervalo [-1; 1] UV y sus valores absolutos. Luego hay un "apretón" complicado. La máscara terminada es la siguiente:


Volveré a esta máscara más tarde.

Ahora omitiré intencionalmente algunas líneas de código y estudiaré cuidadosamente el código responsable del efecto 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 

Primero, se calculan las coordenadas de textura "duplicadas" y se realiza la resta float2 (1, 1):

  float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0); 

Tal texcoord se puede visualizar de la siguiente manera:


Luego se calcula el punto escalar del producto (uv4, uv4) , lo que nos da la máscara:


que se usa para multiplicar por los texcoords anteriores:


Importante: en la esquina superior izquierda (píxeles negros) los valores son negativos. Se muestran en negro (0.0) debido a la precisión limitada del formato R11G11B10_FLOAT. No tiene un bit de signo, por lo que no se pueden almacenar valores negativos.

Luego se calcula el coeficiente de atenuación (como dije anteriormente, fisheyeAmount varía de 0.0 a 1.0).

  float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation; 

Luego se realizan la restricción (max / min) y una multiplicación.

Por lo tanto, se calcula el desplazamiento. Para calcular el uv final, que se utilizará para muestrear la textura del color, simplemente realizamos la resta:

float2 colorUV = mainUv - offset;

Al muestrear la textura de color UV de color de entrada , obtenemos una imagen distorsionada cerca de las esquinas:


Esquemas


El siguiente paso es muestrear el mapa de contorno para encontrar los contornos. Es bastante simple, primero encontramos texcoords para muestrear los contornos de objetos interesantes, y luego hacemos lo mismo para las pistas:

  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y 


Objetos interesantes del mapa de contorno


Rastros del mapa de contorno

Vale la pena señalar que solo muestreamos el canal .x del mapa de contorno y tenemos en cuenta solo los cuadrados superiores.

Movimiento


Para implementar el movimiento de las pistas, se usa casi el mismo truco que en el efecto de la intoxicación. Se agrega un círculo de tamaño de unidad y se muestrea 8 veces el mapa de contorno para objetos y trazas interesantes, así como la textura de color.

Tenga en cuenta que solo dividimos las rutas encontradas por 8.0.

Como estamos en el espacio de coordenadas de textura [0-1] 2 , la presencia de un círculo de radio 1 para rodear un solo píxel creará artefactos inaceptables:


Por lo tanto, antes de continuar, descubramos cómo se calcula este radio. Para hacer esto, necesitamos volver a las líneas que faltan 15-21. Un problema menor con el cálculo de este radio es que su cálculo está disperso alrededor del sombreador (posiblemente debido a las optimizaciones del sombreador por parte del compilador). Por lo tanto, aquí está la primera parte (15-21) y la segunda (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) 

Como puede ver, solo consideramos los texels de [0.00 - 0.03] al lado de cada superficie, resumimos sus valores, multiplicamos 20 y saturamos. Así es como se ven después de las líneas 15-21:


Y así es cómo después de la línea 41:


En la línea 42, multiplicamos esto por 0.03, este valor es el radio del círculo para toda la pantalla. Como puede ver, más cerca de los bordes de la pantalla, el radio se vuelve más pequeño.

Ahora podemos ver el código ensamblador responsable del movimiento:

  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 

Quedémonos aquí por un minuto. En la línea 40 obtenemos el coeficiente de tiempo, solo elapsedTime * 0.1 . En la línea 43 tenemos un buffer para la textura de color obtenida dentro del bucle.

r0.x (líneas 41-42) es, como sabemos ahora, el radio del círculo. r4.x (línea 44) es el bosquejo de objetos interesantes, r4.y (línea 45) es el bosquejo de pistas (previamente dividido entre 8!), y r4.z (línea 46) es el contador de bucle.

Como es de esperar, el ciclo tiene 8 iteraciones. Comenzamos calculando el ángulo en radianes i * PI_4 , que nos da 2 * PI - un círculo completo. El ángulo se distorsiona con el tiempo.

Usando sincos, determinamos el punto de muestreo (círculo unitario) y cambiamos el radio usando la multiplicación (línea 54).

Después de eso, vamos alrededor del píxel en un círculo y tomamos muestras de los contornos y el color. Después del ciclo, obtenemos los valores promedio (debido a la división entre 8) de los contornos y colores.

  float timeParam = time * 0.1; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; } 

El muestreo de color se realizará de la misma manera, pero agregaremos un desplazamiento multiplicado por un círculo "único" al color base UV .

Brillo


Después del ciclo, tomamos muestras del mapa de brillo y cambiamos los valores de brillo finales (porque el mapa de brillo no sabe nada sobre los contornos):

  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 

Código HLSL:

  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces ); 

Esquinas grises y la unificación final de todo.


El color gris más cercano a las esquinas se calcula utilizando el producto escalar (línea de ensamblaje 69):

  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx; 


Luego siguen dos interpolaciones. El primero combina el gris con el "color en el círculo" usando la primera máscara que describí, por lo que las esquinas se vuelven grises. Además, hay un coeficiente de 0.6, que reduce la saturación de la imagen final:


El segundo combina el primer color con el anterior usando fisheyeAmount . ¡Esto significa que la pantalla se vuelve gradualmente más oscura (debido a la multiplicación por 0.6) y gris en las esquinas! Ingenioso

HLSL:

  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount ); 

Ahora podemos pasar a agregar los contornos de los objetos.

Los colores (rojo y amarillo) se toman del búfer constante.

  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting; 


Fuh! Estamos casi en la línea de meta!

Tenemos el color final, está el color del instinto de la bruja ... ¡queda de alguna manera combinarlos!

Y para esto, la simple adición no es adecuada. Primero calculamos el producto escalar:

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

que se ve así:


Y estos valores al final se usan para interpolar entre el color y el estilo (saturado) de la bruja:

  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; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 ); 


Y eso es todo.

El sombreador completo está disponible aquí .

Comparación de mis sombreadores (izquierdo) y original (derecho):


¡Espero que hayas disfrutado este artículo! Hay muchas ideas brillantes en la mecánica del "instinto brujo", y el resultado final es muy plausible.

[Partes anteriores del análisis: primero y segundo .]

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


All Articles