A engenharia reversa da renderização de The Witcher 3

A primeira parte da tradução está aqui . Nesta parte, falaremos sobre o efeito da nitidez, brilho médio, fases da lua e fenômenos atmosféricos durante a chuva.

Parte 6. Afiar


Nesta parte, examinaremos mais de perto outro efeito de pĂłs-processamento de The Witcher 3 - Sharpen.

A nitidez torna a imagem de saída um pouco mais nítida. Esse efeito é conhecido pelo Photoshop e outros editores gráficos.

Em The Witcher 3, a nitidez tem duas opções: baixa e alta. Falarei sobre a diferença entre eles abaixo, mas, por enquanto, vamos dar uma olhada nas capturas de tela:

imagem

Opção "Baixa" - até

imagem

Opção "Baixa" - após


Opção alta - até


Opção "Alta" - depois

Se você quiser dar uma olhada em comparações mais detalhadas (interativas), consulte a seção no Guia de desempenho da Nvidia The Witcher 3 . Como você pode ver, o efeito é especialmente visível na grama e folhagem.

Nesta parte do post, estudaremos o quadro desde o início do jogo: eu o escolhi intencionalmente, porque aqui vemos o relevo (longa distância de renderização) e a cúpula do céu.


Em termos de entrada, a nitidez requer um buffer de cor t0 (LDR após correção de tom e reflexos da lente) e um buffer de profundidade t1 .

Vamos examinar o cĂłdigo do assembler para o pixel shader:

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


50 linhas de código assembler parecem uma tarefa viável. Vamos resolver isso.

Aumentar a geração de valor


O primeiro passo é carregar o buffer de profundidade (linha 1). Vale ressaltar que o "The Witcher 3" usa uma profundidade invertida (1,0 - perto, 0,0 - longe). Como você deve saber, a profundidade do hardware está ligada de maneira não linear (consulte este artigo para obter detalhes ).

As linhas 3-6 fornecem uma maneira muito interessante de associar essa profundidade de hardware [1,0 - 0,0] aos valores [próximos] (os definimos no estágio MatrixPerspectiveFov). Considere os valores do buffer constante:


Tendo o valor "close" de 0,2 e o valor "far" 5000, podemos calcular os valores de cb12_v21.xy da seguinte maneira:

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


Esse pedaço de código é bastante comum nos shaders TW3, então acho que é apenas uma função.

Após obter a “profundidade da pirâmide de visibilidade”, a linha 7 usa a escala / distorção para criar o coeficiente de interpolação (aqui usamos saturado para limitar os valores ao intervalo [0-1]).


cb3_v1.xy e cb3_v2.xy - esse é o brilho do efeito de nitidez a distâncias curtas e longas. Vamos chamá-los de sharpenNear e sharpenFar. E essa é a única diferença entre as opções "Baixa" e "Alta" desse efeito em The Witcher 3.

Agora é hora de usar a proporção resultante. As linhas 8-9 apenas fazem o lerp(sharpenNear, sharpenFar, interpolationCoeff) . Para que é isso? Graças a isso, temos um brilho diferente perto de Geralt e longe dele. Dê uma olhada:


Talvez isso seja quase imperceptível, mas aqui interpolamos com base na distância o brilho de nitidez próximo ao player (2.177151) e o brilho do efeito é muito distante (1.91303). Após esse cálculo, adicionamos 1,0 ao brilho (linha 10). Por que isso é necessário? Suponha que a operação lerp mostrada acima nos deu 0,0. Depois de adicionar 1.0, obtemos naturalmente 1.0, e esse é um valor que não afetará o pixel ao fazer a nitidez. Leia mais sobre isso abaixo.

Enquanto afiamos, não queremos afetar o céu. Isso pode ser conseguido adicionando uma verificação condicional simples:

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


Em The Witcher 3, o valor da profundidade de pixel do céu é 1,0, portanto, usamos-o para obter uma espécie de "filtro binário" (um fato interessante: neste caso, a etapa não funcionará corretamente).

