A engenharia reversa da renderização de The Witcher 3: vários efeitos do céu

imagem

[Partes anteriores da análise: primeira e segunda e terceira .]

Parte 1. Nuvens Cirrus


Quando o jogo acontece em espaços abertos, um dos fatores que determinam a credibilidade do mundo é o céu. Pense nisso - na maioria das vezes o céu literalmente ocupa cerca de 40-50% da tela inteira. O céu é muito mais do que um belo gradiente. Tem estrelas, o sol, a lua e, finalmente, nuvens.

Embora as tendências atuais pareçam consistir na renderização volumétrica de nuvens usando raymarching (consulte este artigo ), as nuvens em The Witcher 3 são completamente baseadas em textura. Eu já os examinei antes, mas acabou que com eles tudo é mais complicado do que eu esperava inicialmente. Se você seguiu minha série de artigos, sabe que há uma diferença entre os DLCs Blood e Wine e o resto do jogo. E, como você pode imaginar, há algumas mudanças no trabalho com nuvens no DLC.

O Witcher 3 tem várias camadas de nuvens. Dependendo do clima, podem ser apenas nuvens cirros , nuvens cumulus altas , possivelmente algumas nuvens da família de nuvens em camadas (por exemplo, durante uma tempestade). No final, pode não haver nuvens.

Algumas camadas diferem em termos de texturas e shaders usados ​​para renderizá-las. Obviamente, isso afeta a complexidade e o comprimento do código do assembler para o pixel shader.

Apesar de toda essa diversidade, existem alguns padrões comuns que podem ser observados ao renderizar nuvens no Witcher 3. Primeiro, todos são renderizados de forma proativa, e essa é a escolha perfeita. Todos eles usam mixagem (veja abaixo). Isso facilita muito o controle de como uma camada separada cobre o céu - isso é afetado pelo valor alfa do sombreador de pixels.


Mais interessante, algumas camadas são renderizadas duas vezes com os mesmos parâmetros.

Depois de analisar o código, escolhi o shader mais curto para (1) provavelmente executar sua engenharia reversa completa, (2) descobrir todos os seus aspectos.

Olhei mais de perto as nuvens cirros de Witcher 3: Blood and Wine.

Aqui está um quadro de exemplo:


Antes de renderizar


Após o primeiro passe de renderização


Após a segunda passagem de renderização

Nesse quadro em particular, as nuvens cirros são a primeira camada na renderização. Como você pode ver, é renderizado duas vezes, o que aumenta seu brilho.

Sombra geométrica e de vértice


Antes do pixel shader, falaremos brevemente sobre os shaders geométricos e de vértice usados. A malha para exibir nuvens é um pouco como uma cúpula de céu comum:


Todos os vértices estão no intervalo [0-1], para centralizar a malha no ponto (0,0,0), a escala e o desvio são usados ​​antes da conversão para o worldViewProj (já conhecemos esse padrão nas partes anteriores da série). No caso das nuvens, a malha se estende fortemente ao longo do plano XY (o eixo Z aponta para cima) para cobrir mais espaço do que a pirâmide de visibilidade. O resultado é o seguinte:


Além disso, a malha possui vetores normais e tangentes. O sombreador de vértice também calcula o vetor bi-tangente pelo produto vetorial - todos os três são exibidos na forma normalizada. Há também um cálculo superior do nevoeiro (sua cor e brilho).

Pixel shader


O código de montagem do pixel shader se parece com o seguinte:

 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 

Duas texturas sem costura são inseridas. Um deles contém um mapa normal (canais xyz ) e uma forma de nuvem (canal a ). O segundo é o ruído para distorcer a forma.


Mapa normal, propriedade vermelha do CD Projekt


Cloud Shape, Property CD Projeto vermelho


Textura de ruído, propriedade do CD Projekt Red

O buffer principal de constantes com parâmetros de nuvem é cb4. Para esse quadro, ele tem os seguintes significados:


Além disso, outros valores de outros amortecedores são usados. Não se preocupe, nós os consideraremos também.

Luz solar invertida na direção Z


