Recientemente, comencé a tratar con la representación de The Witcher 3. Este juego tiene increíbles técnicas de renderizado. Además, ella es magnífica en términos de trama / música / juego.
En este artículo hablaré sobre las soluciones utilizadas para representar The Witcher 3. No será tan completo como el análisis de los gráficos de
GTA V por Adrian Correger, al menos por ahora.
Comenzaremos con la ingeniería inversa de la corrección de tono.
Parte 1: corrección de tono
En la mayoría de los juegos AAA modernos, uno de los pasos de representación es necesariamente la corrección de tono.
Permítame recordarle que en la vida real hay un rango de brillo bastante amplio, mientras que en las pantallas de las computadoras es muy limitado (8 bits por píxel, lo que nos da 0-255). Aquí es donde el mapa de tonos viene al rescate, lo que le permite ajustar un rango más amplio en un intervalo de iluminación limitado. Por lo general, hay dos fuentes de datos en este proceso: una imagen HDR con un punto flotante, cuyos valores de color exceden 1.0, y la iluminación promedio de la escena (esta última se puede calcular de varias maneras, incluso teniendo en cuenta la adaptación del ojo para simular el comportamiento de los ojos humanos, pero no importa aquí).
El siguiente (y último) paso es obtener la velocidad del obturador, calcular el color con la velocidad del obturador y procesarlo usando la curva de corrección de tono. Y aquí todo se vuelve bastante confuso, porque aparecen nuevos conceptos, como el "punto blanco" (punto blanco) y el "gris medio" (gris medio). Hay al menos unas pocas curvas populares, y algunas de ellas están cubiertas en el
mapeo de tonos de Matt Pettineo.
Honestamente, siempre tuve problemas con la implementación correcta de la corrección de tono en mi propio código. Hay al menos
algunos ejemplos diferentes en línea que me han sido útiles ... hasta cierto punto. Algunos de ellos tienen en cuenta el brillo HDR / punto blanco / gris medio, otros no, por lo tanto, realmente no ayudan. Quería encontrar una implementación "probada en batalla".
Trabajaremos en RenderDoc con la captura de este marco de una de las principales misiones de Novigrad. Todos los ajustes son máximos:
Después de buscar un poco, encontré una llamada de sorteo para la corrección de tono. Como mencioné anteriormente, hay un búfer de colores HDR (textura número 0, resolución completa) y el brillo promedio de la escena (textura número 1, 1x1, coma flotante, calculada anteriormente por el sombreador de cálculo).
Echemos un vistazo al código de ensamblador para el sombreador de píxeles:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret
Hay varios puntos que vale la pena señalar. En primer lugar, el brillo cargado no tiene que ser igual al utilizado, ya que está limitado (llamadas máximas / mínimas) dentro de los valores elegidos por los artistas (del búfer constante). Esto es conveniente porque le permite evitar la velocidad de obturación demasiado alta o baja de la escena. Este movimiento parece bastante común, pero nunca lo he hecho antes. En segundo lugar, alguien que esté familiarizado con las curvas de corrección de tono reconocerá instantáneamente este valor "11.2", porque de hecho este es el valor del punto blanco de la curva de
corrección de tono Uncharted2 de John Hable.
Los parámetros AF se cargan desde cbuffer.
Entonces, tenemos tres parámetros más: cb3_v16.x, cb3_v16.y, cb3_v16.z. Podemos examinar sus significados:
Mi corazonada:
Creo que "x" es una especie de "escala blanca" o gris medio, porque se multiplica por 11.2 (línea 4), y luego se usa como un numerador para calcular la configuración de la velocidad del obturador (línea 10).
"Y" - Lo llamé el "factor del numerador u2", y pronto verá por qué.
"Z" es el "parámetro de exponenciación", porque se usa en el triple log / mul / exp (de hecho, en exponenciación).
¡Pero trate estos nombres variables con cierto grado de escepticismo!
También:
cb3_v4.yz - valores min / max de brillo admisible,
cb3_v7.xyz: parámetros de CA de la curva Uncharted2,
cb3_v8.xyz: parámetros DF de la curva Uncharted2.
Ahora pasemos a la parte difícil: escribiremos un sombreador HLSL que nos dará exactamente el mismo código de ensamblador.
Esto puede ser muy difícil, y cuanto más largo sea el sombreador, más difícil será la tarea. Afortunadamente, hace algún tiempo escribí una herramienta para explorar rápidamente hlsl-> asm.
Damas y caballeros ... ¡Bienvenidos D3DShaderDisassembler!
Después de experimentar con el código, obtuve la
corrección tonal HLSL ya preparada
The Witcher 3 :
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); }
Una captura de pantalla de mi utilidad para confirmar esto:
Voila!
Creo que esta es una implementación bastante precisa de la corrección de tono TW3, al menos en términos de código de ensamblador. ¡Ya lo apliqué en mi marco y funciona muy bien!
Dije "suficiente" porque no
tengo idea de por qué el denominador en ToneMapU2Func se convierte en máximo en cero. Al dividir por 0, ¿debería quedar indefinido?
Esto podría terminarse, pero casi por accidente encontré en este cuadro otra versión del sombreador de tonos TW3, utilizada para una hermosa puesta de sol (¡es interesante que se use con configuraciones de gráficos mínimas!)
Vamos a verlo Primero, el código del ensamblador para el sombreador:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret
Al principio, el código puede parecer intimidante, pero de hecho, no todo es tan malo. Después de un breve análisis, notará que hay dos llamadas a la función Uncharted2 con diferentes conjuntos de datos de entrada (AF, brillo mínimo / máximo ...). Nunca antes había visto una decisión así.
Y HLSL:
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); }
Es decir, de hecho, tenemos dos conjuntos de parámetros de control, calculamos dos colores con corrección de tono y al final los interpolamos. Decisión inteligente!
Parte 2: adaptación ocular
La segunda parte será mucho más simple.
En la primera parte, mostré cómo se realiza la corrección tonal en TW3. Al explicar los antecedentes teóricos, mencioné brevemente la adaptación del ojo. ¿Y sabes que? En esta parte hablaré sobre cómo se realiza esta adaptación del ojo.
Pero espera, ¿qué es la adaptación ocular y por qué la necesitamos?
Wikipedia lo sabe todo al respecto, pero lo explicaré: imagina que estás en una habitación oscura (recuerda que Life is Strange) o en una cueva, y ve afuera donde hay luz. Por ejemplo, la principal fuente de iluminación puede ser el sol.
En la oscuridad, nuestras pupilas se dilatan para que entre más luz a la retina a través de ellas. Cuando se vuelve claro, nuestras pupilas disminuyen y a veces cerramos los ojos porque "duele".
Este cambio no ocurre al instante. El ojo debe adaptarse a los cambios de brillo. Es por eso que realizamos la adaptación del ojo en renderizado en tiempo real.
Un buen ejemplo de cuando se nota una falta de adaptación ocular es el
HDRToneMappingCS11 del DirectX SDK. Los cambios bruscos de brillo medio son bastante desagradables y antinaturales.
¡Empecemos! En aras de la coherencia, analizaremos el mismo marco de Novigrad.
Ahora profundizaremos en el programa de captura de cuadros RenderDoc. La adaptación del ojo generalmente se realiza justo antes de la corrección tonal, y The Witcher 3 no es una excepción.
Veamos el estado del sombreador de píxeles:
Tenemos dos fuentes de entrada: 2 texturas, R32_FLOAT, 1x1 (un píxel). texture0 contiene el brillo promedio de la escena del cuadro anterior. texture1 contiene el brillo promedio de la escena desde el cuadro actual (calculado inmediatamente antes de este sombreador de cálculo; lo marqué en azul).
Se espera que haya una salida: R32_FLOAT, 1x1. Veamos el 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_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret
Wow, que simple! Solo 7 líneas de código ensamblador. ¿Qué está pasando aquí? Explicaré cada línea:
0) Obtenga el brillo promedio del cuadro actual.
1) Obtenga el brillo promedio del cuadro anterior.
2) Realice una comprobación: ¿el brillo actual es menor o igual que el brillo del fotograma anterior?
Si es así, entonces el brillo disminuye, si no, entonces el brillo aumenta.
3) Calcule la diferencia:
diferencia = currentLum - previousLum.4) Esta transferencia condicional (movc) asigna un factor de velocidad desde el búfer constante. Se pueden asignar dos valores diferentes desde la línea 2, dependiendo del resultado de la verificación. Este es un movimiento inteligente, porque de esta manera puede obtener diferentes velocidades de adaptación para reducir y aumentar el brillo. Pero en el marco estudiado, ambos valores son iguales y varían de 0.11 a 0.3.
5) El cálculo final del brillo adaptado:
adaptLuminance = speedFactor * diferencia + anteriorLuminance.6) El final del sombreador
Esto se implementa en HLSL simplemente:
// The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; }
Estas líneas nos dan el mismo código de ensamblador. Solo sugeriría reemplazar el tipo de salida con
float4 con
float . No hay necesidad de desperdicio de ancho de banda. Así es como Witcher 3 implementa la adaptación ocular. Bastante simple, ¿verdad?
PS. Muchas gracias a Baldur Karlsson (Twitter:
@baldurk ) por RenderDoc. El programa es simplemente genial.
Parte 3: aberración cromática
La aberración cromática es un efecto que se encuentra principalmente en lentes baratos. Ocurre porque las lentes tienen diferentes índices de refracción para diferentes longitudes de luz visible. Como resultado, aparece una distorsión visible. Sin embargo, no a todos les gusta. Afortunadamente, en Witcher 3 este efecto es muy sutil y, por lo tanto, no es molesto en el juego (al menos para mí). Pero puede desactivarlo si lo desea.
Echemos un vistazo de cerca a un ejemplo de una escena con aberración cromática y sin ella:
Aberración cromática incluidaAberración cromática deshabilitada¿Notas alguna diferencia cerca de los bordes? Yo tampoco Probemos otra escena:
La aberración cromática está incluida. Observe la ligera distorsión "roja" en el área indicada.Si, mucho mejor! Aquí el contraste entre las áreas oscuras y claras es más fuerte, y en la esquina vemos una ligera distorsión. Como puede ver, este efecto es muy débil. Sin embargo, me preguntaba cómo se implementa. Pasemos a la parte más curiosa: ¡el código!
ImplementaciónLo primero que debe hacer es encontrar la llamada de sorteo correcta con un sombreador de píxeles. De hecho, la aberración cromática es parte del gran sombreador de píxeles de "procesamiento posterior", que consiste en la aberración cromática, viñeteado y corrección gamma. Todo esto está dentro de un solo sombreador de píxeles. Echemos un vistazo más de cerca al código de ensamblador para el sombreador de píxeles:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ...
Y a los valores de cbuffer:
Entonces, tratemos de entender lo que está sucediendo aquí. De hecho, cb3_v17.xy es el centro de la aberración cromática, por lo que las primeras líneas calculan el vector 2d a partir de las coordenadas texel (cb3_v17.zw = el recíproco del tamaño de la ventana gráfica) al "centro de aberración cromática" y su longitud, luego realiza otros cálculos, verificación y ramificación . Al aplicar la aberración cromática, calculamos los desplazamientos utilizando ciertos valores del búfer constante y distorsionamos los canales R y G. En general, cuanto más cerca de los bordes de la pantalla, más fuerte es el efecto. La línea 10 es bastante interesante porque hace que los píxeles "se acerquen", especialmente cuando exageramos la aberración. Con mucho gusto compartiré con ustedes mi comprensión del efecto. Como de costumbre, tome nombres de variables con una parte (sólida) de escepticismo. Y tenga en cuenta que el efecto se aplica
antes de la corrección gamma.
void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } }
Agregué "fChromaticAberrationIntensity" para aumentar el tamaño del desplazamiento y, por lo tanto, la fuerza del efecto, como su nombre indica (TW3 = 1.0). Intensidad = 40:
Eso es todo! Espero que hayas disfrutado esta parte.
Parte 4: viñeteado
El viñeteado es uno de los efectos de procesamiento posterior más comunes utilizados en los juegos. También es popular en fotografía. Las esquinas ligeramente sombreadas pueden crear un hermoso efecto. Hay varios tipos de viñetas. Por ejemplo,
Unreal Engine 4 usa natural. Pero volvamos a The Witcher 3.
Haga clic aquí para ver una comparación interactiva de marcos con y sin viñetas. La comparación se toma de
la guía de rendimiento de NVIDIA para The Witcher 3 .
Captura de pantalla de "The Witcher 3" con viñetas activadas.Tenga en cuenta que la esquina superior izquierda (cielo) no está tan sombreada como las otras partes de la imagen. Más tarde volveremos a esto.
Detalles de implementaciónEn primer lugar, hay una ligera diferencia entre las viñetas utilizadas en la versión original de The Witcher 3 (que se lanzó el 19 de mayo de 2015) y en The Witcher 3: Blood and Wine. En el primero, el "gradiente inverso" se calcula dentro del sombreador de píxeles, y en el segundo se calcula previamente en una textura 2D de 256x256:
Textura 256x256, utilizada como "gradiente inverso" en el complemento "Sangre y vino".
Usaré el sombreador de "Blood and Wine" (un gran juego, por cierto). Como en la mayoría de los otros juegos, la viñeta de Witcher 3 se calcula en el sombreador de píxeles del posprocesamiento final. Echa un vistazo al código del ensamblador:
... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ...
Interesante! Parece que se usan tanto gamma (línea 46) como espacios lineales (línea 51) para calcular el viñeteado. En la línea 48, tomamos muestras de la textura del "gradiente inverso". cb3 [9] .xyz no está relacionado con las viñetas. En cada cuadro marcado, se le asigna el valor float3 (1.0, 1.0, 1.0), es decir, es probable que sea el filtro final utilizado en los efectos de aparición / desaparición gradual. Hay tres parámetros principales para viñetar en TW3:
- Opacidad (cb3 [6] .w): afecta la fuerza de las viñetas. 0: sin viñeteado, 1: viñeteado máximo. Según mis observaciones, en la base The Witcher 3 es aproximadamente 1.0, mientras que en Blood and Wine fluctúa alrededor de 0.15.
- Color (cb3 [7] .xyz): una característica excelente de las viñetas TW3 es la capacidad de cambiar su color. No tiene que ser negro, pero en la práctica ... Por lo general, tiene los valores float3 (3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0) y así sucesivamente; en el caso general, estos son múltiplos de 0.00392156 = 1.0 / 255.0
- Los pesos (cb3 [6] .xyz) es un parámetro muy interesante. Siempre vi viñetas "planas", por ejemplo:
Máscara de viñeta típicaPero usando pesos (línea 52), puede obtener resultados muy interesantes:
Máscara de viñeteado TW3 calculada usando pesosLos pesos están cerca de 1.0. Observe los datos constantes del búfer para un cuadro de Blood and Wine (un mundo mágico con un arco iris): esta es la razón por la que el viñeteado no afectó a los píxeles brillantes del cielo de arriba.
CódigoAquí está mi implementación de viñetas TW3 en HLSL.
GammaToLinear = pow (color, 2.2)
float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; }
Espero que lo hayas disfrutado. También puede probar mi HLSLexplorer , que me ayudó mucho a comprender el código del ensamblador HLSL.Como antes, tome los nombres de las variables con cierto grado de escepticismo: los sombreadores TW3 son procesados por D3DStripShader, por lo que, de hecho, no sé casi nada sobre ellos, solo puedo adivinar. Además, no tengo ninguna responsabilidad por el daño causado a su equipo por este sombreador;)Bonificación: cálculo del gradienteEn The Witcher 3, lanzado en 2015, el gradiente inverso se calculó en el sombreador de píxeles, en lugar de muestrear una textura calculada previamente. Echa un vistazo al código del ensamblador: 35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000)
Afortunadamente para nosotros, es bastante simple. En HLSL, se verá más o menos así: float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; }
Es decir, simplemente calculamos la distancia desde el centro al textil, hacemos algo de magia con él (multiplicación, saturación ...) y luego ... ¡calculamos el polinomio! ImpresionanteParte 5: el efecto de la intoxicación
Veamos cómo el juego "The Witcher 3: Wild Hunt" implementa el efecto de la intoxicación. Si aún no lo ha jugado, suelte todo, compre y juegue, mire un video:Tarde:Noche:Primero, vemos una imagen doble y giratoria, que a menudo surge cuando bebes en la vida real. Cuanto más lejos esté un píxel del centro de la imagen, más fuerte será el efecto de rotación. Publiqué intencionalmente el segundo video con la noche, porque puedes ver claramente esta rotación en las estrellas (¿ves 8 puntos separados?)La segunda parte del efecto de la intoxicación, que puede no notarse de inmediato, es un ligero cambio en el zoom. Se nota cerca del centro.Probablemente sea obvio que este efecto es un postprocesamiento típico (sombreador de píxeles). Sin embargo, su ubicación en la canalización de renderizado puede no ser tan obvia. Resulta que el efecto de la intoxicación se aplica inmediatamente después de la corrección tonal y justo antes del desenfoque de movimiento (la imagen "borracha" es la entrada para el desenfoque de movimiento).Comencemos los juegos con el código ensamblador: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
Aquí se usan dos tampones constantes separados. Veamos sus valores:Estamos interesados en algunos de ellos:cb0_v0.x -> tiempo transcurrido (en segundos)cb0_v1.xyzw - tamaño de ventana y el recíproco del tamaño de ventana (también conocido como "tamaño de píxel")cb3_v0.x - rotación alrededor de un píxel, siempre tiene un valor de 1.0.cb3_v0.y: la magnitud del efecto de la intoxicación. Después de encenderlo, no funciona con toda su fuerza, pero aumenta gradualmente de 0.0 a 1.0.cv3_v1.xy - compensaciones de píxeles (más sobre esto a continuación). Este es un par sin / cos, por lo que puede usar sincos (tiempo) en el sombreador si lo desea.cb3_v2.xy es el centro del efecto, generalmente float2 (0.5, 0.5).Aquí queremos centrarnos en comprender lo que está sucediendo, y no solo reescribir ciegamente el sombreador.Comenzaremos con las primeras líneas: ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w
Llamo a la línea 0 "relación de zoom" y pronto verá por qué. Inmediatamente después (línea 1), calculamos el "desplazamiento de rotación". Esto es solo un par de datos de entrada sin / cos multiplicado por 0.05.Líneas 2-4: Primero, calculamos el vector desde el centro del efecto hasta las coordenadas UV de la textura. Luego calculamos el cuadrado de la distancia (3) y la distancia simple (4) (desde el centro hasta el texel)Zoom coordenadas de textura
Veamos el siguiente código de ensamblador: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy
Como están empaquetados de esta manera, solo podemos analizar un par de flotadores.Para empezar, r0.yz son "desplazamientos de rotación", r1.z es la distancia desde el centro al texel, r1.xy es el vector desde el centro al texel, r0.x es el "factor de zoom".Para entender esto, supongamos por ahora que zoomFactor = 1.0, es decir, puede escribir lo siguiente: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center;
Pero zoomFactor = 1.0: r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter;
Del mismo modo para r3.xy: 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center
Pero zoomFactor = 1.0: Genial. Es decir, en este momento tenemos esencialmente el actual TextureUV (texel) ± desplazamiento de rotación, pero ¿qué pasa con zoomFactor? Mire la línea 0. De hecho, zoomFactor = 1.0 - 0.1 * drunkAmount. Para la cantidad máxima de borracho, el valor de zoomFactor debe ser 0.9, y las coordenadas de textura con zoom ahora se calculan de la siguiente manera:r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter
baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter
Quizás tal explicación sería más intuitiva: es simplemente una interpolación lineal por algún factor entre las coordenadas de textura normalizadas y el centro. Esta es una imagen de "acercamiento". Para entender esto, es mejor experimentar con los valores. Aquí hay un enlace a Shadertoy, donde puedes ver el efecto en acción.Desplazamiento de textura
Todo el fragmento en código ensamblador: 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset
crea un cierto gradiente, llamémoslo la "máscara de intensidad de desplazamiento". De hecho, le da dos significados. El primero está en r0.w (lo usaremos más tarde) y el segundo es 5 veces más fuerte en r0.x (línea 15). Este último realmente sirve como un factor para el tamaño del texel, por lo que afecta la fuerza de sesgo.Muestreo relacionado con la rotaciónA continuación, se realiza una serie de muestreo de textura. De hecho, se utilizan 2 series de 8 muestras, una para cada "lado". En HLSL, puede escribir esto de la siguiente manera: static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB;
El truco es que agregamos a baseTexcoordsA / B un desplazamiento adicional en el círculo unitario, multiplicado por la "intensidad del cambio de coordenadas de textura" mencionada anteriormente. Cuanto más lejos del centro esté el píxel, mayor será el radio del círculo alrededor del píxel: lo muestreamos 8 veces, lo que es claramente visible en las estrellas. Valores PointsAroundPixel (múltiplos de 45 grados):Círculo únicoMuestreo relacionado con zoomLa segunda parte del efecto de embriaguez en The Witcher 3 es hacer zoom con acercar y alejar. Veamos el código del ensamblador que realiza esta tarea: 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw
Vemos que hay tres llamadas de textura separadas, es decir, tres coordenadas de textura diferentes. Analicemos cómo se calculan las coordenadas de textura a partir de ellos. Pero primero, mostramos la entrada para esta parte: float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60
Algunas palabras sobre la entrada. Calculamos el desplazamiento en texels (por ejemplo, 8.0 * texel size), que luego se agrega a las coordenadas uv base. La amplitud simplemente oscila entre 0,98 y 1,02 para dar una sensación de zoom, al igual que zoomFactor en la parte que realiza la rotación.Comencemos con el primer par - r1.xy (línea 61) r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude);
Eso es: float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude)
Vamos a ver el segundo par - r3.xy (línea 62) r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV
Eso es: float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates;
Vamos a ver el tercer par - r0.xy (líneas 63-64) r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV
Eso es: float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates
Las tres consultas de textura se suman y el resultado se almacena en el registro r1. Vale la pena señalar que este sombreador de píxeles utiliza un muestreador de direccionamiento limitado.Poniendo todo juntoEntonces, en este momento tenemos el resultado de la rotación en el registro r2 y tres solicitudes de zoom plegadas en el registro r1. Veamos las últimas líneas de código ensamblador: 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
Acerca de la entrada adicional: r0.w se toma de la línea 7, esta es nuestra máscara de intensidad, y cb3 [0] .y es la magnitud del efecto de intoxicación.Veamos como funciona. Mi primer acercamiento fue la fuerza bruta: float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor;
Pero qué demonios, nadie escribe sombreadores así . Tomé un lápiz con papel y escribí esta fórmula: finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart
Donde t = effectAmount * intensidadMaskEntonces, obtenemos: finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t )
Y llegamos a lo siguiente: finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity);
Sí, esta parte del artículo resultó ser muy detallada, ¡pero finalmente terminamos! Personalmente, aprendí algo en el proceso de escritura, ¡espero que tú también lo hagas!Si está interesado, las fuentes completas de HLSL están disponibles aquí . Los probé con mi HLSLexplorer , y aunque no hay correspondencias directas uno a uno con el sombreador original, las diferencias son tan pequeñas (una línea menos) que puedo decir con seguridad que funciona. Gracias por leer!