Agora podemos multiplicar o brilho interpolado por um "filtro do céu":


Essa multiplicação é realizada na linha 13.

Exemplo de cĂłdigo de sombreador:

// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;


Centro de amostragem de pixels


SV_Position tem um aspecto que será importante aqui: um deslocamento de meio pixel . Acontece que esse pixel no canto superior esquerdo (0, 0) possui coordenadas não (0, 0) em termos de SV_Position.xy, mas (0,5, 0,5). Uau!

Aqui queremos tirar uma amostra no centro do pixel, entĂŁo vamos ver as linhas 14-16. VocĂŞ pode escrevĂŞ-los em HLSL:

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

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


E mais tarde, amostramos a textura da cor de entrada dos cabos de texto "uvCenter". Não se preocupe, o resultado da amostragem será o mesmo do método "normal" (SV_Position.xy / ViewportSize.xy).

Para afiar ou nĂŁo afiar


A decisĂŁo sobre o uso de nitidez depende de fSharpenAmount.

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

//
float3 finalColor = colorCenter;

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

return float4( finalColor, 1 );


Afiar


É hora de dar uma olhada no interior do próprio algoritmo.

Essencialmente, ele executa as seguintes ações:

- mostra quatro vezes a textura da cor de entrada nos cantos do pixel,

- adiciona amostras e calcula o valor médio,

- calcula a diferença entre "centro" e "cornerAverage",

- encontre o componente absoluto máximo da diferença,

- corrige no máx. abs. componente usando escala + valores de viés,

- determina a magnitude do efeito usando max. abs. componente

- calcula o valor do brilho (luma) para "centerColor" e "averageColor",

- divide o colorCenter em sua luma,

- calcula um novo valor de luma interpolado com base na magnitude do efeito,

- Multiplica o colorCenter pelo novo valor de luma.

Muito trabalho, e foi difĂ­cil para mim descobrir, porque nunca havia experimentado filtros de nitidez.

Vamos começar com o padrão de amostragem. Como você pode ver no código do assembler, quatro leituras de textura são executadas.

Isso será melhor mostrado usando um exemplo de uma imagem de pixel (o nível de habilidade do artista é um especialista ):


Todas as leituras no sombreador usam amostragem bilinear (D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT).

O deslocamento do centro para cada um dos ângulos é (± 0,5, ± 0,5), dependendo do ângulo.

Veja como isso pode ser implementado no HLSL? Vamos ver:

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

float3 colorCorners = 0;

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

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

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

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


Portanto, agora todas as quatro amostras estão resumidas na variável "colorCorners". Vamos seguir estes passos:

//
float3 averageColorCorners = colorCorners / 4.0;

//
float3 diffColor = colorCenter - averageColorCorners;

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

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

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

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

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

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

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

return float4(finalColor, 1.0);


O reconhecimento da borda é realizado calculando-se máx. abs. componente de diferença. Jogada inteligente! Confira sua visualização:


Visualização do componente absoluto máximo da diferença.

Ótimo. O shader HLSL finalizado está disponível aqui . Desculpe pela formatação bastante ruim. Você pode usar meu programa HLSLexplorer e experimentar o código.

Felizmente, posso dizer que o cĂłdigo acima cria o mesmo cĂłdigo assembler que no jogo!

Para resumir: O shader de nitidez do Witcher 3 está muito bem escrito (observe que fPixelSharpenAmount é maior que 1,0! Isso é interessante ...). Além disso, a principal maneira de alterar o brilho do efeito é o brilho de objetos próximos / distantes. Neste jogo, eles não são constantes; Eu compilei vários exemplos de valores:

Skellige:

sharpenNearsharpenFarsharpenDistanceScalesharpenDistanceBiassharpenLumScalesharpenLumBias
baixo
alto2.01.80,025
-0,25
-13.33333
1,33333

Kaer Morhen:

