La ingeniería inversa de la representación de The Witcher 3: varios efectos de cielo

imagen

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

Parte 1. Nubes Cirrus


Cuando el juego se desarrolla en espacios abiertos, uno de los factores que determina la credibilidad del mundo es el cielo. Piénselo: la mayoría de las veces el cielo literalmente ocupa alrededor del 40-50% de toda la pantalla. El cielo es mucho más que un hermoso gradiente. Tiene estrellas, el sol, la luna y finalmente nubes.

Aunque las tendencias actuales parecen consistir en el renderizado volumétrico de nubes usando raymarching (ver este artículo ), las nubes en The Witcher 3 están completamente basadas en texturas. Ya los examiné antes, pero resultó que con ellos todo es más complicado de lo que originalmente esperaba. Si seguiste mi serie de artículos, entonces sabrás que hay una diferencia entre el DLC Blood and Wine y el resto del juego. Y, como puede suponer, hay algunos cambios en el trabajo con nubes en el DLC.

The Witcher 3 tiene varias capas de nubes. Dependiendo del clima, solo pueden ser cirros , cúmulos altos , posiblemente algunas nubes de la familia de nubes en capas (por ejemplo, durante una tormenta). Al final, puede que no haya nubes en absoluto.

Algunas capas difieren en términos de texturas y sombreadores utilizados para representarlas. Obviamente, esto afecta la complejidad y la longitud del código ensamblador para el sombreador de píxeles.

A pesar de toda esta diversidad, hay algunos patrones comunes que se pueden observar al renderizar nubes en Witcher 3. Primero, todos se renderizan en un paso proactivo, y esta es la elección perfecta. Todos ellos usan mezcla (ver más abajo). Esto hace que sea mucho más fácil controlar cómo una capa separada cubre el cielo; esto se ve afectado por el valor alfa del sombreador de píxeles.


Más interesante aún, algunas capas se representan dos veces con los mismos parámetros.

Después de mirar el código, elegí el sombreador más corto para (1) muy probablemente realizar su ingeniería inversa completa, (2) descubrir todos sus aspectos.

Eché un vistazo más de cerca a las cirros de Witcher 3: Blood and Wine.

Aquí hay un marco de ejemplo:


Antes de renderizar


Después del primer pase de render


Después del segundo paso de renderizado

En este marco en particular, las nubes cirrus son la primera capa en el renderizado. Como puede ver, se procesa dos veces, lo que aumenta su brillo.

Sombreador geométrico y de vértices


Antes del sombreador de píxeles, hablaremos brevemente sobre los sombreadores geométricos y de vértices utilizados. La malla para mostrar nubes es un poco como un domo de cielo normal:


Todos los vértices están en el intervalo [0-1], por lo que para centrar la malla en el punto (0,0,0), se utilizan la escala y la desviación antes de convertir a worldViewProj (ya conocemos este patrón de las partes anteriores de la serie). En el caso de las nubes, la malla se estira fuertemente a lo largo del plano XY (el eje Z apunta hacia arriba) para cubrir más espacio que la pirámide de visibilidad. El resultado es el siguiente:


Además, la malla tiene vectores normales y tangentes. El sombreador de vértices también calcula el vector bi-tangente por el producto vectorial: los tres se muestran en forma normalizada. También hay un cálculo superior de niebla (su color y brillo).

Sombreador de píxeles


El código de ensamblaje del sombreador de píxeles se ve así:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb12[238], immediateIndexed  
    dcl_constantbuffer cb4[13], 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 linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.w  
    dcl_input_ps linear v3.xyzw  
    dcl_input_ps linear v4.xyz  
    dcl_input_ps linear v5.xyz  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  
   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx  
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx  
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   8: add r1.xyz, r1.xyzx, r1.xyzx  
   9: dp3 r0.w, r1.xyzx, r1.xyzx  
  10: rsq r0.w, r0.w  
  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  
  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x  
  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x  
  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x  
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x  
  53: mul r0.w, r0.w, r1.x  
  54: mul_sat r0.w, r0.w, cb4[6].x  
  55: mul o0.xyz, r0.wwww, r0.xyzx  
  56: mov o0.w, r0.w  
  57: ret 

Se introducen dos texturas sin costuras. Uno de ellos contiene un mapa normal (canales xyz ) y una forma de nube (canal a ). El segundo es ruido para distorsionar la forma.


Mapa normal, propiedad CD Projekt Red


Forma de nube, propiedad CD Projekt Red


Textura de ruido, propiedad de CD Projekt Red

El búfer principal de constantes con parámetros de nube es cb4. Para este marco, tiene los siguientes significados:


Además, se utilizan otros valores de otros cbuffers. No te preocupes, los consideraremos también.

Luz solar invertida en dirección Z


Lo primero que sucede en el sombreador es el cálculo de la dirección normalizada de la luz solar invertida a lo largo del eje Z:

   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  

   float3 invertedSunlightDir = normalize(lightDir * float3(1, 1, -1) );

Como se mencionó anteriormente, el eje Z se dirige hacia arriba, y cb0 [9] es la dirección de la luz solar. Este vector está dirigido al sol, ¡es importante! Puede verificar esto escribiendo un sombreador computacional simple que ejecute un NdotL simple e insertándolo en el paso del sombreador diferido.

Muestreo de textura de nube


El siguiente paso es calcular las texcoords para muestrear la textura de la nube, desempaquetar el vector normal y normalizarlo.

   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx   
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx   
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0   
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)   
   8: add r1.xyz, r1.xyzx, r1.xyzx   
   9: dp3 r0.w, r1.xyzx, r1.xyzx   
  10: rsq r0.w, r0.w   
   
   
   // Calc sampling coords  
   float2 cloudTextureUV = Texcoords * textureScale + elapsedTime * speedFactors;  
   
   // Sample texture and get data from it  
   float4 cloudTextureValue = texture0.Sample( sampler0, cloudTextureUV ).rgba;  
   float3 normalMap = cloudTextureValue.xyz;  
   float cloudShape = cloudTextureValue.a;  
   
   // Unpack normal and normalize it  
   float3 unpackedNormal = (normalMap - 0.5) * 2.0;  
   unpackedNormal = normalize(unpackedNormal);

Vamos a lidiar con eso gradualmente.

Para obtener el movimiento de las nubes, necesitamos un tiempo transcurrido en segundos ( cb [0] .x ) multiplicado por el coeficiente de velocidad, que afecta la rapidez con que las nubes se mueven por el cielo ( cb4 [5] .xy ).

Como dije antes, los rayos UV se extienden a lo largo de la geometría del domo del cielo, y también necesitamos factores de escala de textura que afecten el tamaño de las nubes ( cb4 [4] .xy ).

La fórmula final es:

samplingUV = Input.TextureUV * textureScale + time * speedMultiplier;

Después de muestrear los 4 canales, tenemos un mapa normal (canales rgb) y una forma de nube (canal a).

Para desempaquetar el mapa normal del intervalo [0; 1] en el intervalo [-1; 1] usamos la siguiente fórmula:

unpackedNormal = (packedNormal - 0.5) * 2.0;

También puedes usar esto:

unpackedNormal = packedNormal * 2.0 - 1.0;

Finalmente, normalizamos el vector normal desempaquetado.

Superposición de normales


Teniendo los vectores normales, los vectores tangente y bi-tangente del sombreador de vértices, y el vector normal del mapa normal, normalmente mapeamos los normales.

  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
    
   // Perform bump mapping  
   float3 SkyTangent = Input.Tangent;  
   float3 SkyNormal = (float3( Input.Texcoords.zw, Input.param3.w ));  
   float3 SkyBitangent = Input.param3.xyz;  
        
   float3x3 TBN = float3x3(SkyTangent, SkyBitangent, SkyNormal);  
   float3 finalNormal = (float3)mul( unpackedNormal, (TBN) );

Brillo (1)


