La ingeniería inversa de la representación de The Witcher 3

La primera parte de la traducción está aquí . En esta parte, hablaremos sobre el efecto de la nitidez, el brillo promedio, las fases de la luna y los fenómenos atmosféricos durante la lluvia.

Parte 6. Afilar


En esta parte, veremos m√°s de cerca otro efecto de procesamiento posterior de The Witcher 3 - Sharpen.

La nitidez hace que la imagen de salida sea un poco más nítida. Este efecto es conocido por Photoshop y otros editores gráficos.

En The Witcher 3, el enfoque tiene dos opciones: baja y alta. A continuación, hablaré sobre la diferencia entre ellos, pero por ahora, echemos un vistazo a las capturas de pantalla:

imagen

Opción "baja" - hasta

imagen

Opción "baja" - después


Alta opción - hasta


Opción "Alta" - después

Si desea ver comparaciones más detalladas (interactivas), consulte la sección en la Guía de rendimiento The Witcher 3 de Nvidia . Como puede ver, el efecto es especialmente notable en la hierba y el follaje.

En esta parte de la publicaci√≥n, estudiaremos el marco desde el comienzo del juego: lo eleg√≠ intencionalmente, porque aqu√≠ vemos el relieve (larga distancia de dibujo) y la c√ļpula del cielo.


En t√©rminos de entrada, el enfoque requiere un b√ļfer de color t0 (LDR despu√©s de la correcci√≥n de tono y destellos de lente) y un b√ļfer de profundidad t1 .

Examinemos el código del ensamblador para el sombreador de píxeles:

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


50 líneas de código de ensamblador parecen una tarea factible. Vamos a resolverlo.

Agudizar la generación de valor


El primer paso es cargar el b√ļfer de profundidad (l√≠nea 1). Vale la pena se√Īalar que "The Witcher 3" utiliza una profundidad invertida (1.0 - cerca, 0.0 - lejos). Como ya sabr√°, la profundidad del hardware est√° vinculada de forma no lineal (consulte este art√≠culo para m√°s detalles ).

Las l√≠neas 3-6 proporcionan una forma muy interesante de asociar esta profundidad de hardware [1.0 - 0.0] con valores [casi lejanos] (los establecemos en la etapa MatrixPerspectiveFov). Considere los valores del b√ļfer constante:


Teniendo el valor "close" de 0.2 y el valor de "far" 5000, podemos calcular los valores de cb12_v21.xy de la siguiente manera:

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


Este c√≥digo es bastante com√ļn en los sombreadores TW3, as√≠ que creo que es solo una funci√≥n.

Después de obtener la "profundidad de la pirámide de visibilidad", la línea 7 usa la escala / distorsión para crear el coeficiente de interpolación (aquí usamos saturar para limitar los valores al intervalo [0-1]).


cb3_v1.xy y cb3_v2.xy: este es el brillo del efecto de nitidez a distancias cortas y largas. Llam√©moslos sharpenNear y sharpenFar. Y esta es la √ļnica diferencia entre las opciones "Bajo" y "Alto" de este efecto en The Witcher 3.

Ahora es el momento de usar la relaci√≥n resultante. Las l√≠neas 8-9 solo hacen lerp(sharpenNear, sharpenFar, interpolationCoeff) . ¬ŅPara qu√© es esto? Gracias a esto, obtenemos un brillo diferente cerca de Geralt y lejos de √©l. Echa un vistazo:


Quiz√°s esto apenas se nota, pero aqu√≠ interpolamos en funci√≥n de la distancia entre el brillo de nitidez junto al reproductor (2.177151) y el brillo del efecto est√° muy lejos (1.91303). Despu√©s de este c√°lculo, agregamos 1.0 al brillo (l√≠nea 10). ¬ŅPor qu√© se necesita esto? Supongamos que la operaci√≥n lerp que se muestra arriba nos dio 0.0. Despu√©s de agregar 1.0, naturalmente obtenemos 1.0, y este es un valor que no afectar√° el p√≠xel cuando se enfoca. Lea m√°s sobre esto a continuaci√≥n.

Mientras se afila, no queremos afectar el cielo. Esto se puede lograr agregando una simple verificación condicional:

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


En The Witcher 3, el valor de la profundidad de píxel del cielo es 1.0, por lo que lo usamos para obtener una especie de "filtro binario" (un hecho interesante: en este caso, el paso no funcionará correctamente).

Ahora podemos multiplicar el brillo interpolado por un "filtro de cielo":


Esta multiplicación se realiza en la línea 13.

Ejemplo de código de sombreador:

// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;


Centro de muestreo de píxeles


SV_Position tiene un aspecto que será importante aquí: un desplazamiento de medio píxel . Resulta que este píxel en la esquina superior izquierda (0, 0) tiene coordenadas no (0, 0) en términos de SV_Position.xy, sino (0.5, 0.5). Wow!

Aquí queremos tomar una muestra en el centro del píxel, así que veamos las líneas 14-16. Puedes escribirlos en HLSL:

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

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


Y luego, tomamos muestras de la textura de color de entrada de texcoords "uvCenter". No se preocupe, el resultado del muestreo será el mismo que con el método "normal" (SV_Position.xy / ViewportSize.xy).

Afilar o no afilar


La decisión sobre si usar sharpen depende de fSharpenAmount.

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

//
float3 finalColor = colorCenter;

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

return float4( finalColor, 1 );


Afilar


Es hora de echar un vistazo al interior del algoritmo mismo.

B√°sicamente, realiza las siguientes acciones:

- muestra cuatro veces la textura de color de entrada en las esquinas del píxel,

- agrega muestras y calcula el valor promedio,

- calcula la diferencia entre "centro" y "esquina promedio",

- encuentra el componente absoluto m√°ximo de la diferencia,

- corrige max. abs. componente usando escala + valores de sesgo,

- determina la magnitud del efecto usando max. abs. componente

- calcula el valor de brillo (luma) para "centerColor" y "averageColor",

- divide colorCenter en su luma,

- calcula un nuevo valor de luma interpolado basado en la magnitud del efecto,

- Multiplica colorCenter por el nuevo valor de luma.

Mucho trabajo, y fue difícil para mí resolverlo, porque nunca había experimentado con filtros afilados.

Comencemos con el patrón de muestreo. Como puede ver en el código del ensamblador, se realizan cuatro lecturas de textura.