sharpenNear
sharpenFar
sharpenDistanceScale
sharpenDistanceBias
sharpenLumScale
sharpenLumBias
baixo
0,57751
0,31303
0,06665
-0,33256
-1,0
2.0
alto
2.17751
1,91303
0,06665
-0,33256
-1,0
2.0

Parte 7. Brilho médio


A operação de calcular o brilho médio do quadro atual pode ser encontrada em quase todos os videogames modernos. Esse valor geralmente é usado posteriormente para o efeito de adaptação ocular e correção de tons (veja na parte anterior do post). Em soluções simples, o cálculo do brilho é usado para, digamos, a textura 512 2 , depois o cálculo dos níveis de mip e a aplicação deste último. Isso geralmente funciona, mas limita bastante as possibilidades. Soluções mais complexas usam shaders computacionais que realizam, por exemplo, redução paralela .

Vamos descobrir como a equipe do CD Projekt Red resolveu esse problema no The Witcher 3. Na parte anterior, eu já examinei a correção tonal e a adaptação do olho, então a única peça restante do quebra-cabeça era o brilho médio.

Para começar, o cálculo médio do brilho do The Witcher 3 consiste em duas passagens. Para maior clareza, decidi dividi-las em partes separadas, e primeiro examinamos a primeira passagem - "distribuição do brilho" (cálculo do histograma do brilho).

Distribuição de brilho


Essas duas passagens são fáceis de encontrar em qualquer analisador de quadros. Estas são as chamadas de Despacho em ordem imediatamente antes de executar a adaptação ocular:


Vamos dar uma olhada na entrada desse passe. Ele precisa de duas texturas:

1) buffer de cores HDR, cuja escala Ă© reduzida para 1/4 x 1/4 (por exemplo, de 1920x1080 a 480x270),

2) Buffer de profundidade de tela cheia


Buffer colorido HDR de 1/4 x 1/4. Observe o truque complicado - esse buffer faz parte de um buffer maior. Reutilizar buffers é uma boa prática.


Buffer de profundidade de tela cheia

Por que reduzir o buffer de cores? Eu acho que Ă© tudo sobre desempenho.

Quanto Ă  saĂ­da desse passe, Ă© um buffer estruturado. 256 elementos de 4 bytes cada.

Os shaders não possuem informações de depuração aqui, portanto, suponha que seja apenas um buffer de valores int não assinados.

Importante: a primeira etapa no cálculo do brilho médio chama ClearUnorderedAccessViewUint para redefinir todos os elementos do buffer estruturado para zero.

Vamos estudar o código assembler do shader computacional (este é o primeiro shader computacional em toda a nossa análise!)

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


E um buffer constante:


Já sabemos que a primeira entrada é um buffer de cores HDR. Com o FullHD, sua resolução é 480x270. Vamos olhar para a chamada de expedição.

Despacho (270, 1, 1) - isso significa que executamos 270 grupos de encadeamentos. Simplificando, executamos um grupo de linhas por linha do buffer de cores.


Cada grupo de encadeamentos executa uma linha do buffer de cores HDR

Agora que temos esse contexto, vamos tentar descobrir o que o shader faz.

Cada grupo de threads possui 64 threads na direção X (dcl_thread_group 64, 1, 1), além de memória compartilhada, 256 elementos com 4 bytes cada (dcl_tgsm_structured g0, 4, 256).

Observe que no shader usamos SV_GroupThreadID (vThreadIDInGroup.x) [0-63] e SV_GroupID (vThreadGroupID.x) [0-269].

1) Começamos atribuindo a todos os elementos dos valores zero de memória compartilhada. Como a memória total contém 256 elementos e 64 threads por grupo, isso pode ser feito convenientemente com um loop simples:

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


2) Depois disso, definimos a barreira usando GroupMemoryBarrierWithGroupSync (sync_g_t). Fazemos isso para garantir que todos os threads na memĂłria compartilhada dos grupos sejam redefinidos para zero antes de prosseguir para a prĂłxima etapa.