A primeira coisa que acontece no shader é o cálculo da direção normalizada da luz solar invertida ao longo do eixo 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 mencionado anteriormente, o eixo Z é direcionado para cima e cb0 [9] é a direção da luz solar. Esse vetor é voltado para o sol - é importante! Você pode verificar isso escrevendo um shader computacional simples que executa um NdotL simples e inserindo-o na passagem adiada do shader.

Amostragem de textura de nuvem


O próximo passo é calcular os cabos de texto para provar a textura da nuvem, descompactar o vetor normal e normalizá-lo.

   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 lidar com isso gradualmente.

Para obter o movimento das nuvens, precisamos de um tempo decorrido em segundos ( cb [0] .x ) multiplicado pelo coeficiente de velocidade, que afeta a rapidez com que as nuvens se movem pelo céu ( cb4 [5] .xy ).

Como eu disse anteriormente, os UVs são esticados ao longo da geometria da cúpula do céu, e também precisamos de fatores de escala de textura que afetam o tamanho das nuvens ( cb4 [4] .xy ).

A fórmula final é:

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

Depois de amostrar todos os 4 canais, temos um mapa normal (canais rgb) e uma forma de nuvem (canal a).

Para descompactar o mapa normal do intervalo [0; 1] no intervalo [-1; 1] usamos a seguinte fórmula:

unpackedNormal = (packedNormal - 0.5) * 2.0;

Você também pode usar isso:

unpackedNormal = packedNormal * 2.0 - 1.0;

Finalmente, normalizamos o vetor normal descompactado.

Overlay normals


Tendo os vetores normais, os vetores tangente e bi-tangente do sombreador de vértice e o vetor normal do mapa normal, normalmente mapeamos os normais.

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

Brilho (1)


Na próxima etapa, o cálculo de NdotL é aplicado e isso afeta a quantidade de iluminação de um pixel específico.

Considere o seguinte código do assembler:

  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  

Aqui está a visualização do NdotL no quadro em questão:


Este produto escalar (com saturação) é usado para interpolar entre minIntensity e maxIntensity. Graças a isso, partes das nuvens iluminadas pela luz solar ficarão mais brilhantes.

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

Brilho (2)


Há outro fator que afeta o brilho das nuvens.

As nuvens localizadas naquela parte do céu onde o sol está devem ser mais destacadas. Para fazer isso, calculamos o gradiente com base no plano XY.

Esse gradiente é usado para calcular a interpolação linear entre os valores min / max, semelhante ao que acontece na parte (1).

Isto é, teoricamente, podemos pedir para escurecer as nuvens localizadas no lado oposto do sol, mas isso não acontece neste quadro particular, porque param2Min e param2Max ( CB4 [0] .x e CB4 [1] .x ) estão marcadas para 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);

No final, multiplicamos os dois brilhos e aumentamos o resultado para uma potência de 2,2.

Cor da nuvem


O cálculo da cor das nuvens começa com a obtenção, a partir das constantes do buffer, de dois valores indicando a cor das nuvens próximas ao sol e as nuvens do lado oposto do céu. Entre eles, a interpolação linear é realizada com base emSkySection .

Em seguida, o resultado é multiplicado por finalIntensity .

E, no final, o resultado é misturado ao nevoeiro (por razões de desempenho, foi calculado pelo sombreador de vértice).

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

Tornar as nuvens de cirros mais visíveis no horizonte


Isso não é muito perceptível no quadro, mas na verdade essa camada é mais visível perto do horizonte do que acima da cabeça de Geralt. Aqui está como fazê-lo.

Você pode notar que, ao calcular o segundo brilho, calculamos o comprimento do vetor 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

Vamos encontrar as seguintes ocorrências desse tamanho no 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

Uau, o que há com a gente?

cb [7] .xe cb [8] .x têm os valores 2000.0 e 7000.0.

Acontece que esse é o resultado do uso da função linstep .

Ela recebe três parâmetros: intervalo mínimo / máximo e valor v .

Isso funciona da seguinte maneira: se v estiver no intervalo [ min - max ], a função retornará interpolação linear no intervalo [0,0 - 1,0]. Por outro lado, se v estiver fora da faixa, o linstep retornará 0,0 ou 1,0.

Um exemplo simples:

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

Ou seja, é bastante semelhante ao passo suave do HLSL, exceto que, neste caso, em vez da interpolação hermitiana, o linear é executado.