Esto se mostrará mejor utilizando un ejemplo de una imagen de píxeles (el nivel de habilidad del artista es un experto ):


Todas las lecturas en el sombreador usan muestreo bilineal (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).

El desplazamiento desde el centro a cada uno de los √°ngulos es (¬Ī 0.5, ¬Ī 0.5), dependiendo del √°ngulo.

¬ŅVes c√≥mo se puede implementar esto en HLSL? A ver:

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

float3 colorCorners = 0;

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

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

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

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


Entonces, ahora las cuatro muestras se resumen en la variable "colorCorners". Sigamos estos pasos:

//
float3 averageColorCorners = colorCorners / 4.0;

//
float3 diffColor = colorCenter - averageColorCorners;

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

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

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

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

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

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

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

return float4(finalColor, 1.0);


El reconocimiento de bordes se realiza calculando max. abs. componente de diferencia. Movimiento inteligente! Echa un vistazo a su visualización:


Visualización del componente absoluto máximo de la diferencia.

Genial El sombreador HLSL terminado está disponible aquí . Perdón por el formato bastante pobre. Puede usar mi programa HLSLexplorer y experimentar con el código.

¡Puedo decir felizmente que el código anterior crea el mismo código de ensamblador que en el juego!

Para resumir: el sombreador de nitidez Witcher 3 est√° muy bien escrito (tenga en cuenta que fPixelSharpenAmount es mayor que 1.0! Esto es interesante ...). Adem√°s, la forma principal de cambiar el brillo del efecto es el brillo de los objetos cercanos / lejanos. En este juego, no son constantes; He compilado varios ejemplos de valores:

Skellige:

agudizarsharpenFarsharpenDistanceScalesharpenDistanceBiassharpenLumScalesharpenLumBias
bajo
alto2,01,80,025
-0.25
-13.33333
1.33333

Kaer Morhen:

agudizar
sharpenFar
sharpenDistanceScale
sharpenDistanceBias
sharpenLumScale
sharpenLumBias
bajo
0.57751
0.31303
0,06665
-0,33256
-1.0
2,0
alto
2.17751
1.91303
0,06665
-0,33256
-1.0
2,0

Parte 7. Brillo promedio


La operaci√≥n de calcular el brillo promedio del cuadro actual se puede encontrar en casi cualquier videojuego moderno. Este valor a menudo se usa m√°s tarde para el efecto de la adaptaci√≥n ocular y la correcci√≥n tonal (ver en la parte anterior de la publicaci√≥n). En soluciones simples, el c√°lculo del brillo se usa para, por ejemplo, la textura 512 2 , luego el c√°lculo de sus niveles de mip y la aplicaci√≥n de este √ļltimo. Esto generalmente funciona, pero limita enormemente las posibilidades. Las soluciones m√°s complejas utilizan sombreadores computacionales que realizan, por ejemplo, reducci√≥n paralela .

Veamos c√≥mo el equipo de CD Projekt Red resolvi√≥ este problema en The Witcher 3. En la parte anterior, ya examin√© la correcci√≥n tonal y la adaptaci√≥n del ojo, por lo que la √ļnica pieza restante del rompecabezas era el brillo promedio.

Para empezar, el cálculo del brillo promedio de The Witcher 3 consta de dos pases. Para mayor claridad, decidí dividirlos en partes separadas, y primero miramos el primer paso: "distribución de brillo" (cálculo del histograma de brillo).

Distribución de brillo


Estos dos pases son bastante fáciles de encontrar en cualquier analizador de cuadros. Estas son las llamadas de Despacho en orden justo antes de realizar la adaptación ocular:


Veamos la entrada para este pase. Necesita dos texturas:

1) b√ļfer de color HDR, cuya escala se reduce a 1/4 x 1/4 (por ejemplo, de 1920x1080 a 480x270),

2) buffer de profundidad de pantalla completa


Tamp√≥n de color 1/4 x 1/4 HDR. Tenga en cuenta el truco complicado: este b√ļfer es parte de un b√ļfer m√°s grande. Reutilizar los tampones es una buena pr√°ctica.


Buffer de profundidad de pantalla completa

¬ŅPor qu√© alejar el b√ļfer de color? Creo que se trata de rendimiento.

En cuanto a la salida de este pase, es un b√ļfer estructurado. 256 elementos de 4 bytes cada uno.

Los sombreadores no tienen informaci√≥n de depuraci√≥n aqu√≠, as√≠ que supongamos que es solo un b√ļfer de valores int sin signo.

Importante: el primer paso para calcular el brillo promedio llama a ClearUnorderedAccessViewUint para restablecer todos los elementos del b√ļfer estructurado a cero.

Estudiemos el código de ensamblador del sombreador computacional (¡este es el primer sombreador computacional en todo nuestro análisis!)

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


Y un b√ļfer constante:


Ya sabemos que la primera entrada es un b√ļfer de color HDR. Con FullHD, su resoluci√≥n es 480x270. Miremos la llamada de Despacho.

Despacho (270, 1, 1): esto significa que ejecutamos 270 grupos de subprocesos. En pocas palabras, ejecutamos un grupo de hilos por l√≠nea del b√ļfer de color.


Cada grupo de subprocesos ejecuta una l√≠nea de b√ļfer de color HDR

Ahora que tenemos este contexto, intentemos averiguar qué hace el sombreador.

Cada grupo de hilos tiene 64 hilos en la dirección X (dcl_thread_group 64, 1, 1), así como memoria compartida, 256 elementos con 4 bytes cada uno (dcl_tgsm_structured g0, 4, 256).

Observe que en el sombreador usamos SV_GroupThreadID (vThreadIDInGroup.x) [0-63] y SV_GroupID (vThreadGroupID.x) [0-269].

1) Comenzamos asignando a todos los elementos de la memoria compartida valores cero. Dado que la memoria total contiene 256 elementos y 64 hilos por grupo, esto se puede hacer convenientemente con un simple ciclo:

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


2) Después de eso, establecemos la barrera usando GroupMemoryBarrierWithGroupSync (sync_g_t). Hacemos esto para garantizar que todos los subprocesos en la memoria compartida de los grupos se restablezcan a cero antes de continuar con el siguiente paso.

3) Ahora estamos ejecutando un bucle, que puede escribirse más o menos así:

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


Este es un ciclo for simple con un incremento de 64 (¬Ņya entendiste por qu√©?).

El siguiente paso es calcular la posición del píxel cargado.