3) Agora estamos executando um loop, que pode ser escrito aproximadamente assim:

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


Este é um loop for simples, com um incremento de 64 (você já entendeu o porquê?).

O próximo passo é calcular a posição do pixel carregado.

Vamos pensar sobre isso.

Para a coordenada Y, podemos usar SV_GroupID.x porque lançamos 270 grupos de encadeamentos.

Para a coordenada X, podemos ... tirar proveito do fluxo atual do grupo! Vamos tentar fazer isso.

Como existem 64 threads em cada grupo, essa solução ignorará todos os pixels.

Considere o grupo de threads (0, 0, 0).

- O fluxo (0, 0, 0) processará pixels (0, 0), (64, 0), (128, 0), (192, 0), (256, 0), (320, 0), (384, 0), (448,0).

- O encadeamento (1, 0, 0) processará os pixels (1, 0), (65, 0), (129, 0), (193, 0), (257, 0), (321, 0), (385, 0), (449, 0) ...

- O fluxo (63, 0, 0) processará os pixels (63, 0), (127, 0), (191, 0), (255, 0), (319, 0), (383, 0), (447, 0)

Assim, todos os pixels serĂŁo processados.

Também precisamos garantir que não carregemos pixels de fora do buffer de cores:

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


Está vendo? É bem simples!

Também calculei o brilho (linha 21 do código do assembler).

Ótimo, já calculamos o brilho de um pixel colorido. O próximo passo é carregar (não a amostra!) O valor da profundidade correspondente.

Mas aqui temos um problema, porque conectamos o buffer de profundidades de resolução total. O que fazer sobre isso?

Isso Ă© surpreendentemente simples - basta multiplicar colorPos por alguma constante (cb0_v2.z). Reduzimos o buffer de cores HDR quatro vezes. portanto, o valor Ă© 4!

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


Até aí tudo bem! Mas ... chegamos às linhas 24-25 ...

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


Então Primeiro, temos uma comparação da igualdade de ponto flutuante, seu resultado é escrito em r2.x e logo depois disso ... o quê? Bitwise And ?? Sério? Para um valor de ponto flutuante? Que diabos ???

O problema 'eq + and'

Deixe-me dizer que, para mim, foi a parte mais difícil do sombreador. Eu até tentei combinações estranhas asint / asfloat ...

E se você usar uma abordagem um pouco diferente? Vamos apenas fazer a comparação flutuante-flutuante usual no HLSL.

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


E aqui está a saída no código do assembler:

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


Interessante, certo? Eu nĂŁo esperava ver "e" aqui.

0x3f800000 é apenas 1,0f ... É lógico, porque obtemos 1,0 e 0,0, caso contrário, se a comparação for bem-sucedida.

Mas e se "substituirmos" 1.0 por algum outro valor? Por exemplo, assim:

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


Temos o seguinte resultado:

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


Ha! Funcionou. Essa é apenas a mágica do compilador HLSL. Nota: se você substituir 0.0 por outra coisa, será movc.

Vamos voltar ao shader computacional. O próximo passo é verificar se a profundidade é igual a cb0_v2.w. É sempre igual a 0,0 - em outras palavras, verificamos se um pixel está em um plano distante (no céu). Nesse caso, atribuímos a esse coeficiente algum valor, aproximadamente 0,5 (verifiquei em vários quadros).

Este coeficiente calculado é usado para interpolar entre o brilho da cor e o brilho do “céu” (valor cb0_v2.x, que geralmente é aproximadamente igual a 0,0). Suponho que isso seja necessário para controlar a importância do céu no cálculo do brilho médio. Geralmente a importância é reduzida. Idéia muito inteligente.

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

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


Como temos o lumaOk, o próximo passo é calcular seu logaritmo natural para criar uma boa distribuição. Mas espere, digamos que lumaOk é 0,0. Sabemos que o valor do log (0) é indefinido, portanto adicionamos 1,0 porque log (1) = 0,0.