En el siguiente paso, se aplica el cálculo NdotL y esto afecta la cantidad de iluminación de un píxel específico.

Considere el siguiente código de ensamblador:

  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  

Aquí está la visualización de NdotL en el marco en cuestión:


Este producto escalar (con saturación) se usa para interpolar entre minIntensity y maxIntensity. Gracias a esto, partes de las nubes iluminadas por la luz solar serán más brillantes.

   // Calculate cosine between normal and up-inv lightdir  
   float NdotL = saturate( dot(invertedSunlightDir, finalNormal) );  
   
   // Param 1, line 19, r0.x  
   float intensity1 = lerp( param1Min, param1Max, NdotL );

Brillo (2)


Hay otro factor que afecta el brillo de las nubes.

Las nubes ubicadas en esa parte del cielo donde está el sol, deberían resaltarse más. Para hacer esto, calculamos el gradiente basado en el plano XY.

Este gradiente se usa para calcular la interpolación lineal entre los valores mínimo / máximo, similar a lo que sucede en la parte (1).

Es decir, en teoría, podemos pedir oscurecer las nubes ubicadas en el lado opuesto del sol, pero esto no sucede en este marco en particular, porque param2Min y param2Max ( cb4 [0] .x y cb4 [1] .x ) están configurados en 1.0f.

  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x   
   
   
   // Calculate normalized -lightDir.xy (20-22)  
   float2 lightDirXY = normalize( -lightDir.xy );  
   
   // Calculate world to camera  
   float3 vWorldToCamera = ( CameraPos - WorldPos );  
   float worldToCamera_distance = length(vWorldToCamera);  
        
   // normalize vector  
   vWorldToCamera = normalize( vWorldToCamera );  
        
   
   float LdotV = saturate( dot(lightDirXY, vWorldToCamera.xy) );  
   float highlightedSkySection = saturate( 2*LdotV );  
   float intensity2 = lerp( param2Min, param2Max, highlightedSkySection );  
   
   float finalIntensity = pow( intensity2 *intensity1, 2.2);

Al final, multiplicamos ambos brillos y elevamos el resultado a una potencia de 2.2.

Color de la nube


El cálculo del color de las nubes comienza con la obtención de las constantes del búfer de dos valores que indican el color de las nubes junto al sol y las nubes en el lado opuesto del cielo. Entre ellos, la interpolación lineal se realiza en función de la sección de cielo resaltada .

Entonces el resultado se multiplica por finalIntensity .

Y al final, el resultado se mezcla con niebla (por razones de rendimiento, fue calculado por el sombreador de vértices).

  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
   
  float3 cloudsColor = lerp( cloudsColorBack, cloudsColorFront, highlightedSunSection );  
  cloudsColor *= finalIntensity;  
  cloudsColor = lerp( cloudsColor, FogColor, FogAmount );

Hacer cirros más visibles en el horizonte


Esto no se nota mucho en el marco, pero de hecho esta capa es más visible cerca del horizonte que sobre la cabeza de Geralt. Aquí te explicamos cómo hacerlo.

Podrías notar que al calcular el segundo brillo, calculamos la longitud del vector worldToCamera :

  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w

Busquemos las siguientes ocurrencias de esta longitud en el código:

  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  ...  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x

Wow, ¿qué pasa con nosotros?

cb [7] .xy cb [8] .x tienen los valores 2000.0 y 7000.0.

Resulta que este es el resultado del uso de la función linstep .

Ella recibe tres parámetros: min / max - intervalo y v - valor.

Esto funciona de la siguiente manera: si v está en el intervalo [ min - max ], entonces la función devuelve interpolación lineal en el intervalo [0.0 - 1.0]. Por otro lado, si v está fuera de rango, entonces linstep devuelve 0.0 o 1.0.

Un simple ejemplo:

linstep( 1000.0, 2000.0, 999.0) = 0.0
linstep( 1000.0, 2000.0, 1500.0) = 0.5
linstep( 1000.0, 2000.0, 2000.0) = 1.0

Es decir, es bastante similar al paso suave de HLSL, excepto que en este caso, en lugar de la interpolación hermitiana, se realiza lineal.

Linstep no es una característica de HLSL, pero es muy útil. Vale la pena tenerlo en su kit de herramientas.

 // linstep:  
 //  
 // Returns a linear interpolation between 0 and 1 if t is in the range [min, max]   
 // if "v" is <= min, the output is 0  
 // if "v" i >= max, the output is 1  
   
 float linstep( float min, float max, float v )  
 {  
   return saturate( (v - min) / (max - min) );  
 } 

Volvamos a Witcher 3: después de calcular este indicador, informando qué tan lejos está una parte particular del cielo de Geralt, lo usamos para debilitar el brillo de las nubes:

  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
   
   float distanceAttenuation = linstep( fadeDistanceStart, fadeDistanceEnd, worldToCamera_distance );  
    
   float fadedCloudShape = closeCloudsHidingFactor * cloudShape;  
   cloudShape = lerp( fadedCloudShape, cloudShape, distanceAttenuation );

cloudShape es el canal .a de la primera textura, y closeCloudsHidingFactor es un valor de búfer constante que controla la visibilidad de las nubes sobre la cabeza de Geralt. En todos los cuadros que probé, fue igual a 0.0, que es equivalente a la ausencia de nubes. A medida que la atenuación se acerca a 1.0 (la distancia de la cámara a la cúpula del cielo aumenta), las nubes se vuelven más visibles.

Muestreo de textura de ruido


Cálculo de coordenadas de textura de ruido de muestreo cálculos similares para la textura de las nubes, excepto que utiliza un conjunto diferente de textureScale y speedMultiplier .

Por supuesto, un sampler con la envoltura de modo de direccionamiento activado se utiliza para probar todas estas texturas .

  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
   
   // Calc sampling coords for noise  
   float2 noiseTextureUV = Texcoords * textureScaleNoise + elapsedTime * speedFactorsNoise;  
   
   // Sample texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;

Poniendolo todo junto


Habiendo recibido el valor de ruido, debemos combinarlo con cloudShape.

Tuve algunos problemas para comprender estas líneas, donde hay param2.w (que siempre es 1.0) y noiseMult (tiene un valor de 5.0, tomado del búfer constante).

Sea como fuere, lo más importante aquí es el valor final de generalCloudsVisibility , que afecta la visibilidad de las nubes.

Eche un vistazo también al valor final del ruido. El color de salida de cloudsColor se multiplica por el ruido final, que también se emite al canal alfa.

  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x
  53: mul r0.w, r0.w, r1.x
  54: mul_sat r0.w, r0.w, cb4[6].x
  55: mul o0.xyz, r0.wwww, r0.xyzx
  56: mov o0.w, r0.w
  57: ret   

   // Sample noise texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;  
   noiseTextureValue = noiseTextureValue * noiseMult - noiseMult;  
     
   float noiseValue = saturate( noiseMult * Input.param2.w + noiseTextureValue);  
   noiseValue *= cloudShape;  
     
   float finalNoise = saturate( noiseValue * generalCloudsVisibility);  
   
   return float4( cloudsColor*finalNoise, finalNoise ); 

Total


El resultado final se ve muy creíble.

Puedes comparar La primera imagen es mi sombreador, la segunda es el sombreador del juego:


Si tienes curiosidad, el sombreador está disponible aquí .

Parte 2. Niebla


La niebla se puede implementar de varias maneras. Sin embargo, los tiempos en los que podíamos aplicar una niebla simple dependiente de la distancia y eliminarla fueron para siempre en el pasado (muy probablemente). Vivir en el mundo de los sombreadores programables ha abierto la puerta a nuevas soluciones locas, pero más importante, físicamente precisas y visualmente realistas.

Las tendencias actuales en el renderizado de niebla se basan en sombreadores computacionales (para más detalles, vea esta presentación de Bart Wronsky).