O Linstep não é um recurso do HLSL, mas é muito útil. Vale a pena tê-lo em seu kit de ferramentas.

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

Vamos voltar ao Witcher 3: depois de calcular este indicador, que relata a que distância uma parte específica do céu está de Geralt, nós o usamos para enfraquecer o brilho das nuvens:

  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 é o canal .a da primeira textura e closeCloudsHidingFactor é um valor de buffer constante que controla a visibilidade das nuvens acima da cabeça de Geralt. Em todos os quadros que testei, foi igual a 0,0, o que equivale à ausência de nuvens. À medida que distanceAttenuation se aproxima de 1,0 (a distância da câmera até a cúpula do céu aumenta), as nuvens se tornam mais visíveis.

Amostragem de textura de ruído


O cálculo das coordenadas da amostragem do ruído da textura calcula cálculos semelhantes para a textura das nuvens, exceto pelo uso de um conjunto diferente de textureScale e speedMultiplier .

Claro, um sampler com o envoltório modo de endereçamento habilitado é usado para provar todas essas 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;

Juntando tudo


Depois de receber o valor do ruído, devemos combiná-lo com o cloudShape.

Eu tive alguns problemas para entender essas linhas, onde há param2.w (que é sempre 1.0) e noiseMult (tem um valor de 5.0, retirado do buffer constante).

Seja como for, o mais importante aqui é o valor final de generalCloudsVisibility , que afeta a visibilidade das nuvens.

Veja também o valor final do ruído. A cor de saída do cloudsColor é multiplicada pelo ruído final, que também é enviado para o 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


O resultado final parece muito crível.

Você pode comparar. A primeira foto é o meu shader, a segunda é o jogo shader:


Se você estiver curioso, o shader está disponível aqui .

Parte 2. Nevoeiro


O nevoeiro pode ser implementado de várias maneiras. No entanto, os momentos em que poderíamos aplicar um nevoeiro simples , dependente da distância, e eliminá-lo, estavam para sempre no passado (provavelmente). Viver no mundo dos shaders programáveis ​​abriu as portas para novas soluções malucas, mas mais importante, fisicamente precisas e visualmente realistas.

As tendências atuais na renderização de nevoeiro são baseadas em shaders computacionais (para detalhes, consulte esta apresentação de Bart Wronsky).

Apesar do fato de esta apresentação ter aparecido em 2014 e de The Witcher 3 ter sido lançado em 2015/2016, o nevoeiro na última parte das aventuras de Geralt depende completamente da tela e é implementado como um pós-processamento típico.

Antes de começarmos a próxima sessão de engenharia reversa, devo dizer que, no ano passado, tentei descobrir o nevoeiro do Witcher 3 pelo menos cinco vezes, e cada vez falhava. O código do assembler, como você verá em breve, é bastante complicado, e isso torna quase impossível o processo de criação de um shader de neblina legível no HLSL.

No entanto, consegui encontrar um shader de neblina na Internet que imediatamente chamou minha atenção devido à sua semelhança com o nevoeiro The Witcher 3 em termos de nomes de variáveis ​​e ordem geral de instruções. Esse shader não era exatamente o mesmo que no jogo, então tive que refazer um pouco. Quero dizer que a parte principal do código HLSL que você vê aqui foi, com duas exceções, não criada / analisada por mim. Lembre-se disso.

Aqui está o código do assembler para o pixel fog shader - vale a pena notar que é o mesmo para todo o jogo (a parte principal de 2015 e os dois DLCs):

 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, o shader é bastante longo. Provavelmente muito tempo para um processo eficaz de engenharia reversa.

Aqui está um exemplo de uma cena do pôr do sol com neblina:


Vamos dar uma olhada na entrada:

Quanto às texturas, temos um buffer de profundidade, Oclusão ambiental e um buffer de cores HDR.


Buffer de profundidade de entrada


Oclusão ambiental de entrada


O buffer de cores HDR de entrada

... e o resultado da aplicação do shader de neblina nesta cena têm a seguinte aparência:


Textura HDR após a aplicação de neblina O

buffer de profundidade é usado para recriar a posição no mundo. Esse é o padrão padrão para os shaders do Witcher 3.