Depois disso, escalamos o logaritmo calculado para 128 para distribuí-lo em 256 células. Muito esperto!

E Ă© daqui que esse valor 88.722839 Ă© obtido. Este Ă© um 128 * (2) .

É assim que o HLSL calcula os logaritmos.

Há apenas uma função no código do montador HLSL que calcula os logaritmos: log e possui uma base de 2.

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

// 128
lumaOk *= 128;


Finalmente, calculamos o índice da célula a partir do brilho distribuído logaritmicamente e adicionamos 1 à célula correspondente na memória compartilhada.

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

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


A próxima etapa será novamente definir uma barreira para garantir que todos os pixels na linha foram processados.

E o último passo é adicionar valores da memória compartilhada ao buffer estruturado. Isso é feito da mesma maneira, através de um loop simples:

// ,
GroupMemoryBarrierWithGroupSync();

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

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


ApĂłs todos os 64 threads no grupo de threads preencherem os dados comuns, cada thread adiciona 4 valores ao buffer de saĂ­da.

Considere o buffer de saĂ­da. Vamos pensar sobre isso. A soma de todos os valores no buffer Ă© igual ao nĂşmero total de pixels! (em 480x270 = 129.600). Ou seja, sabemos quantos pixels tĂŞm um valor de brilho especĂ­fico.

Se você é pouco versado em sombreadores computacionais (como eu), a princípio pode não estar claro; portanto, leia o post mais algumas vezes, pegue papel e lápis e tente entender os conceitos nos quais essa técnica é construída.

Isso é tudo! É assim que The Witcher 3 calcula um histograma de brilho. Pessoalmente, aprendi muito ao escrever esta parte. Parabéns aos caras da CD Projekt Red pelo excelente trabalho!

Se você estiver interessado em um shader HLSL completo, ele estará disponível aqui . Eu sempre me esforço para obter o código de montagem o mais próximo possível do jogo e estou completamente feliz por ter conseguido novamente!

Cálculo do brilho médio


Esta é a segunda parte da análise dos cálculos de brilho médio em "The Witcher 3: Wild Hunt".

Antes de entrarmos em batalha com outro sombreador computacional, vamos repetir brevemente o que aconteceu na última parte: trabalhamos com um buffer de cores HDR com uma escala de 1 / 4x1 / 4. Após a primeira passagem, obtivemos um histograma de brilho (buffer estruturado de 256 valores inteiros não assinados). Calculamos o logaritmo para o brilho de cada pixel, o distribuímos por 256 células e aumentamos o valor correspondente do buffer estruturado em 1 por pixel. Por esse motivo, a soma total de todos os valores nessas 256 células é igual ao número de pixels.


Um exemplo da saĂ­da da primeira passagem. Existem 256 elementos.

Por exemplo, nosso buffer de tela cheia tem um tamanho de 1920x1080. Depois de diminuir o zoom, a primeira passagem usou um buffer de 480x270. A soma de todos os 256 valores no buffer será igual a 480 * 270 = 129 600.

Após esta breve introdução, estamos prontos para avançar para o próximo passo: a computação.

Desta vez, apenas um grupo de encadeamentos Ă© usado (Despacho (1, 1, 1)).

Vejamos o cĂłdigo do assembler do shader computacional:

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


Há um buffer constante:


Dê uma olhada rápida no código do assembler: dois UAVs estão anexados (u0: buffer de entrada da primeira parte e u1: textura de saída no formato 1x1 R32_FLOAT). Também vemos que existem 64 threads por grupo e 256 elementos de memória de grupo compartilhada de 4 bytes.

Começamos preenchendo a memória compartilhada com dados do buffer de entrada. Temos 64 threads, então você terá que fazer quase o mesmo de antes.

Para ter certeza absoluta de que todos os dados foram carregados para processamento adicional, depois disso, colocamos uma barreira.

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