Pensemos en ello.

Para la coordenada Y, podemos usar SV_GroupID.x porque lanzamos 270 grupos de subprocesos.

Para la coordenada X, podemos ... ¬°aprovechar el flujo de grupo actual! Intentemos hacerlo.

Como hay 64 subprocesos en cada grupo, dicha solución omitirá todos los píxeles.

Considere el grupo de hilos (0, 0, 0).

- La transmisión (0, 0, 0) procesará píxeles (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0), (384, 0), (448,0).

- El hilo (1, 0, 0) procesará píxeles (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0), (449, 0) ...

- La transmisión (63, 0, 0) procesará los píxeles (63, 0), (127, 0), (191, 0), (255, 0), (319, 0), (383, 0), (447, 0)

Por lo tanto, todos los píxeles serán procesados.

Tambi√©n debemos asegurarnos de no cargar p√≠xeles desde fuera del b√ļfer de color:

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


¬ŅVes? ¬°Es bastante simple!

También calculé el brillo (línea 21 del código del ensamblador).

Genial, ya hemos calculado el brillo a partir de un píxel de color. El siguiente paso es cargar (¡no muestra!) El valor de profundidad correspondiente.

Pero aqu√≠ tenemos un problema, porque conectamos el b√ļfer de profundidades de resoluci√≥n completa. ¬ŅQu√© hacer al respecto?

Esto es sorprendentemente simple: simplemente multiplique colorPos por alguna constante (cb0_v2.z). Redujimos el b√ļfer de color HDR cuatro veces. por lo tanto, el valor es 4!

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


Hasta ahora todo bien! Pero ... llegamos a las líneas 24-25 ...

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


Entonces Primero tenemos una comparaci√≥n de la igualdad de coma flotante, su resultado est√° escrito en r2.x, y justo despu√©s de eso va ... ¬Ņqu√©? Bitwise Y ?? Enserio? ¬ŅPara un valor de coma flotante? ¬ŅQu√© demonios?

El problema 'eq + y'

Perm√≠tanme decir que para m√≠ fue la parte m√°s dif√≠cil del sombreador. Incluso prob√© combinaciones extra√Īas de asint / asfloat ...

¬ŅY si usas un enfoque ligeramente diferente? Hagamos la comparaci√≥n habitual de flotante a flotante en HLSL.

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


Y aquí está la salida en código ensamblador:

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


Interesante, ¬Ņverdad? No esperaba ver "y" aqu√≠.

0x3f800000 es solo 1.0f ... Es lógico porque obtenemos 1.0 y 0.0 de lo contrario si la comparación tiene éxito.

Pero, ¬Ņqu√© pasa si "reemplazamos" 1.0 con alg√ļn otro valor? Por ejemplo, as√≠:

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


Obtenemos el siguiente resultado:

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


Ja! Funcionó. Esto es solo la magia del compilador HLSL. Nota: si reemplazas 0.0 con otra cosa, entonces solo obtienes movc.

Volvamos al sombreador computacional. El siguiente paso es verificar que la profundidad sea igual a cb0_v2.w. Siempre es igual a 0.0; en otras palabras, verificamos si un p√≠xel est√° en un plano lejano (en el cielo). Si es as√≠, le asignamos a este coeficiente alg√ļn valor, aproximadamente 0.5 (verifiqu√© en varios cuadros).

Este coeficiente calculado se usa para interpolar entre el brillo del color y el brillo del "cielo" (valor cb0_v2.x, que a menudo es aproximadamente igual a 0.0). Supongo que esto es necesario para controlar la importancia del cielo en el c√°lculo del brillo promedio. Por lo general, la importancia se reduce. Muy buena idea.

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

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


Como tenemos lumaOk, el siguiente paso es calcular su logaritmo natural para crear una buena distribución. Pero espera, digamos que lumaOk es 0.0. Sabemos que el valor de log (0) no está definido, por lo que agregamos 1.0 porque log (1) = 0.0.

Después de eso, escalamos el logaritmo calculado a 128 para distribuirlo en 256 celdas. Muy inteligente!

Y es a partir de aquí que se toma este valor 88.722839. Este es un 128 * (2) .

Esta es solo la forma en que HLSL calcula los logaritmos.

Solo hay una función en el código del ensamblador HLSL que calcula logaritmos: log , y tiene una base de 2.

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

// 128
lumaOk *= 128;


Finalmente, calculamos el índice de la celda a partir del brillo logarítmicamente distribuido y agregamos 1 a la celda correspondiente en la memoria compartida.

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

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


El siguiente paso nuevamente será establecer una barrera para garantizar que se hayan procesado todos los píxeles de la fila.

Y el √ļltimo paso es agregar valores de la memoria compartida al b√ļfer estructurado. Esto se hace de la misma manera, a trav√©s de un bucle simple:

// ,
GroupMemoryBarrierWithGroupSync();

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

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


Despu√©s de que los 64 subprocesos del grupo de subprocesos completen los datos comunes, cada subproceso agrega 4 valores al b√ļfer de salida.

Considere el b√ļfer de salida. Pensemos en ello. ¬°La suma de todos los valores en el b√ļfer es igual al n√ļmero total de p√≠xeles! (a 480x270 = 129,600). Es decir, sabemos cu√°ntos p√≠xeles tienen un valor de brillo espec√≠fico.

Si no conoce bien los sombreadores computacionales (como yo), al principio puede que no sea claro, así que lea la publicación unas cuantas veces más, tome papel y lápiz e intente comprender los conceptos sobre los que se basa esta técnica.

Eso es todo! Así es como The Witcher 3 calcula un histograma de brillo. Personalmente, aprendí mucho al escribir esta parte. ¡Felicitaciones a los chicos de CD Projekt Red por su excelente trabajo!

Si está interesado en un sombreador HLSL completo, está disponible aquí . ¡Siempre me esfuerzo por obtener el código de ensamblaje lo más cerca posible del juego y estoy completamente feliz de haber tenido éxito nuevamente!

C√°lculo de brillo promedio


Esta es la segunda parte del an√°lisis de los c√°lculos de brillo medio en "The Witcher 3: Wild Hunt".

