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:
Opción "baja" - hastaOpción "baja" - despuésAlta opción - hastaOpción "Alta" - despuésSi 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:
| agudizar | sharpenFar | sharpenDistanceScale | sharpenDistanceBias | sharpenLumScale | sharpenLumBias |
---|
bajo |
alto | 2,0 | 1,8 | 0,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 HDRAhora 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 lunaBueno, 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ódicofloat4 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: ala 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. Asumiremosque 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 ToussentBasic 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 superficieRenderizar 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 enfriaren 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: Lageometrí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 desaturadofloat3 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.0desaturationFactor = 3.0desaturationFactor = 4.0Estoy 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 esto
La 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 acrobaciasNormal [0-1] sin trucoNormal [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;
Genial
Este 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 especularOpción 2 - sin textura especularParte 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 lluviaDespués de las cortinas de lluviaGeometría
Primero, nos centraremos en la geometría. La idea es usar un cilindro pequeño:Un cilindro en el espacio localDesde 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 profundidadQueremos 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 );
}