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

Recentemente, comecei a lidar com a renderização de The Witcher 3. Este jogo tem incríveis técnicas de renderização. Além disso, ela é magnífica em termos de enredo / música / jogabilidade.



Neste artigo, falarei sobre as soluções usadas para renderizar The Witcher 3. Não será tão abrangente quanto a análise dos gráficos do GTA V de Adrian Correger, pelo menos por enquanto.

Começaremos com a engenharia reversa da correção de tom.

Parte 1: correção de tom


Na maioria dos jogos AAA modernos, uma das etapas de renderização é necessariamente a correção de tom.

Deixe-me lembrá-lo de que na vida real existe uma gama bastante ampla de brilho, enquanto nas telas de computador é muito limitado (8 bits por pixel, o que nos dá de 0 a 255). É aqui que o mapeamento de tons é essencial, permitindo que você ajuste uma faixa mais ampla em um intervalo de iluminação limitado. Normalmente, existem duas fontes de dados nesse processo: uma imagem HDR com ponto flutuante, cujos valores de cor excedem 1,0 e a iluminação média da cena (a última pode ser calculada de várias maneiras, mesmo levando em consideração a adaptação do olho para simular o comportamento dos olhos humanos, mas isso não importa aqui).

O próximo (e último) passo é obter a velocidade do obturador, calcular a cor com a velocidade do obturador e processá-la usando a curva de correção de tom. E aqui tudo se torna bastante confuso, porque novos conceitos aparecem, como o "ponto branco" (ponto branco) e o "meio cinza" (meio cinza). Existem pelo menos algumas curvas populares, e algumas delas são abordadas em Um olhar mais atento ao mapeamento de tons de Matt Pettineo.

Honestamente, sempre tive problemas com a implementação correta da correção de tom no meu próprio código. Existem pelo menos alguns exemplos online diferentes que me foram úteis ... até certo ponto. Alguns deles levam em consideração o brilho do HDR / ponto branco / cinza médio, outros não - portanto, eles realmente não ajudam. Eu queria encontrar uma implementação "testada em batalha".

Trabalharemos no RenderDoc com a captura desse quadro de uma das principais missões do Novigrad. Todas as configurações estão no máximo:


Depois de pesquisar um pouco, encontrei uma chamada para correção de tom! Como mencionei acima, há um buffer de cores HDR (número de textura 0, resolução total) e o brilho médio da cena (número de textura 1, 1x1, ponto flutuante, calculado anteriormente pelo shader de computação).


Vamos dar uma olhada no código do assembler para o pixel shader:

ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed 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 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret 

Há vários pontos dignos de nota. Em primeiro lugar, o brilho carregado não precisa ser igual ao usado, pois é limitado (chamadas máx / min) dentro dos valores escolhidos pelos artistas (no buffer constante). Isso é conveniente porque permite evitar uma velocidade do obturador muito alta ou baixa. Esse movimento parece bastante comum, mas nunca o fiz antes. Em segundo lugar, alguém familiarizado com as curvas de correção de tons reconhecerá instantaneamente esse valor "11,2", porque, na verdade, esse é o valor do ponto branco da curva de correção de tons Uncharted2 de John Hable.

Os parâmetros AF são carregados no cbuffer.

Portanto, temos mais três parâmetros: cb3_v16.x, cb3_v16.y, cb3_v16.z. Podemos examinar seus significados:


Meu palpite:

Acredito que “x” é um tipo de “escala de branco” ou cinza médio, porque é multiplicado por 11.2 (linha 4) e, depois disso, é usado como numerador no cálculo da velocidade do obturador (linha 10).

"Y" - chamei de "fator numerador u2" e logo você verá o porquê.

"Z" é o "parâmetro de exponenciação", porque é usado no log triplo / mul / exp (de fato, na exponenciação).

Mas trate esses nomes de variáveis ​​com um certo ceticismo!

Também:

cb3_v4.yz - valores mínimo / máximo de brilho admissível,
cb3_v7.xyz - parâmetros CA da curva Uncharted2,
cb3_v8.xyz - parâmetros DF da curva Uncharted2.

Agora vamos à parte difícil - escreveremos um shader HLSL que nos fornecerá exatamente o mesmo código do assembler.

Isso pode ser muito difícil, e quanto maior o sombreador, mais difícil a tarefa. Felizmente, há algum tempo, escrevi uma ferramenta para navegar rapidamente pelo hlsl-> asm.

Senhoras e senhores, sejam bem-vindos ao D3DShaderDisassembler!


Tendo experimentado o código, obtive a correção de tons HLSL pronta para uso The Witcher 3 :

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); } 

Uma captura de tela do meu utilitário para confirmar isso:


Voila!

Acredito que esta seja uma implementação bastante precisa da correção de tons TW3, pelo menos em termos de código assembler. Eu já o apliquei no meu framework e funciona muito bem!

Eu disse “basta” porque não tenho idéia do porquê o denominador em ToneMapU2Func se torna máximo em zero. Ao dividir por 0, você deve ficar indefinido?

Isso pode ser concluído, mas quase por acidente encontrei neste quadro outra versão do sombreador TW3, usada para um belo pôr do sol (é interessante que seja usado com configurações gráficas mínimas!)


Vamos conferir. Primeiro, o código do assembler para o shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed 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 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret 

A princípio, o código pode parecer intimidador, mas, na verdade, nem tudo é tão ruim. Após uma breve análise, você notará que há duas chamadas para a função Uncharted2 com diferentes conjuntos de dados de entrada (AF, brilho mínimo / máximo ...). Eu nunca vi essa decisão antes.

E HLSL:

  cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); } 

Ou seja, de fato, temos dois conjuntos de parâmetros de controle, calculamos duas cores com correção de tom e, no final, as interpolamos. Decisão inteligente!

Parte 2: adaptação ocular


A segunda parte será muito mais simples.

Na primeira parte, mostrei como a correção tonal é realizada no TW3. Explicando a fundamentação teórica, mencionei brevemente a adaptação do olho. E você sabe o que? Nesta parte, falarei sobre como essa adaptação do olho é realizada.

Mas espere, o que é adaptação ocular e por que precisamos dela? A Wikipedia sabe tudo sobre isso, mas vou explicar: imagine que você está em um quarto escuro (lembre-se de que Life is Strange) ou em uma caverna e saia para fora onde está claro. Por exemplo, a principal fonte de iluminação pode ser o sol.

No escuro, nossas pupilas são dilatadas para que mais luz entre na retina através delas. Quando fica claro, nossas pupilas diminuem e às vezes fechamos os olhos porque "dói".

Essa alteração não ocorre instantaneamente. O olho deve se adaptar às mudanças no brilho. É por isso que realizamos a adaptação dos olhos na renderização em tempo real.

Um bom exemplo de quando é notada uma falta de adaptação ocular é o HDRToneMappingCS11 do DirectX SDK. Mudanças nítidas de brilho médio são bastante desagradáveis ​​e não naturais.

Vamos começar! Por uma questão de consistência, analisaremos o mesmo quadro do Novigrad.


Agora vamos nos aprofundar no programa de captura de quadros RenderDoc. A adaptação do olho geralmente é realizada logo antes da correção tonal, e The Witcher 3 não é exceção.


Vejamos o estado do pixel shader:


Temos duas fontes de entrada - 2 texturas, R32_FLOAT, 1x1 (um pixel). texture0 contém o brilho médio da cena do quadro anterior. texture1 contém o brilho médio da cena do quadro atual (calculado imediatamente antes desse shader de computação - marquei isso em azul).

Espera-se que exista uma saída - R32_FLOAT, 1x1. Vamos olhar para o pixel shader.

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret 

Uau, que simples! Apenas 7 linhas de código assembler. O que está acontecendo aqui? Vou explicar cada linha:

0) Obtenha o brilho médio do quadro atual.
1) Obtenha o brilho médio do quadro anterior.
2) Realize uma verificação: o brilho atual é menor ou igual ao brilho do quadro anterior?
Se sim, o brilho diminui; caso contrário, o brilho aumenta.
3) Calcule a diferença: diferença = currentLum - previousLum.
4) Essa transferência condicional (movc) atribui um fator de velocidade a partir do buffer constante. Dois valores diferentes podem ser atribuídos a partir da linha 2, dependendo do resultado da verificação. É uma jogada inteligente, pois dessa maneira você pode obter diferentes velocidades de adaptação para diminuir e aumentar o brilho. Mas no quadro estudado, os dois valores são os mesmos e variam de 0,11 a 0,3.
5) O cálculo final do brilho adaptado : uminação adaptada = velocidadeFator * diferença + luminância anterior.
6) O fim do shader

Isso é implementado no HLSL simplesmente:

  // The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; } 

Essas linhas nos dão o mesmo código assembler. Eu sugeriria apenas substituir o tipo de saída por float4 por float . Não há necessidade de desperdício de largura de banda. É assim que o Witcher 3 implementa a adaptação ocular. Muito simples, certo?

PS. Muito obrigado a Baldur Karlsson (Twitter: @baldurk ) pelo RenderDoc. O programa é ótimo.

Parte 3: aberração cromática


A aberração cromática é um efeito encontrado principalmente em lentes baratas. Isso ocorre porque as lentes têm diferentes índices de refração para diferentes comprimentos de luz visível. Como resultado, uma distorção visível aparece. No entanto, nem todo mundo gosta. Felizmente, em Witcher 3, esse efeito é muito sutil e, portanto, não é irritante na jogabilidade (pelo menos eu). Mas você pode desligá-lo, se desejar.

Vamos dar uma olhada em um exemplo de cena com aberração cromática e sem ela:


Aberração cromática incluída


Aberração cromática desativada

Você percebe alguma diferença perto das bordas? Eu também não. Vamos tentar outra cena:


A aberração cromática está incluída. Observe a leve distorção “vermelha” na área indicada.

Sim, muito melhor! Aqui o contraste entre as áreas escuras e claras é mais forte e, no canto, vemos uma leve distorção. Como você pode ver, esse efeito é muito fraco. No entanto, eu queria saber como é implementado. Vamos para a parte mais curiosa: o código!

Implementação

A primeira coisa a fazer é encontrar a chamada correta com um sombreador de pixels. De fato, a aberração cromática é parte do grande sombreador de pixel "pós-processamento", que consiste em aberração cromática, vinheta e correção de gama. Tudo isso está dentro de um único pixel shader. Vamos dar uma olhada no código do assembler para o pixel shader:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ... 

E para os valores do cbuffer:


Então, vamos tentar entender o que está acontecendo aqui. De fato, cb3_v17.xy é o centro da aberração cromática, de modo que as primeiras linhas calculam o vetor 2d das coordenadas texel (cb3_v17.zw = o inverso do tamanho da janela de exibição) para o “centro da aberração cromática” e seu comprimento, depois executam outros cálculos, verificações e ramificações . Ao aplicar a aberração cromática, calculamos os deslocamentos usando certos valores do buffer constante e distorcemos os canais R e G. Em geral, quanto mais próximos das bordas da tela, maior o efeito. A linha 10 é bastante interessante porque faz com que os pixels "se aproximem", especialmente quando exageramos a aberração. Terei prazer em compartilhar com você minha percepção do efeito. Como sempre, use nomes de variáveis ​​com uma cota (sólida) de ceticismo. E observe que o efeito é aplicado antes da correção gama.

  void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } } 

Eu adicionei “fChromaticAberrationIntensity” para aumentar o tamanho do deslocamento e, portanto, a força do efeito, como o nome sugere (TW3 = 1.0). Intensidade = 40:


Isso é tudo! Espero que você tenha gostado desta parte.

Parte 4: vinhetas