Antes de entrar en batalla con otro sombreador computacional, repitamos brevemente lo que sucedi√≥ en la √ļltima parte: trabajamos con un b√ļfer de color HDR con una escala de 1 / 4x1 / 4. Despu√©s de la primera pasada, obtuvimos un histograma de brillo (b√ļfer estructurado de 256 valores enteros sin signo). Calculamos el logaritmo para el brillo de cada p√≠xel, lo distribuimos en 256 celdas y aumentamos el valor correspondiente del b√ļfer estructurado en 1 por p√≠xel. Debido a esto, la suma total de todos los valores en estas 256 celdas es igual al n√ļmero de p√≠xeles.


Un ejemplo de la salida de la primera pasada. Hay 256 elementos.

Por ejemplo, nuestro b√ļfer de pantalla completa tiene un tama√Īo de 1920x1080. Despu√©s de alejar, la primera pasada us√≥ un b√ļfer de 480x270. La suma de todos los 256 valores en el b√ļfer ser√° igual a 480 * 270 = 129 600.

Después de esta breve introducción, estamos listos para pasar al siguiente paso: la informática.

Esta vez solo se usa un grupo de subprocesos (Despacho (1, 1, 1)).

Veamos el código ensamblador del sombreador computacional:

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


Hay un b√ļfer constante:


Echemos un vistazo r√°pido al c√≥digo del ensamblador: se adjuntan dos UAV (u0: b√ļfer de entrada de la primera parte y u1: textura de salida del formato 1x1 R32_FLOAT). Tambi√©n vemos que hay 64 subprocesos por grupo y 256 elementos de memoria de grupo compartida de 4 bytes.

Comenzamos llenando la memoria compartida con datos del b√ļfer de entrada. Tenemos 64 hilos, por lo que tendr√° que hacer casi lo mismo que antes.

Para estar absolutamente seguros de que todos los datos se han cargado para su posterior procesamiento, después de eso, pusimos una barrera.

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


Todos los c√°lculos se realizan en un solo subproceso, todos los dem√°s se utilizan simplemente para cargar valores desde el b√ļfer en la memoria compartida.

La secuencia "inform√°tica" tiene un √≠ndice de 0. ¬ŅPor qu√©? Te√≥ricamente, podemos usar cualquier secuencia del intervalo [0-63], pero gracias a una comparaci√≥n con 0, podemos evitar comparaciones enteras-enteras adicionales (es decir, instrucciones q ).

El algoritmo se basa en la indicación del intervalo de píxeles que se tendrá en cuenta en la operación.

En la l√≠nea 11, multiplicamos ancho * alto para obtener el n√ļmero total de p√≠xeles y los multiplicamos por dos n√ļmeros del intervalo [0.0f-1.0f], indicando el comienzo y el final del intervalo. Se utilizan restricciones adicionales para garantizar que 0 <= Start <= End <= totalPixels - 1 :

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

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

int pixelsMinusOne = totalPixels - 1;

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


Como puede ver, hay dos ciclos a continuaci√≥n. El problema con ellos (o con su c√≥digo de ensamblador) es que hay transiciones condicionales extra√Īas en los extremos de los bucles. Fue muy dif√≠cil para m√≠ recrearlos. Tambi√©n eche un vistazo a la l√≠nea 21. ¬ŅPor qu√© hay "-1"? Lo explicar√© un poco m√°s abajo.

La tarea del primer ciclo es descartar pixelsToConsiderStart y darnos el √≠ndice de la celda del b√ļfer en la que pixelsToConsiderStart +1 est√° presente (as√≠ como el n√ļmero de todos los p√≠xeles en las celdas anteriores).

Digamos que pixelsToConsiderStart es aproximadamente igual a 30,000, y en el b√ļfer hay 37,000 p√≠xeles en la celda "cero" (esto sucede en el juego por la noche). Por lo tanto, queremos comenzar el an√°lisis de brillo con aproximadamente el p√≠xel 30001, que est√° presente en la celda "cero". En este caso, salimos inmediatamente del bucle, obteniendo el √≠ndice inicial '0' y cero p√≠xeles descartados.

Eche un vistazo al código HLSL:

//
int numProcessedPixels = 0;

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

//
bool bExitLoop = false;

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

// , lumaValue
int tempSum = numProcessedPixels + numPixels;

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


El misterioso n√ļmero "-1" de la l√≠nea 21 del c√≥digo del ensamblador est√° asociado con la condici√≥n booleana para la ejecuci√≥n del bucle (descubr√≠ esto casi por accidente).

Habiendo recibido el n√ļmero de p√≠xeles de las celdas lumaValue y lumaValue , podemos pasar al segundo ciclo.

La tarea del segundo ciclo es calcular la influencia de los píxeles y el brillo promedio.

Comenzamos con lumaValue calculado en el primer bucle.

float finalAvgLuminance = 0.0f;

//
uint numProcessedPixelStart = numProcessedPixels;

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

//
numProcessedPixels += numPixels;

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

//
float numberOfPixelsWithCurrentLuma = numPixels;

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


En esta etapa, obtuvimos el valor de brillo codificado en el intervalo [0.0f-255.f].

El proceso de decodificación es bastante simple: debe invertir el cálculo de la etapa de codificación.

Una breve repetición del proceso de codificación:

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

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

//
outLuma = log( outLuma );

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

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


Para decodificar el brillo, revertimos el proceso de codificación, por ejemplo, así:

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

// :

// 128
fDecodedLuma /= 128.0;

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

// 1.0
fDecodedLuma -= 1.0;


Luego calculamos la distribuci√≥n multiplicando el n√ļmero de p√≠xeles con un brillo dado por el brillo decodificado, y sum√°ndolos hasta que lleguemos al procesamiento de p√≠xeles a los p√≠xeles de Finalizar la Consideraci√≥n .

Después de eso, dividimos el efecto total en la cantidad de píxeles analizados.

Aqu√≠ est√° el resto del bucle (y el sombreador): el sombreador completo est√° disponible aqu√≠ . Es totalmente compatible con mi programa HLSLexplorer , sin el cual no ser√≠a capaz de recrear efectivamente el c√°lculo de brillo promedio en The Witcher 3 (¬°y todos los dem√°s efectos tambi√©n!). En conclusi√≥n, algunos pensamientos. En t√©rminos de c√°lculo del brillo promedio, este sombreador fue dif√≠cil de recrear. Las razones principales: 1) Extra√Īos controles "pendientes" sobre la ejecuci√≥n del ciclo, tom√≥ mucho m√°s tiempo de lo que pensaba anteriormente. 2) Problemas con la depuraci√≥n de este sombreador computacional en RenderDoc (v. 1.2).