Ter dados de oclusão do ambiente (se ativado) permite obscurecer a névoa. Uma ideia muito inteligente, talvez óbvia, mas nunca pensei nisso dessa maneira. Voltarei a esse aspecto mais tarde.

Um sombreador começa determinando se um pixel está no céu. Caso o pixel fique no céu (profundidade == 1,0), o sombreador retornará preto. Se o pixel estiver na cena (profundidade <1,0), recriaremos a posição no mundo usando o buffer de profundidade (linhas 7 a 11) e continuaremos a calcular o nevoeiro.

A passagem do nevoeiro ocorre logo após o processo de sombreamento atrasado. Você pode perceber que alguns elementos relacionados à execução futura ainda não estão disponíveis. Nesta cena em particular, os volumes de iluminação diferidos foram aplicados e, depois disso, renderizamos o cabelo / rosto / olhos de Geralt.

A primeira coisa que você precisa saber sobre o nevoeiro em "The Witcher 3": consiste em duas partes - "cor do nevoeiro" e "cor da atmosfera".

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

Para cada parte, existem três cores: frontal, central e traseira. Ou seja, no buffer constante existem dados como "FogColorFront", "FogColorMiddle", "AerialColorBack", etc. ... Vamos ver os dados recebidos:


   // *** 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 as cores finais, precisamos calcular os vetores e produtos escalares. O sombreador tem acesso à posição do pixel no mundo, à posição da câmera (cb12 [0] .xyz) e à direção da névoa / iluminação (cb12 [38] .xyz). Isso nos permite calcular o produto escalar do vetor da forma e direção do nevoeiro.

   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 o gradiente de mistura, você precisa usar o quadrado do produto escalar absoluto e multiplicar novamente o resultado por algum parâmetro que depende da distância:

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

Esse bloco de código nos deixa claro de onde esses 0,002 e -0,300 vieram. Como podemos ver, o produto escalar entre os vetores de visão e iluminação é responsável pela escolha entre as cores "frontal" e "traseira". Inteligente!

Aqui está uma visualização do gradiente final resultante (_dd).


No entanto, calcular o efeito da atmosfera / névoa é muito mais complicado. Como você pode ver, temos muito mais opções do que apenas cores rgb. Eles incluem, por exemplo, densidade de cena. Usamos o raymarching (16 etapas, e é por isso que o ciclo pode ser expandido) para determinar o tamanho do nevoeiro e o fator de escala:

Tendo um vetor [camera ---> world], podemos dividir todos os seus componentes em 16 - esse será um passo do raymarching. Como podemos ver abaixo, apenas o componente .z (height) ( curr_pos_z_step ) está envolvido nos cálculos .

Leia mais sobre o nevoeiro implementado pelo raymarching, por exemplo, aqui .

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

Obviamente, a quantidade de neblina depende da altura (componentes .z); no final, a quantidade de neblina é elevada ao grau de neblina / atmosfera.

final_exp_fog e final_exp_aerial são obtidos do buffer constante; eles permitem que você controle como as cores do nevoeiro e da atmosfera afetam o mundo com o aumento da altitude.

Substituição de nevoeiro


O shader que encontrei não tinha o seguinte fragmento de código do assembly:

  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 julgar pelo que pude entender, é como redefinir a cor e o efeito do nevoeiro: na

maioria das vezes, apenas uma redefinição é executada (cb12_v192.x é 0,0), mas nesse caso em particular, seu valor é ~ 0,22, então fazemos a segunda substituição.


 #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

Aqui está o preço final sem redefinir a névoa (primeira imagem), com uma redefinição (segunda imagem) e dupla redefinição (terceira imagem, resultado final):




Regulação da oclusão ambiental


O shader que encontrei também não usava oclusão ambiental. Vamos dar uma olhada na textura do AO novamente e no código que nos interessa:


  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)

Talvez essa cena não seja o melhor exemplo, porque não vemos os detalhes em uma ilha distante. No entanto, vamos dar uma olhada no buffer constante, que é usado para definir o valor da oclusão do ambiente:


Começamos carregando AO da textura e depois executamos a instrução max. Nesta cena, cb3_v1.x é muito alto (0,96888), o que torna a AO muito fraca.

A próxima parte do código calcula a distância entre as posições da câmera e os pixels no mundo.

Eu acredito que o código às vezes fala por si, então vamos ver o HLSL, que faz a maior parte dessa instalação:

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