A pesar de que esta presentación apareció en 2014, y The Witcher 3 se lanzó en 2015/2016, la niebla en la última parte de las aventuras de Geralt depende completamente de la pantalla y se implementa como un postprocesamiento típico.

Antes de comenzar nuestra próxima sesión de ingeniería inversa, debo decir que durante el año pasado intenté descubrir la niebla de Witcher 3 al menos cinco veces, y cada vez fallé. El código del ensamblador, como verá pronto, es bastante complicado, y esto hace que el proceso de crear un sombreador de niebla legible en HLSL sea casi imposible.

Sin embargo, logré encontrar un sombreador de niebla en Internet que inmediatamente me llamó la atención debido a su similitud con la niebla de The Witcher 3 en términos de nombres de variables y el orden general de instrucciones. Este sombreador no era exactamente el mismo que en el juego, así que tuve que modificarlo un poco. Quiero decir esto que la parte principal del código HLSL que ves aquí fue, con dos excepciones, no creada / analizada por mí. Recuerda esto.

Aquí está el código de ensamblador para el sombreador de niebla de píxeles: vale la pena señalar que es el mismo para todo el juego (la parte principal de 2015 y ambos DLC):

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[2], immediateIndexed  
    dcl_constantbuffer cb12[214], immediateIndexed  
    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_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 7  
   0: ftou r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r1.x, r0.xyww, t0.xyzw  
   3: mad r1.y, r1.x, cb12[22].x, cb12[22].y  
   4: lt r1.y, r1.y, l(1.000000)  
   5: if_nz r1.y  
   6:  utof r1.yz, r0.xxyx  
   7:  mul r2.xyzw, r1.zzzz, cb12[211].xyzw  
   8:  mad r2.xyzw, cb12[210].xyzw, r1.yyyy, r2.xyzw  
   9:  mad r1.xyzw, cb12[212].xyzw, r1.xxxx, r2.xyzw  
  10:  add r1.xyzw, r1.xyzw, cb12[213].xyzw  
  11:  div r1.xyz, r1.xyzx, r1.wwww  
  12:  ld_indexable(texture2d)(float,float,float,float) r2.xyz, r0.xyww, t1.xyzw  
  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)  
  24:  div r0.yzw, r0.yyzw, r1.xxxx  
  25:  mad r1.y, r0.w, cb12[22].z, cb12[0].z  
  26:  add r1.x, r1.x, -cb12[22].z  
  27:  max r1.x, r1.x, l(0)  
  28:  min r1.x, r1.x, cb12[42].z  
  29:  mul r1.z, r0.w, r1.x  
  30:  mul r1.w, r1.x, cb12[43].x  
  31:  mul r1.zw, r1.zzzw, l(0.000000, 0.000000, 0.062500, 0.062500)  
  32:  dp3 r0.y, cb12[38].xyzx, r0.yzwy  
  33:  add r0.z, r0.y, cb12[42].x  
  34:  add r0.w, cb12[42].x, l(1.000000)  
  35:  div_sat r0.z, r0.z, r0.w  
  36:  add r0.w, -cb12[43].z, cb12[43].y  
  37:  mad r0.z, r0.z, r0.w, cb12[43].z  
  38:  mul r0.w, abs(r0.y), abs(r0.y)  
  39:  mad_sat r2.w, r1.x, l(0.002000), l(-0.300000)  
  40:  mul r0.w, r0.w, r2.w  
  41:  lt r0.y, l(0), r0.y  
  42:  movc r3.xyz, r0.yyyy, cb12[39].xyzx, cb12[41].xyzx  
  43:  add r3.xyz, r3.xyzx, -cb12[40].xyzx  
  44:  mad r3.xyz, r0.wwww, r3.xyzx, cb12[40].xyzx  
  45:  movc r4.xyz, r0.yyyy, cb12[45].xyzx, cb12[47].xyzx  
  46:  add r4.xyz, r4.xyzx, -cb12[46].xyzx  
  47:  mad r4.xyz, r0.wwww, r4.xyzx, cb12[46].xyzx  
  48:  ge r0.y, r1.x, cb12[48].y  
  49:  if_nz r0.y  
  50:   add r0.y, r1.y, cb12[42].y  
  51:   mul r0.w, r0.z, r0.y  
  52:   mul r1.y, r0.z, r1.z  
  53:   mad r5.xyzw, r1.yyyy, l(16.000000, 15.000000, 14.000000, 13.000000), r0.wwww  
  54:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  55:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  56:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  57:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  58:   mul r1.z, r5.y, r5.x  
  59:   mul r1.z, r5.z, r1.z  
  60:   mul r1.z, r5.w, r1.z  
  61:   mad r5.xyzw, r1.yyyy, l(12.000000, 11.000000, 10.000000, 9.000000), r0.wwww  
  62:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  63:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  64:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  65:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:   mul r1.z, r1.z, r5.x  
  67:   mul r1.z, r5.y, r1.z  
  68:   mul r1.z, r5.z, r1.z  
  69:   mul r1.z, r5.w, r1.z  
  70:   mad r5.xyzw, r1.yyyy, l(8.000000, 7.000000, 6.000000, 5.000000), r0.wwww  
  71:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  72:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  73:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  74:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  75:   mul r1.z, r1.z, r5.x  
  76:   mul r1.z, r5.y, r1.z  
  77:   mul r1.z, r5.z, r1.z  
  78:   mul r1.z, r5.w, r1.z  
  79:   mad r5.xy, r1.yyyy, l(4.000000, 3.000000, 0.000000, 0.000000), r0.wwww  
  80:   max r5.xy, r5.xyxx, l(0, 0, 0, 0)  
  81:   add r5.xy, r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  82:   div_sat r5.xy, r1.wwww, r5.xyxx  
  83:   add r5.xy, -r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  84:   mul r1.z, r1.z, r5.x  
  85:   mul r1.z, r5.y, r1.z  
  86:   mad r0.w, r1.y, l(2.000000), r0.w  
  87:   max r0.w, r0.w, l(0)  
  88:   add r0.w, r0.w, l(1.000000)  
  89:   div_sat r0.w, r1.w, r0.w  
  90:   add r0.w, -r0.w, l(1.000000)  
  91:   mul r0.w, r0.w, r1.z  
  92:   mad r0.y, r0.y, r0.z, r1.y  
  93:   max r0.y, r0.y, l(0)  
  94:   add r0.y, r0.y, l(1.000000)  
  95:   div_sat r0.y, r1.w, r0.y  
  96:   add r0.y, -r0.y, l(1.000000)  
  97:   mad r0.y, -r0.w, r0.y, l(1.000000)  
  98:   add r0.z, r1.x, -cb12[48].y  
  99:   mul_sat r0.z, r0.z, cb12[48].z  
  100:  else  
  101:   mov r0.yz, l(0.000000, 1.000000, 0.000000, 0.000000)  
  102:  endif  
  103:  log r0.y, r0.y  
  104:  mul r0.w, r0.y, cb12[42].w  
  105:  exp r0.w, r0.w  
  106:  mul r0.y, r0.y, cb12[48].x  
  107:  exp r0.y, r0.y  
  108:  mul r0.yw, r0.yyyw, r0.zzzz  
  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, cb12[188].w, l(-1.000000)  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.z, r0.w  
  115:  lt r0.z, l(0), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, cb12[190].w, l(-1.000000)  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.z, r0.w  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif  
  126:  mul r0.z, r0.x, r5.w  
  127:  mul r0.x, r0.x, r0.y  
  128:  dp3 r0.y, l(0.333000, 0.555000, 0.222000, 0.000000), r2.xyzx  
  129:  mad r1.xyz, r0.yyyy, r4.xyzx, -r2.xyzx  
  130:  mad r0.xyw, r0.xxxx, r1.xyxz, r2.xyxz  
  131:  add r1.xyz, -r0.xywx, r5.xyzx  
  132:  mad r0.xyz, r0.zzzz, r1.xyzx, r0.xywx  
  133: else  
  134:  mov r0.xyz, l(0, 0, 0, 0)  
  135: endif  
  136: mov o0.xyz, r0.xyzx  
  137: mov o0.w, l(1.000000)  
  138: ret 