//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;

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


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

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

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

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










Las operaciones "ld_structured_indexable" no son totalmente compatibles, aunque el resultado de la lectura del √≠ndice 0 da el valor correcto, todos los dem√°s devuelven ceros, raz√≥n por la cual los ciclos contin√ļan indefinidamente.

Aunque no pude lograr el mismo código de ensamblaje que en el original (vea la captura de pantalla a continuación para ver las diferencias), al usar RenderDoc pude inyectar este sombreador en la tubería, ¡y los resultados fueron los mismos!


El resultado de la batalla. A la izquierda está mi sombreador, a la derecha está el código ensamblador original.

Parte 8. La luna y sus fases.


En la octava parte del artículo, examino el sombreador de luna de The Witcher 3 (y más específicamente, de la extensión Blood and Wine).

La luna es un elemento importante del cielo nocturno, y puede ser bastante difícil hacerlo creíble, pero para mí caminar de noche en TW3 fue un verdadero placer.

¬°Solo mira esta escena!


Antes de tomar el sombreador de píxeles, diré algunas palabras sobre los matices de la representación. Desde un punto de vista geométrico, la Luna es solo una esfera (ver más abajo), que tiene coordenadas de textura, vectores normales y tangentes. El sombreador de vértices calcula la posición en el espacio mundial, así como los vectores normalizados de normales, tangentes y tangentes a dos puntos (usando un producto vectorial), multiplicados por la matriz mundial.

Para garantizar que la luna se encuentre completamente en un plano distante, los campos MinDepth y MaxDepth de la estructura D3D11_VIEWPORT tienen asignado el valor 0.0 (el mismo truco utilizado para la c√ļpula del cielo). La luna se representa inmediatamente despu√©s del cielo.


La esfera solía dibujar la luna

Bueno, creo que todo puede proceder. Echemos un vistazo al sombreador de píxeles: la razón principal por la que elegí un sombreador de Blood and Wine es simple: es más corto. Primero, calculamos el desplazamiento para muestrear la textura. cb0 [0] .w se usa como desplazamiento a lo largo del eje X. Con este simple truco podemos simular la rotación de la luna alrededor de su eje.

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









Ejemplos de valores del b√ļfer constante.

Una textura (1024x512) se adjunta como entrada. El mapa normal est√° codificado en los canales RGB y el color de la superficie de la luna en el canal alfa. Listo!


El canal alfa de una textura es el color de la superficie de la luna.


Los canales de textura RGB son un mapa normal.

Habiendo recibido las coordenadas de textura correctas, tomamos muestras de los canales RGBA. Necesitamos desempaquetar el mapa normal y realizar la correcci√≥n gamma del color de la superficie. Actualmente, un sombreador HLSL puede escribirse as√≠, por ejemplo: El siguiente paso es realizar un enlace normal, pero solo en componentes XY. (En The Witcher 3, el eje Z est√° arriba y todo el canal Z de la textura es 1.0). Podemos hacerlo de esta manera: ahora es el momento de mi parte favorita de este sombreador. Mire nuevamente las l√≠neas 15-16: ¬ŅQu√© es este misterioso 0.033864? Al principio, parece que no tiene sentido, pero si calculamos el valor inverso a √©l, obtenemos aproximadamente 29.53, que es igual a la duraci√≥n del mes sin√≥dico

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

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

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

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

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




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

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

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




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


en dias! ¡Esto es lo que llamo la atención al detalle!

Podemos suponer de manera confiable que cb0 [0] .y es el n√ļmero de d√≠as que han pasado durante el juego. Aqu√≠ se usa una desviaci√≥n adicional, utilizada como el desplazamiento a lo largo del eje x de la textura.

Habiendo recibido este coeficiente, lo multiplicamos por 2 * Pi.

Luego, usando sincos, calculamos otro vector 2d.

Al calcular el producto escalar entre el vector normal y el vector "luna", se simula una fase de la luna. Mira las capturas de pantalla con diferentes fases de la luna:

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

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

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

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






El paso final es realizar una serie de operaciones de multiplicación para calcular el color final. Probablemente no entiendas por qué este sombreador envía un valor alfa de 0.0 a la salida. Esto se debe a que la luna se representa con la combinación habilitada:

// .

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

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

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

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

moonColor *= paramHorizon;

//
return float4(moonColor, 0.0);





Este enfoque le permite obtener el color de fondo (cielo) si este sombreador devuelve negro.

Si está interesado en un sombreador completo, puede tomarlo aquí . Tiene grandes buffers constantes y ya debería estar listo para la inyección en RenderDoc en lugar del sombreador original (solo cambie el nombre de "MoonPS" a "EditedShaderPS").

Y el √ļltimo: quer√≠a compartir los resultados contigo: a

la izquierda est√° mi sombreador, a la derecha est√° el sombreador original del juego.

La diferencia es mínima y no afecta los resultados.


Como puede ver, este sombreador fue bastante f√°cil de recrear.

Parte 9. G-buffer


En esta parte, revelaré algunos detalles del gbuffer en The Witcher 3. Asumiremos

que conoce los conceptos b√°sicos del sombreado diferido.

Breve repetición: la idea de posponer no es calcular todas las luces y sombras terminadas a la vez, sino dividir los cálculos en dos etapas.

En la primera (pasada de geometría) llenamos el GBuffer con datos de superficie (posición, normales, color especular, etc.), y en la segunda (pasada de iluminación) combinamos todo y calculamos la iluminación.

El sombreado diferido es un enfoque muy popular porque le permite calcular en una sola pasada de pantalla completa mediante técnicas como el sombreado diferido de mosaico , que mejora enormemente el rendimiento.

En pocas palabras, GBuffer es un conjunto de texturas con propiedades de geometría. Es muy importante crear la estructura adecuada para ello. Como ejemplo de la vida real, puede estudiar la tecnología de renderizado Crysis 3 .

Después de esta breve introducción, veamos un marco de ejemplo de The Witcher 3: Blood and Wine:


Uno de los muchos hoteles en Toussent

Basic GBuffer consta de tres objetivos de representaci√≥n en pantalla completa en el formato DXGI_FORMAT_R8G8B8A8_UNORM y un b√ļfer de profundidad + plantilla en el formato DXGI_FORMAT_D24_UNORM_S8_UINT.

Aquí están sus capturas de pantalla:


Renderizar objetivo 0: canales RGB, color de superficie


