Escrevendo shaders no Unity. GrabPass, PerRendererData

Oi Gostaria de compartilhar minha experiência escrevendo shaders no Unity. Vamos começar com o sombreador Displacement / Refraction em 2D, considerar a funcionalidade usada para escrevê-lo (GrabPass, PerRendererData) e também prestar atenção aos problemas que necessariamente surgirão.

As informações são úteis para aqueles que têm uma idéia geral dos shaders e tentaram criá-los, mas não estão familiarizados com os recursos que o Unity fornece e não sabem de que lado se aproximar. Dê uma olhada, talvez minha experiência o ajude a descobrir.



Este é o resultado que queremos alcançar.

imagem

Preparação


Primeiro, crie um shader que simplesmente desenhe o sprite especificado. Ele será nossa base para futuras manipulações. Algo será adicionado a ele, algo pelo contrário será excluído. Ele será diferente do padrão "Padrão de Sprites" pela ausência de algumas tags e ações que não afetarão o resultado.

Código de sombreador para renderizar sprite
Shader "Displacement/Displacement_Wave" { Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Cull Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; }; fixed4 _Color; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return texColor; } ENDCG } } } 

Sprite para exibir
O fundo é realmente transparente, intencionalmente escurecido.

imagem

A peça resultante.

imagem

Grabpass


Agora, nossa tarefa é fazer alterações na imagem atual na tela e, para isso, precisamos obter uma imagem. E a passagem do GrabPass nos ajudará com isso. Esta passagem capturará a imagem da tela na textura _GrabTexture . A textura conterá apenas o que foi desenhado antes que nosso objeto usando esse shader fosse renderizado.

Além da textura em si, precisamos das coordenadas da digitalização para obter a cor do pixel. Para fazer isso, adicione coordenadas de textura adicionais aos dados do sombreador de fragmentos. Essas coordenadas não são normalizadas (os valores não estão na faixa de 0 a 1) e descrevem a posição de um ponto no espaço da câmera (projeção).

 struct v2f { float4 vertex : SV_POSITION; float2 uv : float4 color : COLOR; float4 grabPos : TEXCOORD1; }; 

E no vertex shader, preencha-os.

 o.grabPos = ComputeGrabScreenPos (o.vertex); 

Para obter a cor de _GrabTexture , podemos usar o seguinte método se usarmos coordenadas não normalizadas

 tex2Dproj(_GrabTexture, i.grabPos) 

Mas usaremos um método diferente e normalizaremos as coordenadas, usando a divisão de perspectiva, ou seja, dividindo todos os outros no componente w.

 tex2D(_GrabTexture, i.grabPos.xy/i.grabPos.w) 

componente w
A divisão em um componente w é necessária somente ao usar a perspectiva, na projeção ortográfica sempre será 1. Na verdade, w armazena o valor da distância, aponte para a câmera. Mas não é a profundidade - z , cujo valor deve estar no intervalo de 0 a 1. Trabalhar com profundidade é digno de um tópico separado, portanto, retornaremos ao nosso sombreador.

A divisão em perspectiva também pode ser realizada no shader de vértice e os dados já preparados podem ser transferidos para o shader de fragmento.

 v2f vert (appdata v) { v2f o; o.uv = v.uv; o.color = v.color; o.vertex = UnityObjectToClipPos(v.vertex); o.grabPos = ComputeScreenPos (o.vertex); o.grabPos /= o.grabPos.w; return o; } 

Adicione um sombreador de fragmento, respectivamente.

 fixed4 frag (v2f i) : SV_Target { fixed4 = grabColor = tex2d(_GrabTexture, i.grabPos.xy); fixed4 texColor = tex2D(_MainTex, i.uv)*i.color; return grabColor; } 

Desligue o modo de mistura especificado, porque agora estamos implementando nosso modo de mesclagem dentro do shader de fragmento.

 //Blend SrcAlpha OneMinusSrcAlpha Blend Off 

E veja o resultado do GrabPass .

imagem

Parece que nada aconteceu, mas não é. Para maior clareza, apresentamos uma pequena mudança, para isso adicionaremos o valor da variável às coordenadas da textura. Para que possamos modificar a variável, adicione uma nova propriedade _DisplacementPower .

 Properties { [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} _Color ("Color" , Color) = (1,1,1,1) _DisplacementPower ("Displacement Power" , Float) = 0 } SubShader { Pass { ... float _DisplacementPower; ... } } 

E, novamente, faça alterações no shader de fragmento.

 fixed4 grabColor = tex2d(_GrabTexture, i.grabPos.xy + _DisplaccementPower); 

Op hop e resultado! Imagem com uma mudança.