A vinheta é um dos efeitos de pós-processamento mais comuns usados ​​nos jogos. Ele também é popular na fotografia. Cantos ligeiramente sombreados podem criar um efeito bonito. Existem vários tipos de vinhetas. Por exemplo, o Unreal Engine 4 usa natural. Mas voltando ao The Witcher 3. Clique aqui para ver uma comparação interativa de quadros com e sem vinheta. A comparação é feita no guia de desempenho da NVIDIA para The Witcher 3 .


Captura de tela de "The Witcher 3" com vinhetas ativadas.

Observe que o canto superior esquerdo (céu) não está tão sombreado quanto as outras partes da imagem. Mais tarde retornaremos a isso.

Detalhes da implementação

Em primeiro lugar, há uma pequena diferença entre as vinhetas usadas na versão original de The Witcher 3 (lançado em 19 de maio de 2015) e em The Witcher 3: Blood and Wine. No primeiro, o "gradiente inverso" é calculado dentro do pixel shader e, no segundo, é pré-calculado em uma textura 2D de 256x256:


Textura 256x256, usada como "gradiente reverso" no complemento "Sangue e vinho".

Vou usar o shader de "Blood and Wine" (um ótimo jogo, por sinal). Como na maioria dos outros jogos, a vinheta de Witcher 3 é calculada no pixel shader do pós-processamento final. Dê uma olhada no código do assembler:

  ... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ... 

Interessante! Parece que os espaços gama (linha 46) e lineares (linha 51) são usados ​​para calcular as vinhetas. Na linha 48, amostramos a textura do "gradiente inverso". cb3 [9] .xyz não está relacionado à vinheta. Em cada quadro verificado, é atribuído o valor float3 (1.0, 1.0, 1.0), ou seja, é provavelmente o filtro final usado nos efeitos de desvanecimento / desvanecimento. Existem três parâmetros principais para vinhetas no TW3:

  • Opacidade (cb3 [6] .w) - afeta a força da vinheta. 0 - sem vinheta, 1 - vinheta máxima. De acordo com minhas observações, na base The Witcher 3 é de aproximadamente 1,0, enquanto em Blood and Wine flutua em torno de 0,15.
  • Cor (cb3 [7] .xyz) - um excelente recurso da vinheta TW3 é a capacidade de alterar sua cor. Não precisa ser preto, mas na prática ... Geralmente, possui os valores float3 (3.0 / 255.0, 4.0 / 255.0, 5.0 / 255.0) e assim por diante - no caso geral, são múltiplos de 0,00392156 = 1,0 / 255,0
  • Pesos (cb3 [6] .xyz) é um parâmetro muito interessante. Eu sempre vi vinhetas "planas", por exemplo:



Máscara típica de vinheta

Mas usando pesos (linha 52), você pode obter resultados muito interessantes:


Máscara de vinheta TW3 calculada usando pesos

Os pesos estão próximos de 1,0. Observe os dados constantes do buffer de um quadro de Blood and Wine (um mundo mágico com um arco-íris): é por isso que as vinhetas não afetaram os pixels brilhantes do céu acima.


Código

Aqui está minha implementação da vinheta TW3 no HLSL.

GammaToLinear = prisioneiro de guerra (cor, 2.2)

  /* // The Witcher 3 vignette. // // Input color is in gamma space // Output color is in gamma space as well. */ float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; } 

Espero que tenham gostado. Você também pode experimentar o meu HLSLexplorer , que me ajudou muito a entender o código do montador HLSL.

Como antes, tome os nomes das variáveis ​​com um certo ceticismo - os shaders TW3 são processados ​​pelo D3DStripShader; portanto, na verdade eu não sei quase nada sobre eles, só posso adivinhar. Além disso, não me responsabilizo pelos danos causados ​​ao seu equipamento por este shader;)

Bônus: calculando o gradiente

No The Witcher 3, lançado em 2015, o gradiente inverso foi calculado no pixel shader, em vez de amostrar uma textura pré-calculada. Dê uma olhada no código do assembler:

  35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000) 