Renderizar objetivo 0: canal alfa. Honestamente, no tengo idea de qué es esta información.


Render Target 1 - Canales RGB. Aquí se registran los vectores normales en el intervalo [0-1].


Renderizar objetivo 1: canal alfa. Parece que la reflectividad!


Render Target 2: canales RGB. ¬°Parece un color especular!

En esta escena, el canal alfa es negro (pero luego se usa).


Buffer profundidades. Tenga en cuenta que la profundidad invertida se utiliza aquí.


El b√ļfer de plantilla utilizado para marcar un tipo espec√≠fico de p√≠xel (por ejemplo, piel, vegetaci√≥n, etc.)

Este no es el GBuffer completo. El pase de iluminación también utiliza sondas de iluminación y otros amortiguadores, pero no los analizaré en este artículo.

Antes de pasar a la parte "principal" de la publicación, haré observaciones generales:

Observaciones generales



1) El √ļnico b√ļfer a limpiar es el b√ļfer de profundidad / plantilla.

Si analiza las texturas mencionadas anteriormente en un buen analizador de cuadros, se sorprenderá un poco, ya que no utilizan la llamada "Borrar", con la excepción de Profundidad / Plantilla.

Es decir, en realidad, RenderTarget1 se ve así (tenga en cuenta los píxeles "borrosos" en el plano lejano):


Esta es una optimización simple e inteligente.

Una lecci√≥n importante: debe gastar recursos en llamadas ClearRenderTargetView , as√≠ que √ļselas solo cuando sea necesario.

2) la profundidad invertida - es enfriar

en muchos artículos ya escritos sobre la precisión de la memoria intermedia de profundidad con coma flotante. Witcher 3 usa z invertida. Esta es la elección natural para un juego de mundo abierto con largas distancias de renderizado.

Cambiar a DirectX no será difícil:

a) Limpiamos el b√ļfer de profundidad escribiendo "0", no "1".

En el enfoque tradicional, el valor lejano "1" se utiliz√≥ para borrar el b√ļfer de profundidad. Despu√©s del cambio de profundidad, el nuevo valor "distante" se convirti√≥ en 0, por lo que debe cambiar todo.

b) Cambie los límites cercanos y lejanos al calcular la matriz de proyección.

c) Cambie la verificación de profundidad de "menos" a "más".

Para OpenGL, se necesita un poco más de trabajo (ver los artículos mencionados anteriormente), pero vale la pena.

3) No mantenemos nuestra posición en el mundo

, sí, todo es muy simple. En el paso de la iluminación, recreamos una posición en el mundo desde las profundidades.

Sombreador de píxeles


En esta parte, quería mostrar exactamente el sombreador de píxeles que proporciona datos de superficie a GBuffer.

Así que ahora ya sabemos cómo almacenar colores, normales y especulares.

Por supuesto, no todo es tan simple como podría pensar.

El problema con el sombreador de p√≠xeles es que tiene muchas opciones. Difieren en el n√ļmero de texturas transferidas a ellos y en el n√ļmero de par√°metros utilizados del buffer constante (probablemente del buffer constante que describe el material).

Para el análisis, decidí usar este hermoso barril:


Nuestro heroico barril!

Por favor, dale la bienvenida a las texturas:


Entonces tenemos albedo, un mapa normal y color especular. Estuche bastante est√°ndar.

Antes de comenzar, algunas palabras sobre la entrada de geometría: La

geometría se transmite con posición, texcoords, buffers normales y tangentes.

El sombreador de vértices genera al menos texcoords, vectores tangentes / normales / tangentes normalizados a dos puntos, previamente multiplicados por la matriz mundial. Para materiales más complejos (por ejemplo, con dos mapas difusos o dos mapas normales), el sombreador de vértices puede generar otros datos, pero quería mostrar un ejemplo simple aquí.

Sombreador de píxeles en el código del ensamblador: un sombreador consta de varios pasos. Describiré cada parte principal de este sombreador por separado.

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




Pero primero, como de costumbre, una captura de pantalla con los valores del b√ļfer constante:


Albedo


Comenzaremos con cosas complejas. No es solo "OutputColor.rgb = Texture.Sample (uv) .rgb"

Después de probar la textura de color RGB (línea 1), las siguientes 14 líneas son lo que yo llamo el "buffer de reducción de saturación". Déjame mostrarte el código HLSL: para la mayoría de los objetos, este código no hace más que devolver el color original de la textura. Esto se logra mediante los valores correspondientes de "material cbuffer". cb4_v1.x tiene un valor de 1.0, que devuelve una máscara de 0.0 y devuelve el color de entrada de la instrucción lerp . Sin embargo, hay algunas excepciones. El factor de desaturación más grande que encontré es 4.0 (nunca es menor que 1.0), y el color desaturado

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

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

float factor = 0.5 * (desaturationFactor - 1.0);

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

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

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




Depende del material. Puede ser algo como (0.2, 0.3, 0.4); No hay reglas estrictas. Por supuesto, no pude resistirme a implementar esto en mi propio marco DX11, y aquí están los resultados donde todos los valores de color desaturado son iguales a float3 (0.25, 0.3, 0.45)


desaturationFactor = 1.0 (no tiene efecto)


desaturationFactor = 2.0


desaturationFactor = 3.0


desaturationFactor = 4.0

Estoy seguro de que esto es solo una aplicación de parámetros de material, pero no se realiza al final de la parte albedo.

Las l√≠neas 15-20 agregan los toques finales: v0.z es la salida del sombreador de v√©rtices, y son cero. No lo olvides, porque v0.z luego se usar√° un par de veces. Parece que es alg√ļn tipo de coeficiente, y todo el c√≥digo parece un peque√Īo albedo de atenuaci√≥n, pero dado que v0.z es 0, el color permanece sin cambios. HLSL: Con respecto a RT0.a, como podemos ver, se toma del b√ļfer de material constante, pero dado que el sombreador no tiene informaci√≥n de depuraci√≥n, es dif√≠cil decir qu√© es. Tal vez la translucidez? ¬°Hemos terminado con el primer objetivo de renderizado!

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






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

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

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

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






Normal


Comencemos desempacando el mapa normal y luego, como de costumbre, vincularemos las normales: hasta ahora, nada sorprendente. Mire las líneas 28-33: podemos escribirlas aproximadamente de la siguiente manera: No estoy seguro si es correcto escribirlas. Si sabes qué es esta operación matemática, házmelo saber. Vemos que el sombreador de píxeles usa SV_IsFrontFace.

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

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

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

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






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




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