Honestamente, el sombreador es bastante largo. Probablemente demasiado tiempo para un proceso de ingeniería inversa efectivo.

Aquí hay un ejemplo de una escena de puesta de sol con niebla:


Echemos un vistazo a la entrada: en

cuanto a las texturas, tenemos un búfer de profundidad, oclusión ambiental y un búfer de color HDR.


Tampón de profundidad de entrada


Oclusión ambiental entrante


El búfer de color HDR entrante

... y el resultado de aplicar el sombreador de niebla en esta escena se ve así:


Textura HDR después de aplicar niebla El

buffer de profundidad se utiliza para recrear la posición en el mundo. Este es el patrón estándar para los sombreadores Witcher 3.

Tener datos de oclusión ambiental (si está habilitado) nos permite ocultar la niebla. Una idea muy inteligente, quizás obvia, pero nunca lo pensé de esa manera. Volveré sobre este aspecto más tarde.

Un sombreador comienza determinando si hay un píxel en el cielo. En caso de que el píxel se encuentre en el cielo (profundidad == 1.0), el sombreador vuelve negro. Si el píxel está en la escena (profundidad <1.0), recreamos la posición en el mundo usando el búfer de profundidad (líneas 7-11) y continuamos calculando la niebla.

El paso de la niebla ocurre poco después del proceso de sombreado retrasado. Puede notar que algunos elementos relacionados con la ejecución directa aún no están disponibles. En esta escena en particular, se aplicaron volúmenes de iluminación diferidos, y luego mostramos el cabello / rostro / ojos de Geralt.

Lo primero que debe saber sobre la niebla en "The Witcher 3": consta de dos partes: "color de la niebla" y "color de la atmósfera".

 struct FogResult  
 {  
    float4 paramsFog;     // RGB: color, A: influence  
    float4 paramsAerial;  // RGB: color, A: influence  
 };

Para cada parte hay tres colores: delantero, medio y trasero. Es decir, en el búfer constante hay datos como "FogColorFront", "FogColorMiddle", "AerialColorBack", etc. ... Veamos los datos entrantes:


   // *** Inputs *** //  
   float3 FogSunDir = cb12_v38.xyz;  
   float3 FogColorFront = cb12_v39.xyz;  
   float3 FogColorMiddle = cb12_v40.xyz;  
   float3 FogColorBack = cb12_v41.xyz;  
     
   float4 FogBaseParams = cb12_v42;  
   float4 FogDensityParamsScene = cb12_v43;  
   float4 FogDensityParamsSky = cb12_v44;  
     
   float3 AerialColorFront = cb12_v45.xyz;  
   float3 AerialColorMiddle = cb12_v46.xyz;  
   float3 AerialColorBack = cb12_v47.xyz;  
   float4 AerialParams = cb12_v48;

Antes de calcular los colores finales, necesitamos calcular los vectores y productos escalares. El sombreador tiene acceso a la posición de píxeles en el mundo, la posición de la cámara (cb12 [0] .xyz) y la dirección de niebla / iluminación (cb12 [38] .xyz). Esto nos permite calcular el producto escalar del vector de la forma y dirección de la niebla.

   float3 frag_vec = fragPosWorldSpace.xyz - customCameraPos.xyz;  
   float frag_dist = length(frag_vec);  
     
   float3 frag_dir = frag_vec / frag_dist;  
   
   float dot_fragDirSunDir = dot(GlobalLightDirection.xyz, frag_dir);

Para calcular el gradiente de mezcla, debe usar el cuadrado del producto escalar absoluto y luego multiplicar nuevamente el resultado por algún parámetro que dependa de la distancia:

   float3 curr_col_fog;  
   float3 curr_col_aerial;  
   {  
     float _dot = dot_fragDirSunDir;  
   
     float _dd = _dot;  
     {  
       const float _distOffset = -150;  
       const float _distRange = 500;  
       const float _mul = 1.0 / _distRange;  
       const float _bias = _distOffset * _mul;  
   
       _dd = abs(_dd);  
       _dd *= _dd;  
       _dd *= saturate( frag_dist * _mul + _bias );  
     }  
   
     curr_col_fog = lerp( FogColorMiddle.xyz, (_dot>0.0f ? FogColorFront.xyz : FogColorBack.xyz), _dd );  
     curr_col_aerial = lerp( AerialColorMiddle.xyz, (_dot>0.0f ? AerialColorFront.xyz : AerialColorBack.xyz), _dd );  
   }

Este bloque de código nos deja claro de dónde provienen estos 0.002 y -0.300. Como podemos ver, el producto escalar entre los vectores de visión e iluminación es responsable de la elección entre los colores "frontal" y "posterior". ¡Inteligente!

Aquí hay una visualización del gradiente final resultante (_dd).


Sin embargo, calcular el efecto de la atmósfera / niebla es mucho más complicado. Como puede ver, tenemos muchas más opciones que solo colores rgb. Incluyen, por ejemplo, la densidad de la escena. Usamos raymarching (16 pasos, y es por eso que el ciclo se puede expandir) para determinar el tamaño de la niebla y el factor de escala: al

tener un vector [cámara ---> mundo], podemos dividir todos sus componentes en 16; este será un paso de raymarching. Como vemos a continuación, solo el componente .z (altura) ( curr_pos_z_step ) está involucrado en los cálculos .

Lea más sobre la niebla implementada por raymarching, por ejemplo, aquí .

   float fog_amount = 1;  
   float fog_amount_scale = 0;  
   [branch]  
   if ( frag_dist >= AerialParams.y )  
   {  
     float curr_pos_z_base = (customCameraPos.z + FogBaseParams.y) * density_factor;  
     float curr_pos_z_step = frag_step.z * density_factor;  
   
     [unroll]  
     for ( int i=16; i>0; --i )  
     {  
       fog_amount *= 1 - saturate( density_sample_scale / (1 + max( 0.0, curr_pos_z_base + (i) * curr_pos_z_step ) ) );  
     }  
   
     fog_amount = 1 - fog_amount;  
     fog_amount_scale = saturate( (frag_dist - AerialParams.y) * AerialParams.z );  
   }  
   
   FogResult ret;  
   
   ret.paramsFog = float4 ( curr_col_fog, fog_amount_scale * pow( abs(fog_amount), final_exp_fog ) );  
   ret.paramsAerial = float4 ( curr_col_aerial, fog_amount_scale * pow( abs(fog_amount), final_exp_aerial ) );

La cantidad de niebla obviamente depende de la altura (componentes .z), al final la cantidad de niebla se eleva al grado de niebla / atmósfera.

final_exp_fog y final_exp_aerial se toman del búfer constante; te permiten controlar cómo los colores de la niebla y la atmósfera afectan al mundo con una altitud creciente.

Anulación de niebla


El sombreador que encontré no tenía el siguiente fragmento de código de ensamblaje:

  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, l(-1.000000), cb12[188].w  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.w, r0.z  
  115:  lt r0.z, l(0.000000), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, l(-1.000000), cb12[190].w  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.w, r0.z  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif

A juzgar por lo que pude entender, esto es como redefinir el color y el efecto de la niebla: la