Felizmente para nós, é bastante simples. No HLSL, será algo parecido com isto:

  float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; } 

Ou seja, simplesmente calculamos a distância do centro ao têxtil, fazemos um pouco de mágica (multiplicação, saturam ...) e depois ... calculamos o polinômio! Awesome.



Parte 5: o efeito da intoxicação


Vamos ver como o jogo "The Witcher 3: Wild Hunt" implementa o efeito da intoxicação. Se você ainda não o reproduziu, largue tudo, compre e reproduza, assista a um vídeo

:



Noite:


Primeiro, vemos uma imagem dupla e turbilhão, surgindo frequentemente quando você bebe na vida real. Quanto mais um pixel estiver do centro da imagem, mais forte será o efeito de rotação. Eu intencionalmente postei o segundo vídeo com a noite, porque você pode ver claramente essa rotação nas estrelas (veja 8 pontos separados?)

A segunda parte do efeito da intoxicação, que pode não ser imediatamente perceptível, é uma pequena alteração no zoom. É perceptível perto do centro.

Provavelmente, é óbvio que esse efeito é um pós-processamento típico (pixel shader). No entanto, sua localização no pipeline de renderização pode não ser tão óbvia. Acontece que o efeito da intoxicação é aplicado imediatamente após a correção tonal e logo antes do desfoque de movimento (a imagem "bêbada" é a entrada para o desfoque de movimento).

Vamos começar os jogos com código assembler:

  ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Dois buffers constantes separados são usados ​​aqui. Vamos verificar seus valores:



Estamos interessados ​​em alguns deles:

cb0_v0.x -> tempo decorrido (em segundos)
cb0_v1.xyzw - tamanho da viewport e o inverso do tamanho da viewport (também conhecido como "tamanho do pixel")
cb3_v0.x - rotação em torno de um pixel, sempre com um valor de 1,0.
cb3_v0.y - a magnitude do efeito da intoxicação. Depois de ligado, ele não funciona com força total, mas aumenta gradualmente de 0,0 para 1,0.
cv3_v1.xy - deslocamentos de pixel (mais sobre isso abaixo). Este é um par sin / cos, para que você possa usar sincos (tempo) no shader, se desejar.
cb3_v2.xy é o centro do efeito, geralmente float2 (0,5, 0,5).
Aqui, queremos nos concentrar em entender o que está acontecendo, e não apenas reescrever cegamente o sombreador.

Começaremos com as primeiras linhas:

  ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 

Eu chamo a linha 0 de "proporção de zoom" e você logo verá o porquê. Imediatamente após (linha 1), calculamos o "deslocamento da rotação". Este é apenas um par de dados sin / cos de entrada multiplicado por 0,05.

Linhas 2-4: Primeiro, calculamos o vetor do centro do efeito para as coordenadas UV da textura. Então calculamos o quadrado da distância (3) e a distância simples (4) (do centro ao texel)

Coordenadas de textura de zoom


Vejamos o seguinte código do assembler:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 

Como eles são compactados dessa maneira, podemos analisar apenas um par de flutuador.

Para iniciantes, r0.yz são "deslocamentos de rotação", r1.z é a distância do centro ao texel, r1.xy é o vetor do centro ao texel, r0.x é o "fator de zoom".

Para entender isso, vamos supor, por enquanto, que zoomFactor = 1.0, ou seja, você pode escrever o seguinte:

  8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center; 

Mas zoomFactor = 1.0:

  r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter; 

Da mesma forma para r3.xy:

  10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center 


Mas zoomFactor = 1.0: Ótimo. Ou seja, no momento, temos essencialmente o deslocamento atual TextureUV (texel) ± rotação, mas e o zoomFactor? Veja a linha 0. De fato, zoomFactor = 1.0 - 0.1 * drunkAmount. Para o máximo drunkAmount, o valor de zoomFactor deve ser 0,9 e as coordenadas da textura com zoom agora são calculadas da seguinte forma:

r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter



  baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter 

Talvez tal explicação seja mais intuitiva: é simplesmente interpolação linear por algum fator entre as coordenadas normalizadas da textura e o centro. Esta é uma imagem de "zoom-in". Para entender isso, é melhor experimentar os valores. Aqui está um link para Shadertoy, onde você pode ver o efeito em ação.

Deslocamento de textura


Todo o fragmento no código do assembler:

  2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset 

cria um certo gradiente, vamos chamá-lo de "máscara de intensidade de deslocamento". De fato, ele dá dois significados. O primeiro está em r0.w (vamos usá-lo mais tarde) e o segundo é 5 vezes mais forte em r0.x (linha 15). Este último realmente serve como um fator para o tamanho do texel, por isso afeta a força de polarização.

Amostragem relacionada à rotação

Em seguida, é realizada uma série de amostragens de textura. De fato, são utilizadas 2 séries de 8 amostras, uma para cada "lado". No HLSL, você pode escrever isso da seguinte maneira:

  static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB; 

O truque é adicionar à baseTexcoordsA / B um deslocamento adicional em um círculo unitário, multiplicado pela “intensidade de deslocamento da coordenada de textura” mencionada anteriormente. Quanto mais longe o centro do pixel, maior o raio do círculo ao redor do pixel - nós o amostramos 8 vezes, o que é claramente visível nas estrelas. Valores PointsAroundPixel (múltiplos de 45 graus):


Círculo único

Amostragem relacionada ao zoom

A segunda parte do efeito de embriaguez no The Witcher 3 é o zoom com mais e menos zoom. Vamos dar uma olhada no código do assembler que executa esta tarefa:

  56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 

Vemos que há três chamadas de textura separadas, ou seja, três coordenadas de textura diferentes. Vamos analisar como as coordenadas de textura são calculadas a partir delas. Mas primeiro, mostramos a entrada para esta parte:

  float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60 

Algumas palavras sobre a entrada. Calculamos o deslocamento em texels (por exemplo, 8,0 * tamanho texel), que é então adicionado às coordenadas uv de base. A amplitude simplesmente varia entre 0,98 e 1,02 para dar uma sensação de zoom, assim como o zoomFactor na parte que executa a rotação.

Vamos começar com o primeiro par - r1.xy (linha 61)

  r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude); 

Isto é:

 float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude); 

Vamos verificar o segundo par - r3.xy (linha 62)

  r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV 

Isto é:

  float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates; 

Vamos verificar o terceiro par - r0.xy (linhas 63-64)

  r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV 

Isto é:

  float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates 

Todas as três consultas de textura são adicionadas e o resultado é armazenado no registro r1. Vale a pena notar que este pixel shader usa um amostrador de endereçamento limitado.

Juntando tudo

Então, no momento, temos o resultado da rotação no registro r2 e três solicitações de zoom dobrado no registro r1. Vejamos as últimas linhas do código do assembler:

  70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret 

Sobre informações adicionais: r0.w é obtido da linha 7, esta é a nossa máscara de intensidade e cb3 [0] .y é a magnitude do efeito de intoxicação.

Vamos ver como isso funciona. Minha primeira abordagem foi a força bruta:

  float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor; 

Mas que diabos, ninguém escreve shaders assim . Peguei um lápis com papel e escrevi esta fórmula:

  finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart 


Onde t = effectAmount * intensidadeMask

Então, obtemos:

  finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t ) 

E chegamos ao seguinte:

  finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity); 

Sim, essa parte do artigo acabou sendo muito detalhada, mas finalmente terminamos! Pessoalmente, aprendi algo no processo de escrever, espero que você também!

Se você estiver interessado, as fontes HLSL completas estão disponíveis aqui . Testei-os com meu HLSLexplorer e, embora não haja correspondências diretas individuais com o shader original, as diferenças são tão pequenas (uma linha a menos) que posso dizer com confiança que ele funciona. Obrigado pela leitura!

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


All Articles