Que es estoLa documentación viene para ayudar (quería escribir "msdn", pero ...):

Determina si el triángulo está mirando a la cámara. Para líneas y puntos, IsFrontFace es verdadero. Una excepción son las líneas dibujadas a partir de triángulos (modo de estructura alámbrica), que establecen IsFrontFace de manera similar a rasterizar un triángulo en modo sólido. Se puede escribir en él con un sombreador de geometría y leerlo con un sombreador de píxeles.

Quería comprobarlo por mi cuenta. Y, de hecho, el efecto solo se nota en el modo de estructura alámbrica. Creo que este código es necesario para el cálculo correcto de las normales (y, por lo tanto, la iluminación) en modo de estructura alámbrica.

Aquí hay una comparación: tanto los colores del marco de la escena terminada con este truco activado / desactivado, como la textura del gbuffer [0-1] normales con el truco activado / desactivado:


El color de la escena sin truco.


Escena de color con acrobacias


Normal [0-1] sin truco


Normal [0-1] con un truco

¬ŅHas notado que cada objetivo de renderizado en GBuffer tiene el formato R8G8B8A8_UNORM? Esto significa que hay 256 valores posibles por componente. ¬ŅEs esto suficiente para almacenar normales?

El almacenamiento de normales de alta calidad con suficientes bytes en Gbuffer es un problema conocido, pero afortunadamente hay muchos materiales diferentes para aprender . Quizás algunos de ustedes ya saben qué técnica se usa aquí. Debo decir que en todo el pasaje de la geometría hay una textura adicional unida a la ranura 13 ...:





Ja!The Witcher 3 utiliza una técnica llamada " Normal de mejor ajuste ". Aquí no lo explicaré en detalle (ver presentación). Fue inventado alrededor de 2009-2010 por Crytek, y dado que CryEngine tiene código abierto, BFN también es de código abierto .

BFN le da a la textura de las normales un aspecto "granulado".

Después de escalar las normales usando BFN, las recodificamos del intervalo [-1; 1] a [0, 1].

Especular


Comencemos desde la línea 34 y muestreemos la textura especular: como puede ver, hay un filtro de "atenuación" que conocemos de Albedo: calculamos el componente con máx. valor, y luego calcular el color "oscuro" e interpolarlo con el color especular original, tomando el parámetro del sombreador de vértices ... que es 0, entonces en la salida obtenemos el color de la textura. HLSL:

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








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

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


Reflexividad


No tengo idea de si este nombre es adecuado para este parámetro, porque no sé cómo afecta el paso de la iluminación. El hecho es que el canal alfa del mapa normal de entrada contiene datos adicionales:


Textura de canal alfa "mapa normal".

Código de ensamblador: ¡ Saluda a nuestro viejo amigo - v0.z! Su significado es similar al albedo y especular:

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




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

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


GenialEste es el final del análisis de la primera versión del sombreador de píxeles.

Aquí hay una comparación de mi sombreador (izquierda) con el original (derecha):


Estas diferencias no afectan los cálculos, por lo que mi trabajo aquí está terminado.

Pixel Shader: Albedo + Opción Normal


Decid√≠ mostrar una opci√≥n m√°s, ahora solo con albedo y mapas normales, sin textura especular. El c√≥digo del ensamblador es un poco m√°s largo: la diferencia entre esta y las opciones anteriores es la siguiente: a) l√≠neas 1, 19 : el par√°metro de interpolaci√≥n v0.z se multiplica por cb4 [0] .x del b√ļfer constante, pero este producto solo se usa para el albedo de interpolaci√≥n en la l√≠nea 19. Para otra salida, se utiliza el valor "normal" de v0.z. b) l√≠neas 54-55 : o2.w ahora se establece bajo la condici√≥n de que (cb4 [7] .x> 0.0) Ya reconocemos este patr√≥n "alg√ļn tipo de comparaci√≥n - Y" a partir del c√°lculo del histograma de brillo. Se puede escribir as√≠: c) l√≠neas 34-42 : un c√°lculo especular completamente diferente.

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










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



No hay textura especular aqu√≠. Veamos el c√≥digo del ensamblador responsable de esta parte: tenga en cuenta que lo usamos aqu√≠ (1 - capacidad reflejada). Afortunadamente, escribir esto en HLSL es bastante simple: agregar√© que en esta versi√≥n el b√ļfer constante con datos de material es un poco m√°s grande. Aqu√≠, estos valores adicionales se utilizan para emular el color especular. El resto del sombreador es el mismo que en la versi√≥n anterior. 72 l√≠neas de c√≥digo de ensamblador es demasiado para mostrar en WinMerge, as√≠ que cr√©ame: mi c√≥digo result√≥ ser casi el mismo que en el original. ¬°O puede descargar mi HLSLexplorer y verlo usted mismo!

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




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

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








Para resumir


... y si lo lees aquí, entonces probablemente quieras profundizar un poco más.

Lo que parece simple en la vida real a menudo no es el caso, y la transferencia de datos a gbuffer The Witcher 3 no fue la excepción. Le mostré solo las versiones más simples de los sombreadores de píxeles responsables de ello, y también proporcioné observaciones generales relacionadas con el sombreado diferido en general.

Para la mayoría de los pacientes, hay dos opciones para sombreadores de píxeles en pastebin:

Opción 1 - con textura especular

Opción 2 - sin textura especular

Parte 10. Cortinas de lluvia a lo lejos


En esta parte veremos un maravilloso efecto atmosférico que realmente me gusta: lluvia distante / cortinas de luz cerca del horizonte. En el juego, son más fáciles de encontrar en las Islas Skellig.


Personalmente, me gusta mucho este fenómeno atmosférico y tenía curiosidad por saber cómo lo implementaron los programadores gráficos de CD Projekt Red. ¡Vamos a resolverlo!

Aquí hay dos capturas de pantalla antes y después de aplicar las cortinas de lluvia:


A las cortinas de lluvia


Después de las cortinas de lluvia

Geometría


Primero, nos centraremos en la geometr√≠a. La idea es usar un cilindro peque√Īo:


Un cilindro en el espacio local

Desde el punto de vista de su posici√≥n en el espacio local, es bastante peque√Īo: su posici√≥n est√° en el rango (0.0 - 1.0).