mayoría de las veces, solo se realiza una redefinición (cb12_v192.x es 0.0), pero en este caso particular su valor es ~ 0.22, por lo que hacemos la segunda anulación.


 #ifdef OVERRIDE_FOG  
     
   // Override  
   float fog_influence = ret.paramsFog.w; // r0.w  
   
   float override1ColorScale = cb12_v189.x;  
   float override1ColorBias = cb12_v189.y;  
   float3 override1Color = cb12_v188.rgb;  
     
   float override1InfluenceScale = cb12_v189.z;  
   float override1InfluenceBias = cb12_v189.w;  
   float override1Influence = cb12_v188.w;  
     
   float override1ColorAmount = saturate(fog_influence * override1ColorScale + override1ColorBias);  
   float override1InfluenceAmount = saturate(fog_influence * override1InfluenceScale + override1InfluenceBias);    
     

   float4 paramsFogOverride;  
   paramsFogOverride.rgb = lerp(curr_col_fog, override1Color, override1ColorAmount ); // ***r5.xyz   
     
   float param1 = lerp(1.0, override1Influence, override1InfluenceAmount); // r0.x  
   paramsFogOverride.w = saturate(param1 * fog_influence ); // ** r5.w  
   
     
   const float extraFogOverride = cb12_v192.x;  
     
   [branch]   
   if (extraFogOverride > 0.0)  
   {  
     float override2ColorScale = cb12_v191.x;  
     float override2ColorBias = cb12_v191.y;  
     float3 override2Color = cb12_v190.rgb;  
     
     float override2InfluenceScale = cb12_v191.z;  
     float override2InfluenceBias = cb12_v191.w;  
     float override2Influence = cb12_v190.w;  
       
     float override2ColorAmount = saturate(fog_influence * override2ColorScale + override2ColorBias);  
     float override2InfluenceAmount = saturate(fog_influence * override2InfluenceScale + override2InfluenceBias);  
      

     float4 paramsFogOverride2;  
     paramsFogOverride2.rgb = lerp(curr_col_fog, override2Color, override2ColorAmount); // r3.xyz   
           
     float ov_param1 = lerp(1.0, override2Influence, override2InfluenceAmount); // r0.z  
     paramsFogOverride2.w = saturate(ov_param1 * fog_influence); // r3.w  
   
     paramsFogOverride = lerp(paramsFogOverride, paramsFogOverride2, extraFogOverride);  
   
   }  
   ret.paramsFog = paramsFogOverride;  
     
 #endif

Aquí está nuestro precio final sin redefinir la niebla (primera imagen), con una redefinición (segunda imagen) y doble redefinición (tercera imagen, resultado final):




Regulación de la oclusión ambiental.


El sombreador que encontré tampoco usaba oclusión ambiental en absoluto. Echemos un vistazo a la textura de AO nuevamente y al código que nos interesa:


  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)

Quizás esta escena no sea el mejor ejemplo, porque no vemos los detalles en una isla distante. Sin embargo, echemos un vistazo al búfer constante, que se utiliza para establecer el valor de oclusión ambiental:


Comenzamos cargando AO desde la textura, luego ejecutamos la instrucción max. En esta escena, cb3_v1.x es muy alto (0.96888), lo que hace que el AO sea muy débil.

La siguiente parte del código calcula la distancia entre las posiciones de la cámara y los píxeles en el mundo.

Creo que el código a veces habla por sí mismo, así que echemos un vistazo a HLSL, que hace la mayor parte de esta configuración:

 float AdjustAmbientOcclusion(in float inputAO, in float worldToCameraDistance)  
 {  
   // *** Inputs *** //  
   const float aoDistanceStart = cb3_v0.x;  
   const float aoDistanceEnd = cb3_v0.y;  
   const float aoStrengthStart = cb3_v0.z;  
   const float aoStrengthEnd = cb3_v0.w;  
      
   // * Adjust AO  
   float aoDistanceIntensity = linstep( aoDistanceStart, aoDistanceEnd, worldToCameraDistance );  
   float aoStrength = lerp(aoStrengthStart, aoStrengthEnd, aoDistanceIntensity);   
   float adjustedAO = lerp(1.0, inputAO, aoStrength);  
     
   return adjustedAO;   
 }

La distancia calculada entre la cámara y el mundo se usa para la función linstep. Ya conocemos esta función, apareció en el sombreador de nubes cirrus.

Como puede ver, en el búfer constante tenemos los valores de distancia de inicio / fin de AO. La salida de linstep afecta la fuerza del AO (así como también de cbuffer), y la fuerza afecta la salida del AO.

Un breve ejemplo: el píxel está lejos, por ejemplo, la distancia es 500.

linstep devuelve 1.0;
aoStrength es igual a aoStrengthEnd;

Esto da como resultado un retorno de AO, que es aproximadamente el 77% (fuerza final) del valor de entrada.

El AO entrante para esta función se sometió previamente a la operación máxima.

Poniendolo todo junto


Después de haber recibido el color y el efecto del color de la niebla y el color de la atmósfera, finalmente puede combinarlos.

Comenzamos atenuando el efecto con el AO resultante:

   ...
   FogResult fog = CalculateFog( worldPos, CameraPosition, fogStart, ao, false );  
      
   // Apply AO to influence  
   fog.paramsFog.w *= ao;  
   fog.paramsAerial.w *= ao; 
      
   // Mix fog with scene color  
   outColor = ApplyFog(fog, colorHDR);

Toda la magia ocurre en la función ApplyFog :

 float3 ApplyFog(FogResult fog, float3 color)  
 {  
   const float3 LuminanceFactors = float3(0.333f, 0.555f, 0.222f);  
   
   float3 aerialColor = dot(LuminanceFactors, color) * fog.paramsAerial.xyz;  
   color = lerp(color, aerialColor, fog.paramsAerial.w);  
   color = lerp(color, fog.paramsFog.xyz, fog.paramsFog.w);  
    
   return color.xyz;  
 }

Primero, calculamos la luminosidad de los píxeles:


Luego lo multiplicamos por el color de la atmósfera:


Luego combinamos el color HDR con el color de la atmósfera:


El último paso es combinar el resultado intermedio con el color de la niebla:


¡Eso es todo!

Algunas capturas de pantalla de depuración



Efecto atmosférico


Color de la atmósfera


Efecto de niebla


Color de la niebla


Escena terminada sin niebla


Escena preparada solo con niebla


La escena terminada es solo la niebla principal


Escena preparada nuevamente con toda la niebla para facilitar la comparación

Total


Creo que puedes entender mucho de lo anterior, si miras el sombreador, está aquí .

Puedo decir con placer que este sombreador es exactamente el mismo que el original, me hace muy feliz.

En general, el resultado final depende en gran medida de los valores pasados ​​al sombreador. Esta no es una solución "mágica" que proporciona colores perfectos en la salida, requiere muchas iteraciones y artistas para que el resultado final se vea decente. Creo que puede ser un proceso largo, pero después de completarlo, el resultado será muy convincente, al igual que esta escena al atardecer.

El Witcher 3 Sky Shader también utiliza cálculos de niebla para crear una transición suave de colores cerca del horizonte. Sin embargo, un conjunto diferente de coeficientes de densidad se pasa al sombreador del cielo.

Permítame recordarle que la mayoría de este sombreador no fue creado / analizado por mí. Todos los reconocimientos deben enviarse a CD PROJEKT RED. Apóyalos, hacen un excelente trabajo.

Parte 3. Estrellas fugaces


En The Witcher 3 hay un pequeño pero curioso detalle: las estrellas fugaces. Curiosamente, no parecen estar en el DLC Blood and Wine.

En el video puedes ver cómo se ven:


Veamos cómo logramos este efecto.

Como puede ver, el cuerpo de una estrella fugaz es mucho más brillante que la cola. Esta es una propiedad importante que usaremos más adelante.

Nuestra agenda es bastante familiar: primero describiré las propiedades generales, luego hablaré sobre temas relacionados con la geometría, y al final pasaremos al sombreador de píxeles, donde suceden las cosas más interesantes.

1. Descripción general


Describa brevemente lo que está sucediendo.

Las estrellas fugaces se dibujan en un pasaje proactivo, inmediatamente después de la cúpula del cielo, el cielo y la luna:



DrawIndexed (720) - la cúpula del cielo,
DrawIndexed (2160) - la esfera del cielo / luna,
DrawIndexed (36) - es irrelevante, parece un paralelepípedo de la oclusión del sol (?)
DrawIndexed (12) - la estrella fugaz
DrawIndexedInstanced (1116, 1) - Cirros