Todos os cálculos são realizados em apenas um encadeamento, todos os outros são usados ​​simplesmente para carregar valores do buffer na memória compartilhada.

O fluxo de "computação" tem um índice de 0. Por quê? Teoricamente, podemos usar qualquer fluxo do intervalo [0-63], mas, graças a uma comparação com 0, podemos evitar comparações adicionais inteiro-inteiro (instruções ieq ).

O algoritmo é baseado na indicação do intervalo de pixels que serão levados em consideração na operação.

Na linha 11, multiplicamos width * height para obter o número total de pixels e os multiplicamos por dois números do intervalo [0.0f-1.0f], indicando o início e o final do intervalo. Restrições adicionais são usadas para garantir que 0 <= Start <= End <= totalPixels - 1 :

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

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

int pixelsMinusOne = totalPixels - 1;

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


Como você pode ver, existem dois ciclos abaixo. O problema com eles (ou com seu código assembler) é que existem transições condicionais estranhas no final dos loops. Foi muito difícil para mim recriá-los. Também dê uma olhada na linha 21. Por que existe "-1"? Vou explicar um pouco abaixo.

A tarefa do primeiro ciclo é eliminar pixelsToConsiderStart e fornecer o índice da célula de buffer na qual pixelsToConsiderStart +1 está presente (assim como o número de todos os pixels nas células anteriores).

Digamos que pixelsToConsiderStart é aproximadamente igual a 30.000, e no buffer existem 37.000 pixels na célula "zero" (isso acontece no jogo à noite). Portanto, queremos iniciar a análise de brilho a partir de aproximadamente pixel 30001, presente na célula "zero". Nesse caso, saímos imediatamente do loop, obtendo o índice inicial '0' e zero pixels descartados.

DĂŞ uma olhada no cĂłdigo HLSL:

//
int numProcessedPixels = 0;

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

//
bool bExitLoop = false;

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

// , lumaValue
int tempSum = numProcessedPixels + numPixels;

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


O número misterioso "-1" da linha 21 do código do assembler está associado à condição booleana para execução de loop (eu descobri isso quase por acidente).

Tendo recebido o número de pixels das células lumaValue e do próprio lumaValue , podemos passar para o segundo ciclo.

A tarefa do segundo ciclo é calcular a influência dos pixels e o brilho médio.

Começamos com lumaValue calculado no primeiro loop.

float finalAvgLuminance = 0.0f;

//
uint numProcessedPixelStart = numProcessedPixels;

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

//
numProcessedPixels += numPixels;

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

//
float numberOfPixelsWithCurrentLuma = numPixels;

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


Nesta fase, obtivemos o valor do brilho codificado no intervalo [0.0f-255.f].

O processo de decodificação é bastante simples - você precisa reverter o cálculo do estágio de codificação.

Uma breve repetição do processo de codificação:

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

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

//
outLuma = log( outLuma );

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

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


Para decodificar o brilho, revertemos o processo de codificação, por exemplo, assim:

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

// :

// 128
fDecodedLuma /= 128.0;

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

// 1.0
fDecodedLuma -= 1.0;


Em seguida, calculamos a distribuição multiplicando o número de pixels com um determinado brilho pelo brilho decodificado e somando-os até chegarmos ao processamento dos pixels de pixelsToConsiderEnd .

.

( ):

//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;

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


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

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

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

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


. HLSLexplorer , « 3» ( !).

. . :

1) «» , , .

2) RenderDoc (v. 1.2).

«ld_structured_indexable» , 0 , , - .

, ( . ), RenderDoc — !


. — , — .

8.


« 3» ( — « »).

— , , TW3 .

!


, . , — (. ), , . , , ( ), .

, , MinDepth MaxDepth D3D11_VIEWPORT 0.0 ( , ). .


,

, , . :

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


, « », — .

.

cb0[0].w X. .




(1024x512). RGB- , - — . !


- — .


RGB- — .