El circuito de entrada para esta llamada de sorteo se ve así ...


Lo siguiente es importante para nosotros aquí: Texcoords e Instance_Transform.

Los Texcoords se envuelven de manera bastante simple: U de las bases superior e inferior est√°n en el intervalo [0.02777 - 1.02734]. V en la base inferior es 1.0, y en la superior - 0.0. Como puede ver, simplemente puede crear esta malla incluso de manera procesal.

Habiendo recibido este peque√Īo cilindro en el espacio local, lo multiplicamos por la matriz mundial provista para cada instancia del elemento de entrada INSTANCE_TRANSFORM. Veamos los valores de esta matriz:




Parece bastante aterrador, ¬Ņverdad? ¬°Pero no se preocupe, analizaremos esta matriz y veremos qu√© oculta! Los resultados son muy interesantes: es importante conocer la posici√≥n de la c√°mara en este cuadro en particular: (-116.5338, 234.8695, 2.09) Como puede ver, escalamos el cilindro para hacerlo bastante grande en el espacio mundial (en TW3 el eje Z est√° dirigido hacia arriba), lo movimos en relaci√≥n con la posici√≥n de la c√°mara y se volvi√≥. As√≠ es como se ve el cilindro despu√©s de la conversi√≥n con el sombreador de v√©rtices:

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

mat = XMMatrixTranspose( mat );

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

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




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

vScale: (299.999969, 300.000000, 1000.00012)

vTranslation: (-116.495102, 238.690796, -1.45564997)









Cilindro después de la conversión por sombreador de vértices. Vea cómo se ubica en relación con la pirámide de visibilidad.

Sombreador de vértices


La geometría de entrada y el sombreador de vértices dependen estrictamente el uno del otro.

Echemos un vistazo m√°s de cerca al c√≥digo de ensamblador para el sombreador de v√©rtices: junto con Texcoords simples que pasan (l√≠nea 0) e Instance_LOD_Params (l√≠nea 8), se necesitan dos elementos m√°s para la salida: SV_Position (esto es obvio) y Altura (componente .z) de la posici√≥n en el mundo. ¬ŅRecuerdas que el espacio local est√° en el rango [0-1]? Entonces, justo antes de aplicar la matriz mundial, el sombreador de v√©rtices usa escala y desviaci√≥n para cambiar la posici√≥n local. Movimiento inteligente! En este caso, scale = float3 (4, 4, 2) y bias = float3 (-2, -2, -1). < El patr√≥n que se nota entre las l√≠neas 9 y 28 es la multiplicaci√≥n de dos matrices de filas principales. Veamos el sombreador de v√©rtices terminado en HLSL:

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












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

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

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

struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;

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

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

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

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

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

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

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

return Output;
}


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


Las diferencias no afectan los cálculos. ¡Inyecté mi sombreador en el marco y todo seguía bien!

Sombreador de píxeles


Por fin!Para comenzar, le mostraré la entrada:

aqu√≠ se utilizan dos texturas: la textura de ruido y el b√ļfer de profundidad:



Valores de buffers constantes:





Y el código ensamblador para el sombreador de píxeles:

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


Wow!Una cantidad bastante grande, pero de hecho, no todo es tan malo.

¬ŅQu√© est√° pasando aqu√≠? Primero, calculamos los UV animados usando el tiempo transcurrido desde cbuffer (cb0 [0] .x) y la escala / compensaciones. Estos texcoords se usan para tomar muestras de la textura de ruido (l√≠nea 2).

Habiendo recibido el valor de ruido de la textura, interpolamos entre los valores min / max (generalmente 0 y 1).

Luego hacemos la multiplicaci√≥n, por ejemplo, por la coordenada de la textura V (¬Ņrecuerda que la coordenada V va de 1 a 0?) - l√≠nea 5.

Por lo tanto, calculamos la "máscara de brillo" - se ve así:


Tenga en cuenta que los objetos distantes (un faro, monta√Īas ...) han desaparecido. Esto sucedi√≥ porque el cilindro pasa la prueba de profundidad: el cilindro no est√° en el plano lejano y se dibuja sobre estos objetos:


Prueba de profundidad

Queremos simular que la cortina de lluvia est√° m√°s lejos (pero no necesariamente en el plano lejano). Para hacer esto, calculamos otra m√°scara, la "m√°scara de objetos distantes".

Se calcula mediante la siguiente fórmula:

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

(0.001 se toma del b√ļfer), lo que nos da la m√°scara deseada:


(En la parte sobre el efecto Enfocar, ya expliqu√© superficialmente c√≥mo se extrae la profundidad de la pir√°mide de visibilidad del b√ļfer de profundidad).

Personalmente, me parece que este efecto podr√≠a ser menos costoso sin calcular la altura en el espacio mundial multiplicando la profundidad de la pir√°mide de visibilidad por un n√ļmero menor, por ejemplo 0.0004.

Cuando ambas m√°scaras se multiplican, se obtiene la √ļltima:


Habiendo recibido esta máscara final (línea 16), realizamos otra interpolación, que no hace casi nada (al menos en el caso probado), y luego multiplicamos la máscara final por el color de las cortinas (línea 19), realizamos la corrección gamma (líneas 20 -22) y multiplicaciones finales (23-24).

Al final, devolvemos un color con un valor alfa cero. Esto se debe a que la mezcla est√° habilitada en este paso:

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

si no comprende bien cómo funciona la mezcla, aquí hay una breve explicación:

SourceColor es la salida RGB del sombreador de píxeles, y DestColor es el color RGB actual del píxel en el objetivo de renderizado . Desde SourceAlpha siempre igual a 0,0, la ecuación anterior se reduce a: FinalColor = SourceColor + DestColor.

En pocas palabras, aquí estamos realizando una mezcla aditiva. Si el sombreador de píxeles regresa (0, 0, 0), el color seguirá siendo el mismo.

Aquí está el código HLSL terminado: creo que después de explicarlo será mucho más fácil de entender: puedo decir con placer que mi sombreador de píxeles crea el mismo código de ensamblador que en el original. Espero que hayas disfrutado el artículo. Gracias por leer!

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

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

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

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

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

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

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

float2 invViewportSize = cb0_v1.zw;

float depthScale = cb4_v6.x; // 0.001

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

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

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


// * Calculate mask covering distant objects behind cylinder.

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

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

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

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

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

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

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




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


All Articles