Após uma mudança bem-sucedida, você pode prosseguir para uma distorção mais complexa. Utilizamos texturas pré-preparadas que armazenam a força de deslocamento no ponto especificado. Cor vermelha para o valor de deslocamento no eixo xe verde no eixo y.

Texturas usadas para distorção



Vamos começar. Adicione uma nova propriedade para armazenar a textura.

 _DisplacementTex ("Displacement Texture", 2D) = "white" {} 

E uma variável.

 sampler2D _DisplacementTex; 

No shader de fragmento, obtemos os valores de deslocamento da textura e os adicionamos às coordenadas da textura.

 fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); 

Agora, alterando os valores do parâmetro _DisplacementPower , não apenas mudamos a imagem original, mas a distorcemos.



Sobreposição


Agora, na tela, há apenas uma distorção do espaço, e o sprite, que mostramos no início, está ausente. Vamos devolvê-lo ao seu lugar. Para fazer isso, usaremos uma difícil mistura de cores. Faça outra coisa, como o modo de mesclagem de sobreposição. Sua fórmula é a seguinte:



onde S é a imagem original, C é corretiva, ou seja, nosso sprite, R é o resultado.

Transfira esta fórmula para o nosso shader.

 fixed4 color = grabColor < 0.5 ? 2*grabColor*texColor : 1-2*(1-texColor)*(1-grabColor); 

O uso de operadores condicionais em um shader é um tópico bastante confuso. Depende muito da plataforma e da API gráfica usada. Em alguns casos, declarações condicionais não afetarão o desempenho. Mas sempre vale a pena ter um retorno. O operador condicional pode ser substituído usando a matemática e os métodos disponíveis. Usamos a seguinte construção

 c = step ( y, x); r = c * a + (1 - c) * b; 

Função de passo
A função step retornará 1 se x for maior que ou igual a y . E 0 se x for menor que y .

Por exemplo, se x = 1 e y = 0,5, o resultado de c será 1. E a expressão a seguir será semelhante a
r = 1 * a + 0 * b
Porque multiplicando por 0 obtém 0, então o resultado será apenas o valor de a .
Caso contrário, se c for 0,
r = 0 * a + 1 * b
E o resultado final será b .

Reescreva a cor para o modo de sobreposição .

 fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); 

Certifique-se de considerar a transparência do sprite. Para fazer isso, usaremos interpolação linear entre as duas cores.

 color = lerp(grabColor, color ,texColor.a); 

Código completo do shader fragment.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * (2 * grabColor * texColor) + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

E o resultado do nosso trabalho.



Recurso GrabPass


Foi mencionado acima que o passe GrabPass {} captura o conteúdo da tela em uma textura _GrabTexture . Ao mesmo tempo, toda vez que essa passagem é chamada, o conteúdo da textura será atualizado.
A atualização constante pode ser evitada especificando o nome da textura na qual o conteúdo da tela será capturado.
 GrabPass{"_DisplacementGrabTexture"} 

Agora, o conteúdo da textura será atualizado apenas na primeira chamada do passe do GrabPass por quadro. Isso economiza recursos se houver muitos objetos usando o GrabPass {} . Mas se dois objetos se sobrepuserem, os artefatos serão perceptíveis, pois os dois objetos usarão a mesma imagem.

Usando GrabPass {"_ DisplacementGrabTexture"}.



Usando o GrabPass {}.



Animação


Agora é hora de animar nosso efeito. Queremos reduzir suavemente a força da distorção à medida que a onda de explosão cresce, simulando sua extinção. Para fazer isso, precisamos alterar as propriedades do material.

Script para animação
 public class Wave : MonoBehaviour { private float _elapsedTime; private SpriteRenderer _renderer; public float Duration; [Space] public AnimationCurve ScaleProgress; public Vector3 ScalePower; [Space] public AnimationCurve PropertyProgress; public float PropertyPower; [Space] public AnimationCurve AlphaProgress; private void Start() { _renderer = GetComponent<SpriteRenderer>(); } private void OnEnable() { _elapsedTime = 0f; } void Update() { if (_elapsedTime < Duration) { var progress = _elapsedTime / Duration; var scale = ScaleProgress.Evaluate(progress) * ScalePower; var property = PropertyProgress.Evaluate(progress) * PropertyPower; var alpha = AlphaProgress.Evaluate(progress); transform.localScale = scale; _renderer.material.SetFloat("_DisplacementPower", property); var color = _renderer.color; color.a = alpha; _renderer.color = color; _elapsedTime += Time.deltaTime; } else { _elapsedTime = 0; } } } 

E suas configurações


O resultado da animação.



Perrendererdata


Preste atenção na linha abaixo.

 _renderer.material.SetFloat("_DisplacementPower", property); 