Como los cirros , cada estrella fugaz se dibuja dos veces seguidas.


Antes del primer sorteo


Resultado del primer sorteo


Resultado del segundo sorteo

Además, como en muchos elementos del pase preventivo de este juego, se usa el siguiente estado de mezcla:


2. Geometría


En términos de geometría, lo primero que hay que mencionar es que cada estrella fugaz está representada por un quad delgado con códigos de texto: 4 vértices, 6 índices. Este es el quad más simple posible.


Cuádruple


aproximado de una estrella fugaz. Puede ver la visualización de la estructura alámbrica de una línea que denota dos triángulos.

¡Espere un minuto , pero hay DrawIndexed (12) ! ¿Significa esto que dibujamos dos estrellas fugaces al mismo tiempo?

Si.


En este marco, una de las estrellas fugaces está completamente fuera de la pirámide de visibilidad.

Veamos el código del ensamblador para el sombreador de vértices:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[193], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xyzw  
    dcl_input v2.xy  
    dcl_input v3.xy  
    dcl_input v4.xy  
    dcl_input v5.xyz  
    dcl_input v6.x  
    dcl_input v7.x  
    dcl_output o0.xyzw  
    dcl_output o1.xyzw  
    dcl_output o2.xy  
    dcl_output o3.xyzw  
    dcl_output_siv o4.xyzw, position  
    dcl_temps 5  
   0: mov r0.xyz, v0.xyzx  
   1: mov r0.w, l(1.000000)  
   2: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   3: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   4: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   5: add r0.x, v2.x, v2.y  
   6: add r0.y, -v2.y, v2.x  
   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx  
  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx  
  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx  
  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000)  
  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  
  30: add r0.xyz, r0.xyzx, -cb12[0].xyzx  
  31: dp3 r0.w, r0.xyzx, r0.xyzx  
  32: sqrt r0.w, r0.w  
  33: div r0.xyz, r0.xyzx, r0.wwww  
  34: add r0.w, r0.w, -cb12[22].z  
  35: max r0.w, r0.w, l(0)  
  36: min r0.w, r0.w, cb12[42].z  
  37: dp3 r0.x, cb12[38].xyzx, r0.xyzx  
  38: mul r0.y, abs(r0.x), abs(r0.x)  
  39: mad_sat r1.x, r0.w, l(0.002000), l(-0.300000)  
  40: mul r0.y, r0.y, r1.x  
  41: lt r1.x, l(0), r0.x  
  42: movc r1.yzw, r1.xxxx, cb12[39].xxyz, cb12[41].xxyz  
  43: add r1.yzw, r1.yyzw, -cb12[40].xxyz  
  44: mad r1.yzw, r0.yyyy, r1.yyzw, cb12[40].xxyz  
  45: movc r2.xyz, r1.xxxx, cb12[45].xyzx, cb12[47].xyzx  
  46: add r2.xyz, r2.xyzx, -cb12[46].xyzx  
  47: mad o0.xyz, r0.yyyy, r2.xyzx, cb12[46].xyzx  
  48: ge r0.y, r0.w, cb12[48].y  
  49: if_nz r0.y  
  50:  mad r0.y, r0.z, cb12[22].z, cb12[0].z  
  51:  mul r0.z, r0.w, r0.z  
  52:  mul r0.z, r0.z, l(0.062500)  
  53:  mul r1.x, r0.w, cb12[43].x  
  54:  mul r1.x, r1.x, l(0.062500)  
  55:  add r0.x, r0.x, cb12[42].x  
  56:  add r2.x, cb12[42].x, l(1.000000)  
  57:  div_sat r0.x, r0.x, r2.x  
  58:  add r2.x, -cb12[43].z, cb12[43].y  
  59:  mad r0.x, r0.x, r2.x, cb12[43].z  
  60:  add r0.y, r0.y, cb12[42].y  
  61:  mul r2.x, r0.x, r0.y  
  62:  mul r0.z, r0.x, r0.z  
  63:  mad r3.xyzw, r0.zzzz, l(16.000000, 15.000000, 14.000000, 13.000000), r2.xxxx  
  64:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  65:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  67:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  68:  mul r2.y, r3.y, r3.x  
  69:  mul r2.y, r3.z, r2.y  
  70:  mul r2.y, r3.w, r2.y  
  71:  mad r3.xyzw, r0.zzzz, l(12.000000, 11.000000, 10.000000, 9.000000), r2.xxxx  
  72:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  73:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  74:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  75:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  76:  mul r2.y, r2.y, r3.x  
  77:  mul r2.y, r3.y, r2.y  
  78:  mul r2.y, r3.z, r2.y  
  79:  mul r2.y, r3.w, r2.y  
  80:  mad r3.xyzw, r0.zzzz, l(8.000000, 7.000000, 6.000000, 5.000000), r2.xxxx  
  81:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  82:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  83:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  84:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  85:  mul r2.y, r2.y, r3.x  
  86:  mul r2.y, r3.y, r2.y  
  87:  mul r2.y, r3.z, r2.y  
  88:  mul r2.y, r3.w, r2.y  
  89:  mad r2.zw, r0.zzzz, l(0.000000, 0.000000, 4.000000, 3.000000), r2.xxxx  
  90:  max r2.zw, r2.zzzw, l(0, 0, 0, 0)  
  91:  add r2.zw, r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  92:  div_sat r2.zw, r1.xxxx, r2.zzzw  
  93:  add r2.zw, -r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  94:  mul r2.y, r2.z, r2.y  
  95:  mul r2.y, r2.w, r2.y  
  96:  mad r2.x, r0.z, l(2.000000), r2.x  
  97:  max r2.x, r2.x, l(0)  
  98:  add r2.x, r2.x, l(1.000000)  
  99:  div_sat r2.x, r1.x, r2.x  
  100:  add r2.x, -r2.x, l(1.000000)  
  101:  mul r2.x, r2.x, r2.y  
  102:  mad r0.x, r0.y, r0.x, r0.z  
  103:  max r0.x, r0.x, l(0)  
  104:  add r0.x, r0.x, l(1.000000)  
  105:  div_sat r0.x, r1.x, r0.x  
  106:  add r0.x, -r0.x, l(1.000000)  
  107:  mad r0.x, -r2.x, r0.x, l(1.000000)  
  108:  add r0.y, r0.w, -cb12[48].y  
  109:  mul_sat r0.y, r0.y, cb12[48].z  
  110: else  
  111:  mov r0.xy, l(1.000000, 0.000000, 0.000000, 0.000000)  
  112: endif  
  113: log r0.x, r0.x  
  114: mul r0.z, r0.x, cb12[42].w  
  115: exp r0.z, r0.z  
  116: mul r0.z, r0.z, r0.y  
  117: mul r0.x, r0.x, cb12[48].x  
  118: exp r0.x, r0.x  
  119: mul o0.w, r0.x, r0.y  
  120: mad_sat r0.xy, r0.zzzz, cb12[189].xzxx, cb12[189].ywyy  
  121: add r2.xyz, -r1.yzwy, cb12[188].xyzx  
  122: mad r2.xyz, r0.xxxx, r2.xyzx, r1.yzwy  
  123: add r0.x, cb12[188].w, l(-1.000000)  
  124: mad r0.x, r0.y, r0.x, l(1.000000)  
  125: mul_sat r2.w, r0.x, r0.z  
  126: lt r0.x, l(0), cb12[192].x  
  127: if_nz r0.x  
  128:  mad_sat r0.xy, r0.zzzz, cb12[191].xzxx, cb12[191].ywyy  
  129:  add r3.xyz, -r1.yzwy, cb12[190].xyzx  
  130:  mad r1.xyz, r0.xxxx, r3.xyzx, r1.yzwy  
  131:  add r0.x, cb12[190].w, l(-1.000000)  
  132:  mad r0.x, r0.y, r0.x, l(1.000000)  
  133:  mul_sat r1.w, r0.x, r0.z  
  134:  add r0.xyzw, -r2.xyzw, r1.xyzw  
  135:  mad o1.xyzw, cb12[192].xxxx, r0.xyzw, r2.xyzw  
  136: else  
  137:  mov o1.xyzw, r2.xyzw  
  138: endif  
  139: mov o3.xyzw, v1.xyzw  
  140: mov o2.xy, v4.yxyy  
  141: ret