A distância calculada entre a câmera e o mundo é usada para a função linstep. Já conhecemos essa função, que apareceu no sombreador da nuvem de cirros.

Como você pode ver, no buffer constante, temos os valores da distância inicial / final da AO. A saída do linstep afeta a força do AO (assim como do cbuffer), e a força afeta a saída do AO.

Um breve exemplo: o pixel está longe, por exemplo, a distância é 500.

linstep retorna 1.0;
aoStrength é igual a aoStrengthEnd;

Isso resulta em um retorno AO, que é aproximadamente 77% (força final) do valor de entrada.

O AO de entrada para esta função foi previamente submetido à operação máxima.

Juntando tudo


Tendo recebido a cor e o efeito da cor do nevoeiro e da cor da atmosfera, você pode finalmente combiná-los.

Começamos atenuando o efeito com a 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 a mágica acontece na função 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;  
 }

Primeiro, calculamos a luminosidade dos pixels:


Então multiplicamos pela cor da atmosfera:


Em seguida, combinamos a cor HDR com a cor da atmosfera:


O último passo é combinar o resultado intermediário com a cor do nevoeiro:


Isso é tudo!

Algumas capturas de tela de depuração



Efeito atmosférico


Cor da atmosfera


Efeito nevoeiro


Cor de nevoeiro


Cena finalizada sem nevoeiro


Cena pronta com apenas nevoeiro


A cena final é apenas o nevoeiro principal


Cena pronta novamente com todo o nevoeiro para facilitar a comparação

Total


Eu acho que você pode entender muitas coisas acima, se você olhar para o shader, está aqui .

Posso dizer com prazer que esse shader é exatamente o mesmo que o original - isso me deixa muito feliz.

Em geral, o resultado final é altamente dependente dos valores transmitidos ao shader. Esta não é uma solução "mágica" que fornece cores perfeitas na saída; requer muitas iterações e artistas para fazer com que o resultado final pareça decente. Acho que pode ser um processo longo, mas depois que você o concluir, o resultado será muito convincente, assim como esta cena do pôr do sol.

O Witcher 3 Sky Shader também usa cálculos de nevoeiro para criar uma transição suave de cores perto do horizonte. No entanto, um conjunto diferente de coeficientes de densidade é passado para o sombreador do céu.

Deixe-me lembrá-lo - a maior parte desse shader não foi criada / analisada por mim. Todos os agradecimentos devem ser enviados para o CD PROJEKT RED. Apoie-os, eles fazem um excelente trabalho.

Parte 3. Estrelas cadentes


Em The Witcher 3, há um pequeno mas curioso detalhe - estrelas cadentes. Curiosamente, eles não parecem estar no DLC Blood and Wine.

No vídeo, você pode ver como elas se parecem:


Vamos ver como conseguimos esse efeito.

Como você pode ver, o corpo de uma estrela cadente é muito mais brilhante que a cauda. Essa é uma propriedade importante que usaremos posteriormente.

Nossa agenda é bastante familiar: primeiro descreverei as propriedades gerais, depois falarei sobre tópicos relacionados à geometria e, no final, passaremos para o pixel shader, onde as coisas mais interessantes estão acontecendo.

1. Visão geral


Descreva brevemente o que está acontecendo.

Estrelas cadentes são desenhadas em uma passagem proativa, imediatamente após a cúpula do céu, céu e lua:



DrawIndexed (720) - a cúpula do céu,
DrawIndexed (2160) - a esfera do céu / lua,
DrawIndexed (36) - é irrelevante, parece um paralelepípedo da oclusão do sol (?)
DrawIndexed (12) - a estrela cadente
DrawIndexedInstanced (1116, 1) - nuvens cirros

Como as nuvens cirros , cada estrela cadente é desenhada duas vezes seguidas.


Antes da primeira chamada de empate


Resultado da primeira chamada de empate


Resultado da segunda chamada de empate

Além disso, como em muitos elementos do passe preventivo deste jogo, o seguinte estado de mistura é usado:


2. Geometria


Do ponto de vista da geometria, a primeira coisa a mencionar é que cada estrela cadente é representada por um fino quadrilátero com cabos de texto: 4 vértices, 6 índices. Este é o quad mais simples possível.