, RGBA-. - . HLSL- , :

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

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

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

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

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


, XY. ( The Witcher 3 Z , Z- 1.0). :

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

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

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


. 15-16:

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


0.033864? , , , 29.53, em dias! É isso que chamo de atenção aos detalhes!

Podemos assumir com segurança que cb0 [0] .y é o número de dias que se passaram durante o jogo. Um desvio adicional é usado aqui, usado como o deslocamento ao longo do eixo x da textura.

Tendo recebido esse coeficiente, multiplicamos por 2 * Pi.

EntĂŁo, usando sincos, calculamos outro vetor 2D.

Ao calcular o produto escalar entre o vetor normal e o vetor "lua", uma fase da lua Ă© simulada. Veja as capturas de tela com diferentes fases da lua:

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

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

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

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






O passo final é executar uma série de operações de multiplicação para calcular a cor final. Você provavelmente não entende por que esse sombreador envia um valor alfa de 0,0 para a saída. Isso ocorre porque a lua é renderizada com a mistura ativada:

// .

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

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

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

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

moonColor *= paramHorizon;

//
return float4(moonColor, 0.0);





(), .

, . RenderDoc ( «MoonPS» «EditedShaderPS»).

: :

— , — .

.


, .

9. G-


(gbuffer) The Witcher 3.

, (deferred shading).

: , , .

( ) GBuffer (, , specular color ....), ( ) .

— , , , .

, GBuffer — . . Crysis 3 .

« 3: »:




GBuffer render target DXGI_FORMAT_R8G8B8A8_UNORM + DXGI_FORMAT_D24_UNORM_S8_UINT.

:


Render Target 0 — RGB-,


Render Target 0 — -. , , .


Render Target 1 — RGB-. [0-1].


Render Target 1 — -. !


Render Target 2 — RGB-. specular color!

- ( ).


. , .


-, ( , ..)

GBuffer. , .

«» , :



1) O Ăşnico buffer a limpar Ă© o buffer de profundidade / estĂŞncil.

Se você analisar as texturas mencionadas acima em um bom analisador de quadros, ficará um pouco surpreso, porque elas não usam a chamada "Limpar", com exceção de Profundidade / Estêncil.

Ou seja, na realidade, RenderTarget1 se parece com isso (observe os pixels “embaçados” no plano distante):


.

: ClearRenderTargetView , .

2) —

. Witcher 3 reversed-z. .

DirectX :

a) «0», «1».

«1». «» 0, .

b)

c) «» «»

OpenGL (. ), .

3)

, . .


, GBuffer .

, , , specular.

, , .

, . , (, , ).

:


!

, :


, albedo, specular color. .

, :

, texcoords, .

texcoords, // , . (, ) , .

:

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


. .

— :


Albedo


. «OutputColor.rgb = Texture.Sample(uv).rgb»

RGB ( 1), 14 — , « ». HLSL:

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

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

float factor = 0.5 * (desaturationFactor - 1.0);

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

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

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


, . «material cbuffer». cb4_v1.x 1.0, , 0.0, lerp .

. desaturationFactor 4.0 ( 1.0), desaturatedColor . - (0.2, 0.3, 0.4); . , DX11-, , desaturatedColor float3( 0.25, 0.3, 0.45 )


desaturationFactor = 1.0 ( )


desaturationFactor = 2.0


desaturationFactor = 3.0


desaturationFactor = 4.0

, , albedo.

15-20 :

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


v0.z — , . , v0.z .

, - , albedo, v0.z 0, . HLSL:

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

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

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

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


RT0.a, , , , , , . ?

render target!


Vamos começar descompactando o mapa normal e, como sempre, vincularemos os normais: até agora, nada de surpreendente. Veja as linhas 28-33: Podemos escrevê-las mais ou menos da seguinte maneira: Não tenho certeza se está correto escrever. Se você souber o que é essa operação matemática, me avise. Vemos que o pixel shader usa SV_IsFrontFace.

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

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

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

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






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




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