Aqui, não estamos apenas alterando uma das propriedades do material, mas criando uma cópia do material de origem (apenas na primeira chamada deste método) e trabalhando com ele já. É uma opção bastante funcional, mas se houver mais de um objeto no palco, por exemplo, mil, a criação de tantas cópias não levará a nada de bom. Existe uma opção melhor - é usar o atributo [PerRendererData] no shader e o objeto MaterialPropertyBlock no script.

Para fazer isso, adicione um atributo à propriedade _DisplacementPower no shader.

 [PerRendererData] _DisplacementPower ("Displacement Power" , Range(-.1,.1)) = 0 

Depois disso, a propriedade não será mais exibida no inspetor, porque Agora é individual para cada objeto, que definirá os valores.



Retornamos ao script e fazemos alterações.

 private MaterialPropertyBlock _propertyBlock; private void Start() { _renderer = GetComponent<SpriteRenderer>(); _propertyBlock = new MaterialPropertyBlock(); } void Update() { ... //_renderer.material.SetFloat("_DisplacementPower", property); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat("_DisplacementPower", property); _renderer.SetPropertyBlock(_propertyBlock); ... } 

Agora, para alterar a propriedade, atualizaremos o MaterialPropertyBlock do nosso objeto sem criar cópias do material.

Sobre o SpriteRenderer
Vamos olhar para esta linha no shader.

 [PerRendererData] _MainTex ("Main Texture", 2D) = "white" {} 

SpriteRenderer funciona da mesma forma com sprites. Ele define a propriedade _MainTex como seu valor usando MaterialPropertyBlock . Portanto, no inspetor, a propriedade _MainTex não é exibida para o material e, no componente SpriteRenderer , especificamos a textura necessária. Ao mesmo tempo, pode haver muitos sprites diferentes no palco, mas apenas um material será usado para a renderização (se você não o alterar).

Recurso PerRendererData


Você pode obter o MaterialPropertyBlock de quase todos os componentes relacionados à renderização. Por exemplo, SpriteRenderer , ParticleRenderer , MeshRenderer e outros componentes do Renderer . Mas sempre há uma exceção, este é um CanvasRenderer . É impossível obter e alterar propriedades usando esse método. Portanto, se você escrever um jogo 2D usando componentes da interface do usuário, encontrará esse problema ao escrever sombreadores.

Rotação


Um efeito desagradável ocorre quando a imagem é girada. No exemplo de uma onda redonda, isso é especialmente perceptível.

A onda certa ao girar (90 graus) gera outra distorção.



Vermelho indica os vetores obtidos do mesmo ponto na textura, mas com uma rotação diferente dessa textura. O valor do deslocamento permanece o mesmo e não considera a rotação.

Para resolver esse problema, usaremos a matriz de transformação unity_ObjectToWorld . Isso ajudará a recontar nosso vetor de coordenadas locais para coordenadas mundiais.

 float2 offset = (displPos.xy*2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld, offset); 

Mas a matriz também contém dados sobre a escala do objeto; portanto, ao especificar a intensidade da distorção, devemos levar em consideração a escala do próprio objeto.

 _propertyBlock.SetFloat("_DisplacementPower", property/transform.localScale.x); 

A onda direita também é girada em 90 graus, mas a distorção agora é calculada corretamente.



Clip


Nossa textura tem pixels transparentes suficientes (especialmente se usarmos o tipo de malha Rect ). O sombreador os processa, o que, neste caso, não faz sentido. Portanto, tentamos reduzir o número de cálculos desnecessários. Podemos interromper o processamento de pixels transparentes usando o método clip (x) . Se o parâmetro passado para ele for menor que zero, o sombreador terminará. Mas como o valor alfa não pode ser menor que 0, subtraímos dele um pequeno valor. Também pode ser colocado em propriedades ( Recorte ) e usado para cortar as partes transparentes da imagem. Nesse caso, não precisamos de um parâmetro separado, portanto, usaremos apenas o número 0,01 .

Código completo do shader fragment.

 fixed4 frag (v2f i) : SV_Target { fixed4 displPos = tex2D(_DisplacementTex, i.uv); float2 offset = (displPos.xy * 2 - 1) * _DisplacementPower * displPos.a; offset = mul( unity_ObjectToWorld,offset); fixed4 texColor = tex2D(_MainTex, i.uv + offset)*i.color; clip(texColor.a - 0.01); fixed4 grabColor = tex2D (_GrabTexture, i.grabPos.xy + offset); fixed s = step(grabColor, 0.5); fixed4 color = s * 2 * grabColor * texColor + (1 - s) * (1 - 2 * (1 - texColor) * (1 - grabColor)); color = lerp(grabColor, color ,texColor.a); return color; } 

PS: O código fonte do shader e do script é um link para o git . O projeto também possui um pequeno gerador de textura para distorção. O cristal com o pedestal foi retirado do ativo - 2D Game Kit.

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


All Articles