Aquí, el cálculo de la niebla puede llamar inmediatamente la atención (líneas 30-138). El cálculo de la capa superior de niebla tiene sentido por razones de rendimiento. Además, no necesitamos tanta precisión de niebla: los meteoritos generalmente vuelan sobre la cabeza de Geralt y no alcanzan el horizonte.

Los parámetros atmosféricos (rgb = color, a = influencia) se almacenan en o0.xyzw, y los parámetros de niebla en o1.xyzw.

o2.xy (línea 140) es solo texcoords.
o3.xyzw (línea 139) es irrelevante.

Ahora digamos algunas palabras sobre el cálculo de una posición en el mundo. Los sombreadores de vértices realizan vallas publicitarias . En primer lugar, los datos entrantes para vallas publicitarias provienen del búfer de vértices; echemos un vistazo a ellos.

El primer dato es Posición:


Como se mencionó anteriormente, aquí tenemos 2 quad-a: 8 vértices, 12 índices.

Pero, ¿por qué la posición es la misma para cada quad? Muy simple: esta es la posición del centro del quad.

Además, cada vértice tiene un desplazamiento desde el centro hasta el borde del quad:


Esto significa que cada estrella fugaz tiene un tamaño de (400, 3) unidades en el espacio mundial. (en el plano XY, en Witcher 3, el eje Z se dirige hacia arriba)

El último elemento que tiene cada vértice es un vector de dirección unitario en el espacio mundial que controla el movimiento de una estrella fugaz:


Como los datos provienen de la CPU, es difícil entender cómo se calculan.

Ahora pasemos al código de la cartelera. La idea es bastante simple: primero obtienes un vector unitario desde el centro del quad hasta la cámara:

   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx

Luego obtenemos un solo vector tangente que controla el movimiento de la estrella fugaz.

Dado que este vector ya está normalizado en el lado de la CPU, esta normalización es redundante.

  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx

Si hay dos vectores, se usa un producto vectorial para determinar el vector bi-tangente perpendicular a ambos vectores entrantes.

  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx

Ahora tenemos vectores normalizados tangente (r3.xyz) y bitangente (r2.xyz).

Introduzcamos xsize y ysize correspondiente al elemento de entrada TEXCOORD1, así por ejemplo (-200, 1,50).

El cálculo final de la posición en el espacio mundial se realiza de la siguiente manera:

  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000) 

Dado que r0.x, r0.y y r0.z son iguales a 1.0, el cálculo final se simplifica:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

la última parte es una simple multiplicación de una posición en el espacio mundial por una matriz de vista-proyección para obtener SV_Position:

  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  

3. Pixel Shader


Como se indicó en la sección Descripción general, se utiliza el siguiente estado de fusión: donde SrcColor y SrcAlpha son los componentes .rgb y .a del sombreador de píxeles, respectivamente , y DestColor es el color .rgb actualmente en rendertarget. El indicador principal que controla la transparencia es SrcAlpha . Muchos sombreadores de juegos proactivos lo calculan como opacidad y lo aplican al final de la siguiente manera: El sombreador de estrellas fugaces no fue la excepción. Siguiendo este patrón, consideramos tres casos en los que la opacidad es 1.0, 0.1 y 0.0.

FinalColor = SrcColor * One + DestColor * (1.0 - SrcAlpha) =
FinalColor = SrcColor + DestColor * (1.0 - SrcAlpha)






return float4( color * opacity, opacity )



a) opacity = 1.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = color = SrcColor



b) opacity = 0.1

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = 0.1 * color + 0.9 * DestColor



c) opacity = 0.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = DestColor



La idea subyacente de este sombreador es modelar y usar la función de opacidad opacidad (x) , que controla la opacidad de un píxel a lo largo de una estrella fugaz. El requisito principal es que la opacidad alcance los valores máximos al final de la estrella (su "cuerpo") y se desvanezca suavemente a 0.0 (a su "cola").

Cuando comenzamos a comprender el código de ensamblador del sombreador de píxeles, esto se vuelve obvio:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[2], immediateIndexed  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.y  
    dcl_input_ps linear v3.w  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mov_sat r0.x, v2.y  
   1: ge r0.y, r0.x, l(0.052579)  
   2: ge r0.z, l(0.965679), r0.x  
   3: and r0.y, r0.z, r0.y  
   4: if_nz r0.y  
   5:  ge r0.y, l(0.878136), r0.x  
   6:  add r0.z, r0.x, l(-0.052579)  
   7:  mul r1.w, r0.z, l(1.211303)  
   8:  mov_sat r0.z, r1.w  
   9:  mad r0.w, r0.z, l(-2.000000), l(3.000000)  
  10:  mul r0.z, r0.z, r0.z  
  11:  mul r0.z, r0.z, r0.w  
  12:  mul r2.x, r0.z, l(0.084642)  
  13:  mov r1.yz, l(0.000000, 0.000000, 0.084642, 0.000000)  
  14:  movc r2.yzw, r0.yyyy, r1.yyzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  15:  not r0.z, r0.y  
  16:  if_z r0.y  
  17:   ge r0.y, l(0.924339), r0.x  
  18:   add r0.w, r0.x, l(-0.878136)  
  19:   mul r1.w, r0.w, l(21.643608)  
  20:   mov_sat r0.w, r1.w  
  21:   mad r3.x, r0.w, l(-2.000000), l(3.000000)  
  22:   mul r0.w, r0.w, r0.w  
  23:   mul r0.w, r0.w, r3.x  
  24:   mad r1.x, r0.w, l(0.889658), l(0.084642)  
  25:   mov r1.yz, l(0.000000, 0.084642, 0.974300, 0.000000)  
  26:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  27:  else  
  28:   mov r2.y, l(0)  
  29:   mov r0.y, l(-1)  
  30:  endif  
  31:  not r0.w, r0.y  
  32:  and r0.z, r0.w, r0.z  
  33:  if_nz r0.z  
  34:   ge r0.y, r0.x, l(0.924339)  
  35:   add r0.x, r0.x, l(-0.924339)  
  36:   mul r1.w, r0.x, l(24.189651)  
  37:   mov_sat r0.x, r1.w  
  38:   mad r0.z, r0.x, l(-2.000000), l(3.000000)  
  39:   mul r0.x, r0.x, r0.x  
  40:   mul r0.x, r0.x, r0.z  
  41:   mad r1.x, r0.x, l(-0.974300), l(0.974300)  
  42:   mov r1.yz, l(0.000000, 0.974300, 0.000000, 0.000000)  
  43:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  44:  endif  
  45: else  
  46:  mov r2.yzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  47:  mov r0.y, l(0)  
  48: endif  
  49: mov_sat r2.w, r2.w  
  50: mad r0.x, r2.w, l(-2.000000), l(3.000000)  
  51: mul r0.z, r2.w, r2.w  
  52: mul r0.x, r0.z, r0.x  
  53: add r0.z, -r2.y, r2.z  
  54: mad r0.x, r0.x, r0.z, r2.y  
  55: movc r0.x, r0.y, r2.x, r0.x  
  56: mad r0.y, cb4[1].x, -cb0[9].w, l(1.000000)  
  57: mul_sat r0.y, r0.y, v3.w  
  58: mul r0.x, r0.y, r0.x  
  59: mul r0.yzw, cb2[2].xxyz, cb4[0].xxxx  
  60: mul r0.x, r0.x, cb2[2].w  
  61: dp3 r1.x, l(0.333000, 0.555000, 0.222000, 0.000000), r0.yzwy  
  62: mad r1.xyz, r1.xxxx, v0.xyzx, -r0.yzwy  
  63: mad r0.yzw, v0.wwww, r1.xxyz, r0.yyzw  
  64: add r1.xyz, -r0.yzwy, v1.xyzx  
  65: mad r0.yzw, v1.wwww, r1.xxyz, r0.yyzw  
  66: mul o0.xyz, r0.xxxx, r0.yzwy  
  67: mov o0.w, r0.x  
  68: ret