Quadrilátero aproximado de uma estrela cadente


Ainda mais próximo é o quadrilátero aproximado de uma estrela cadente. Você pode ver a exibição em estrutura de arame de uma linha que indica dois triângulos.

Espere um minuto , mas há DrawIndexed (12) ! Isso significa que desenhamos duas estrelas cadentes ao mesmo tempo?

Sim.


Nesse quadro, uma das estrelas cadentes está completamente fora da pirâmide de visibilidade.

Vejamos o código do assembler para o vertex shader:

 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

Aqui, o cálculo da neblina pode atrair imediatamente atenção (linhas 30-138). O cálculo do topo da neblina faz sentido por razões de desempenho. Além disso, não precisamos de tanta precisão de neblina - os meteoróides geralmente voam sobre a cabeça de Geralt e não alcançam o horizonte.

Os parâmetros atmosféricos (rgb = cor, a = influência) são armazenados em o0.xyzw e os parâmetros de nevoeiro em o1.xyzw.

o2.xy (linha 140) é apenas texcoords.
o3.xyzw (linha 139) é irrelevante.

Agora vamos dizer algumas palavras sobre o cálculo de uma posição no mundo. Os shaders de vértice executam outdoors . Primeiro de tudo, os dados recebidos para outdoors vêm do buffer de vértice - vamos dar uma olhada neles.

Os primeiros dados são Posição:


Como mencionado acima, aqui temos 2 quad-a: 8 vértices, 12 índices.

Mas por que a posição é a mesma para cada quadrante? Muito simples - esta é a posição do centro do quad.

Além disso, cada vértice tem um deslocamento do centro para a borda do quad:


Isso significa que toda estrela cadente tem um tamanho de (400, 3) unidades no espaço do mundo. (no plano XY, no Witcher 3, o eixo Z é direcionado para cima)

O último elemento que cada vértice possui é um vetor de direção unitária no espaço do mundo que controla o movimento de uma estrela cadente:


Como os dados vêm da CPU, é difícil entender como são calculados.

Agora vamos para o código de outdoor. A idéia é bastante simples - primeiro você obtém um vetor de unidade do centro do quad para a câmera:

   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

Então temos um único vetor tangente que controla o movimento da estrela cadente.

Dado que esse vetor já está normalizado no lado da CPU, essa normalização é redundante.

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

Se houver dois vetores, um produto vetorial será usado para determinar o vetor bi-tangente perpendicular aos dois vetores recebidos.

  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

Agora, temos vetores normalizados tangente (r3.xyz) e bitangente (r2.xyz).

Vamos introduzir Xsize e Ysize correspondentes ao elemento recebido TEXCOORD1, por exemplo (-200, 1,50).

O cálculo final da posição no espaço mundial é realizado da seguinte forma:

  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 e r0.z são iguais a 1,0, o cálculo final é simplificado:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

A última parte é uma multiplicação simples de uma posição no espaço do mundo por uma matriz de visão de projeção para obter 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


Conforme declarado na seção Visão geral, o seguinte estado de mesclagem é usado: onde SrcColor e SrcAlpha são os componentes .rgb e .a do pixel shader, respectivamente , e DestColor é a cor .rgb atualmente no rendertarget. O principal indicador que controla a transparência é o SrcAlpha . Muitos shaders proativos de jogos o calculam como opacidade e o aplicam no final da seguinte maneira: O shader de estrela cadente não foi exceção. Seguindo esse padrão, consideramos três casos em que a opacidade é 1,0, 0,1 e 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



A idéia subjacente desse sombreador é modelar e usar a função de opacidade opacidade (x) , que controla a opacidade de um pixel ao longo de uma estrela cadente. O principal requisito é que a opacidade atinja os valores máximos no final da estrela (seu “corpo”) e desbote suavemente para 0,0 (em sua “cauda”).

Quando começamos a entender o código do assembler do pixel shader, isso se torna óbvio:

 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

Em geral, o shader é um pouco complicado demais e era difícil para mim descobrir o que estava acontecendo nele. Por exemplo, de onde vieram todos os valores como 1.211303, 21.643608 e 24.189651?