O que é issoA documentação vem para ajudar (eu queria escrever "msdn", mas ...):

, . IsFrontFace true. , ( wireframe), IsFrontFace solid mode. , — .

. , (wireframe) . , ( ) wireframe.

: / , gbuffer [0-1] / :








[0-1]


[0-1]

, render target GBuffer R8G8B8A8_UNORM? , 256 . ?

Gbuffer — , , , .

, , . , , 13...:



Ha! The Witcher 3 " Best Fit Normals ". ( ). 2009-2010 Crytek, CryEngine , BFN open source .

BFN «» .

BFN [-1;1] [0, 1].

Specular


34, specular:

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


, Albedo «»:

. , «» specular color, … 0, .

HLSL:

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

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



, , , . , - :


- « ».

:

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


— v0.z! albedo specular:

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

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


Ótimo!Este é o fim da análise da primeira versão do pixel shader.

Aqui está uma comparação do meu sombreador (esquerda) com o original (direita):


Essas diferenças não afetam os cálculos, então meu trabalho aqui está concluído.

Pixel Shader: Albedo + opção normal


, albedo , specular. :

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


:

a) 1, 19 : v0.z cb4[0].x , albedo 19. «» v0.z.

b) 54-55 : o2.w , (cb4[7].x > 0.0 )

«- — » . :

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

c) 34-42 : specular.

specular. :

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


, (1 — ). , HLSL :

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

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


, . specular color.

, .

72 — WinMerge, : , . HLSLexplorer !


… , , , .

, , gbuffer The Witcher 3 . , , , .

pastebin:

1 — specular

2 — specular

10.


, — / . .


, CD Projekt Red . !

:








. :




— (0.0 — 1.0).

…


: Texcoords Instance_Transform.

Texcoords : U [0.02777 — 1.02734]. V 1.0, — 0.0. , .

, , INSTANCE_TRANSFORM. :




, ? , , !

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

mat = XMMatrixTranspose( mat );

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

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


:

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

vScale: (299.999969, 300.000000, 1000.00012)

vTranslation: (-116.495102, 238.690796, -1.45564997)


: (-116.5338, 234.8695, 2.09)

, , ( TW3 Z ), , .

:


. , .


.

:

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


Texcoords ( 0) Instance_LOD_Params ( 8), : SV_Position ( ) Height ( .z) .

, [0-1]? , . !

scale = float3(4, 4, 2), bias = float3(-2, -2, -1).<

, 9 28 — row-major-.

HLSL:

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

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

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

struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;

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

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

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

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

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

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

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

return Output;
}


Comparação do meu sombreador (esquerda) e do original (direita):


As diferenças não afetam os cálculos. Eu injetei meu shader no quadro e estava tudo bem!

Pixel shader


Finalmente!Para começar, mostrarei a entrada:

Duas texturas sĂŁo usadas aqui: a textura do ruĂ­do e o buffer de profundidade:



Valores de buffers constantes:





E o cĂłdigo do assembler para o pixel shader:

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


Uau! , .

? UV, cbuffer (cb0[0].x) /. texcoords ( 2).

, min/max ( 0 1).

, V (, V 1 0?) — 5.

« » — :


, (, ...) . , — :




, ( ). , , « ».

:

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

(0.001 ), :


( Sharpen , .)

, , , , 0.0004.

:


( 16), , ( , ), ( 19), - ( 20-22) (23-24).

-. , :

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

, , :

SourceColor — RGB- , DestColor — RGB- render target. SourceAlpha 0.0, : FinalColor = SourceColor + DestColor .

, . (0, 0, 0), .

HLSL — , :

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

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

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

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

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

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

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

float2 invViewportSize = cb0_v1.zw;

float depthScale = cb4_v6.x; // 0.001

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

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

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


// * Calculate mask covering distant objects behind cylinder.

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

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

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

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

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

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

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


, , .

, . !

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


All Articles