Efeito Glitch de NieR:Automata

Recentemente joguei NieR:Automata e fiquei simplesmente impressionado, então eu decidi replicar um dos efeitos de glitch que aparecem no final do jogo.


Breakdown do Shader

Primeiro precisamos quebrar o shader em pequenas partes, para que seja fácil replicar uma por uma.

nier automata glitch shader

Pixelização

nier automata glitch shader

Separação de Cor (Dano)

nier automata glitch shader

Corrupção de Cor e Separação de Cor

nier automata glitch shader

Dessaturação, Distorção de Lente e Aberração Cromática

Certo, agora que temos as pequenas partes podemos começar a replicar uma de cada vez.


Pixelização

Parece que o efeito de pixelização só acontece em linhas verticais, então para fazer isso precisamos fazer o sample da tela em uma resolução diferente no eixo U.

float glitchStep = lerp(4, 32, _Rnd);
float glitchUV = round(i.uv.x * glitchStep) / glitchStep;
fixed4 glitchCol = tex2D(_MainTex, float2(glitchUV, i.uv.y));

Primeiro pegamos um valor aleatório (de um script C#) para mudar a resolução em tempo real, note como no shader original ele fica mudando.

Depois posterizamos o UV com a quantidade de steps que precisamos.

E finalmente fazemos o sample da tela usando nosso novo UV (mas só o U, o V fica intocado) 

nier automata glitch shader

O efeito está meio forte, porém, para resolver isso podemos fazer um lerp entre a cor do glitch e a cor da tela.

fixed4 glitchFinal = lerp(scrCol, glitchCol, 0.25);

Vamos usar 0.25 por enquanto, mas depois vamos mudar para um valor controlado por um script C#.

nier automata glitch shader

Bem melhor!


Separação de Cor

Quando o jogador toma dmg no jogo há um efeito de separação de cor, isso também é bem simples de fazer, basta fazer o sample da cor da cena 2 vezes extras, uma para o canal R e outra para o canal B, depois deslocar por uma textura de ruído e combinar com a cor da tela não modificada.

fixed chrNoise = tex2D(_Noise, frac(i.uv + _Rnd2 * 10)*0.5);
fixed chrNoise2 = tex2D(_Noise, frac(i.uv + _Rnd * 10)*0.75);

Primeiro fazemos o sample da textura de ruído 2 vezes, _Rnd e _Rnd2 são valores aleatórios gerados pela CPU, fazemos isso para evitar que padrões óbvios apareçam.

A textura de ruído

Depois aplicamos step nessas 2 texturas para obter um valor binário e remapear para -1 ~ 1.

float chrOffset = step(0.5*(chrNoise + chrNoise2), 0.5);
chrOffset = (2 * chrOffset + 1) * 0.005 * _Dmg;

O _Dmg também é calculado na CPU, apenas um timer simples para que ele possa ser disparado como no jogo.

Isso vai nos dar um resultado como este.

nier automata glitch shader

Agora tudo o que precisamos fazer é o sample da textura com os 2 offsets.

fixed3 chrColR = tex2D(_MainTex, float2(i.uv.x + chrOffset, i.uv.y));
fixed3 chrColB = tex2D(_MainTex, float2(i.uv.x - chrOffset, i.uv.y));

nier automata glitch shader

Mas também há uma separação de cor que acontece mesmo sem dmg, então vamos adicionar isso também.

fixed3 chrCol2R = tex2D(_MainTex, float2(i.uv.x + step(0.5*(chrNoise + chrNoise2), 0.2) * 0.005, i.uv.y));
fixed3 chrCol2B = tex2D(_MainTex, float2(i.uv.x - step(0.5*(chrNoise + chrNoise2), 0.1) * 0.005, i.uv.y));

E então fazemos o lerp dos dois usando a variável _Dmg.

fixed4 finalScrCol = fixed4(0, 0, 0, 0);
finalScrCol.r = lerp(chrCol2R.r, chrColR.r, _Dmg) - step(chrNoise2, 0.2);
finalScrCol.g = scrCol.g + step(chrNoise, 0.2);
finalScrCol.b = lerp(chrCol2B.b, chrColB.b, _Dmg) - step(chrNoise2, 0.2);

O step serve para fazer a máscara da separação de cor, para que ela não fique na tela inteira.

nier automata glitch shader


Corrupção de Cor

A maior parte do trabalho para esse efeito já está feita na separação de cor, basta adicionar um multiplicador ao nosso finalScrCol.

finalScrCol.r = lerp(chrCol2R.r, chrColR.r, _Dmg) - step(chrNoise2, 0.2) * _Corruption;
finalScrCol.g = scrCol.g + step(chrNoise, 0.2) * _Corruption;
finalScrCol.b = lerp(chrCol2B.b, chrColB.b, _Dmg) - step(chrNoise2, 0.2) * _Corruption;

_Corruption é apenas um float, só lembre de mantê-lo pequeno!

nier automata glitch shader

O efeito exagerado


Distorção de Lente

Finalmente chegamos à última parte.

float aberration = pow(((length(i.uv * 2 - 1)) - (_SinTime.z)*0.1), 2);

Aqui remapeamos o uv para -1 ~ 1, depois pegamos o comprimento dele, isso vai resultar em uma máscara circular

Note que a distorção tem um movimento de pulso no jogo, então aqui subtraímos o _SintTime para conseguir esse movimento.

Agora que temos a máscara pronta, vamos fazer ela distorcer nossa tela.

fixed4 lensBlur = tex2D(_MainTex, i.uv - (i.uv* 2 - 1) * aberration * 0.0125*2);
fixed4 lensBlur2 = tex2D(_MainTex, i.uv - (i.uv * 2 - 1) * aberration * 0.0125);
fixed4 lensBlur3 = tex2D(_MainTex, i.uv - (i.uv * 2 - 1) * aberration * 0.025);

Há um pequeno desfoque no efeito original, estou fazendo um aqui aplicando um offset diferente a um sample extra.

Agora pegamos um desses samples, dessaturamos ele, e subtraímos para obter a diferença em relação à cor original da tela.

fixed lensAbrR = dot((lensBlur3 - scrCol), float3(0.3, 0.59, 0.11));

A máscara de aberração cromática

Então tudo o que falta é combinar tudo o que fizemos até agora!

fixed4 compositeCol = lerp(finalScrCol, glitchFinal, _GlitchAmmount);
fixed4 desaturatedCol = dot((scrCol + lensBlur + lensBlur2) / 3, float3(0.3, 0.59, 0.11));
desaturatedCol += fixed4(lensAbrR*2, 0, 0, 0);

return lerp(compositeCol, desaturatedCol, _Desaturate);

E terminamos!!

Aqui está o shader finalizado em ação.