Se estamos falando sobre a função de opacidade, precisamos de um valor de entrada. Com isso, é bem simples - o texcoord no intervalo de [0,1] (linha 0) será útil aqui, para que possamos aplicar a função a todo o comprimento do meteoróide.

A função opacidade possui três segmentos / intervalos definidos por quatro pontos de controle:

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

Não faço ideia de como eles foram selecionados / calculados.

Como podemos ver no código do assembler, a primeira condição é apenas verificar se o valor de entrada está no intervalo [controlPoint0 - controlPoint3]. Caso contrário, a opacidade é de apenas 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)  
   {  
      ...

A descriptografia do código do assembler abaixo é necessária se quisermos entender como a função de opacidade funciona:

   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)

A linha 9 possui os coeficientes '-2.0' e '3.0', que sugerem o uso da função smoothstep . Sim, este é um bom palpite.

A função HLSL smoothstep com o protótipo: ret smoothstep (min, max, x) sempre limita x a [ min-max ]. Do ponto de vista do montador, isso subtrai min do valor de entrada (ou seja, de r0.z na linha 9), mas não há nada parecido no código. Para max, isso implica uma multiplicação do valor de entrada, mas não há nada como 'mul_sat' no código. Em vez disso, existe 'mov_sat'. Isso nos diz que as funções mín e máx do passo suave são 0 e 1.

Agora sabemos que xdeve estar no intervalo [0, 1]. Como mencionado acima, existem três segmentos na função de opacidade. Isso indica claramente que o código está procurando onde estamos no intervalo [segmentStart-segmentEnd].

A resposta é a função Linstep!

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

Por exemplo, vamos pegar o primeiro segmento: [0,052579 - 0,878136]. A subtração está na linha 6. Se substituirmos a divisão pela multiplicação -> 1,0 / (0,878136 - 0,052579) = 1,0 / 0,825557 = ~ 1,211303.

O resultado do passo suave está no intervalo [0, 1]. A multiplicação na linha 12 é o peso do segmento. Cada segmento tem seu próprio peso, permitindo controlar a opacidade máxima desse segmento específico.

Isso significa que, para o primeiro segmento [0,052579 - 0,878136], a opacidade está no intervalo [0 - 0,084642].

Uma função HLSL que calcula a opacidade para um segmento arbitrário pode ser escrita da seguinte maneira:

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

Portanto, o ponto principal é simplesmente chamar essa função para o segmento correspondente.

Dê uma olhada nos pesos:

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

De acordo com o código do assembler, a função opacidade (x) é calculada da seguinte maneira:

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

Aqui está um gráfico da função de opacidade. Você pode ver facilmente um aumento acentuado da opacidade, indicando o início do corpo de uma estrela cadente:


Função de opacidade do gráfico.

Canal vermelho - valor de opacidade
Canal verde - pontos de controle
Canal azul - pesos


Após calcular a opacidade, tudo o resto são apenas os retoques finais. Depois, há multiplicações adicionais: a opacidade das estrelas, a cor da estrela cadente e a influência do nevoeiro. Como de costume nos shaders TW3, você também pode encontrar multiplicações redundantes por 1,0 aqui:

   // 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. Resumo


A principal dificuldade está na parte com a função de opacidade. Depois de decodificá-lo, tudo o resto é bastante simples de entender.

Eu disse acima que o pixel shader é um pouco complicado demais. De fato, nos preocupamos apenas com o valor da função opacidade (x) , que é armazenada em r2.x (começando na linha 49). No entanto, a função de opacidade no código do assembler cria mais três variáveis ​​adicionais: minRange (r2.y), maxRange (r2.z) e valor (r2.w). Todos eles são parâmetros usados ​​para calcular a opacidade quando a opacidade (x) não é usada:

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

De fato, o valor final da opacidade é obtido no ramo condicional na linha 55 - se o valor de entrada for xestá no intervalo [controlPoint0 - controlPoint3], isso significa que a função de opacidade é usada e, portanto, r2.x é selecionado. Caso contrário, quando x estiver fora do intervalo, a opacidade é calculada a partir de r0.x, ou seja, de acordo com a equação acima.

Eu depurei alguns pixels fora do intervalo [controlPoint0 - controlPoint3], e a opacidade final sempre era zero.

Isso é tudo por hoje. E, como sempre, obrigado pela leitura.

All Articles