En general, el sombreador es un poco complicado y fue difícil para mí descubrir qué estaba pasando. Por ejemplo, ¿de dónde provienen todos los valores como 1.211303, 21.643608 y 24.189651?

Si estamos hablando de la función de opacidad, entonces necesitamos un valor de entrada. Con esto, es bastante simple: texcoord en el rango de [0,1] (línea 0) será útil aquí, para que podamos aplicar la función a toda la longitud del meteoroide.

La función de opacidad tiene tres segmentos / intervalos definidos por cuatro puntos de control:

   // current status: no idea how these are generated  
   const float controlPoint0 = 0.052579;  
   const float controlPoint1 = 0.878136;  
   const float controlPoint2 = 0.924339;  
   const float controlPoint3 = 0.965679;

No tengo idea de cómo fueron seleccionados / calculados.

Como podemos ver en el código del ensamblador, la primera condición es simplemente verificar si el valor de entrada está en el rango [controlPoint0 - controlPoint3]. Si no, entonces la opacidad es solo 0.0.

   // Input for the opacity function
   float y = saturate(Input.Texcoords.y);  // r0.x
     
   // Value of opacity function.  
   // 0 - no change  
   // 1 - full color  
   float opacity = 0.0;  
     
   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
      ...

El descifrado del código del ensamblador a continuación es necesario si queremos entender cómo funciona la función de opacidad:

   6: add r0.z, r0.x, l(-0.052579)   
   7: mul r1.w, r0.z, l(1.211303)   
   8: mov_sat r0.z, r1.w   
   9: mad r0.w, r0.z, l(-2.000000), l(3.000000)   
  10: mul r0.z, r0.z, r0.z   
  11: mul r0.z, r0.z, r0.w   
  12: mul r2.x, r0.z, l(0.084642)

La línea 9 tiene los coeficientes '-2.0' y '3.0', lo que sugiere el uso de la función smoothstep . Sí, esta es una buena suposición.

La función HLSL smoothstep con prototipo: ret smoothstep (min, max, x) siempre limita x a [ min-max ]. Desde el punto de vista del ensamblador, esto resta min del valor de entrada (es decir, de r0.z en la línea 9), pero no hay nada de eso en el código. Para max, esto implica una multiplicación del valor de entrada, pero no hay nada como 'mul_sat' en el código. En cambio, hay 'mov_sat'. Esto nos dice que las funciones mín. Y máx . De smoothstep son 0 y 1.

Ahora sabemos que xdebe estar en el intervalo [0, 1]. Como se indicó anteriormente, hay tres segmentos en la función de opacidad. Esto sugiere claramente que el código está buscando dónde estamos en el intervalo [segmentoStart-segmentoEnd].

¡La respuesta es la función Linstep!

 float linstep(float min, float max, float v)  
 {  
   return ( (v-min) / (max-min) );  
 }

Por ejemplo, tomemos el primer segmento: [0.052579 - 0.878136]. La resta está en la línea 6. Si reemplazamos la división por multiplicación -> 1.0 / (0.878136 - 0.052579) = 1.0 / 0.825557 = ~ 1.211303.

El resultado de smoothstep está en el rango [0, 1]. La multiplicación en la línea 12 es el peso del segmento. Cada segmento tiene su propio peso, lo que le permite controlar la opacidad máxima de este segmento en particular.

Esto significa que para el primer segmento [0.052579 - 0.878136], la opacidad está en el rango [0 - 0.084642].

Una función HLSL que calcula la opacidad para un segmento arbitrario se puede escribir de la siguiente manera:

 float getOpacityFunctionValue(float x, float cpLeft, float cpRight, float weight)  
 {  
   float val = smoothstep( 0, 1, linstep(cpLeft, cpRight, x) );  
   return val * weight;  
 }

Entonces, el punto es simplemente llamar a esta función para el segmento correspondiente.

Echa un vistazo a los pesos:

   const float weight0 = 0.084642;  
   const float weight1 = 0.889658;  
   const float weight2 = 0.974300; // note: weight0+weight1 = weight2

Según el código del ensamblador, la función de opacidad (x) se calcula de la siguiente manera:

   float opacity = 0.0;

   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
     // Range of v: [0, weight0]  
     float v = getOpacityFunctionValue(y, controlPoint0, controlPoint1, weight0);  
     opacity = v;  
     
     [branch]  
     if ( y >= controlPoint1 )  
     {  
       // Range of v: [0, weight1]  
       float v = getOpacityFunctionValue(y, controlPoint1, controlPoint2, weight1);  
       opacity = weight0 + v;  
   
       [branch]  
       if (y >= controlPoint2)  
       {  
         // Range of v: [0, weight2]  
         float v = getOpacityFunctionValue(y, controlPoint2, controlPoint3, weight2);
         opacity = weight2 - v;          
       }  
     }  
   }

Aquí hay un gráfico de la función de opacidad. Puede ver fácilmente un fuerte aumento de la opacidad, lo que indica el comienzo del cuerpo de una estrella fugaz:


Función de opacidad gráfica.

Canal rojo - valor de opacidad.
Canal verde - puntos de control.
Canal azul - pesos.


Después de calcular la opacidad, todo lo demás son solo los toques finales. Luego hay multiplicaciones adicionales: la opacidad de las estrellas, el color de la estrella fugaz y la influencia de la niebla. Como es habitual en los sombreadores TW3, también puede encontrar multiplicaciones redundantes por 1.0 aquí:

   // cb4_v1.x = 1.0  
   float starsOpacity = 1.0 - cb0_v9.w * cb4_v1.x;    
   opacity *= starsOpacity;  

   // Calculate color of a shooting star  
   // cb4_v0.x = 10.0
   // cb2_v2.rgb = (1.0, 1.0, 1.0)
   float3 color = cb2_v2.rgb * cb4_v0.x;
     
   // cb2_v2.w = 1  
   opacity *= cb2_v2.w;
     
   FogResult fr = { Input.FogParams, Input.AerialParams };  
   color = ApplyFog(fr, color);
     
   return float4( color*opacity, opacity);  
 }

4. Resumen


La principal dificultad radica en la parte con la función de opacidad. Después de decodificarlo, todo lo demás es bastante simple de entender.

Dije anteriormente que el sombreador de píxeles es un poco complicado. De hecho, solo nos importa el valor de la función de opacidad (x) , que se almacena en r2.x (comenzando en la línea 49). Sin embargo, la función de opacidad en el código del ensamblador crea tres variables adicionales más: minRange (r2.y), maxRange (r2.z) y value (r2.w). Todos ellos son parámetros utilizados para calcular la opacidad cuando no se utiliza la opacidad (x) : de

lerp( minRange, maxRange, smoothstep(0, 1, value) );

hecho, el valor de opacidad final se obtiene en la rama condicional en la línea 55, si el valor de entrada es xestá en el rango [controlPoint0 - controlPoint3], esto significa que se utiliza la función de opacidad, por lo tanto, se selecciona r2.x. De lo contrario, cuando x está fuera del intervalo, la opacidad se calcula a partir de r0.x, es decir, de acuerdo con la ecuación anterior.

Depuré algunos píxeles fuera del intervalo [controlPoint0 - controlPoint3], y la opacidad final siempre resultó ser cero.

Eso es todo por hoy. Y como siempre, gracias por leer.

All Articles