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.

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 spriteShader "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 exibirO fundo é realmente transparente, intencionalmente escurecido.

A peça resultante.

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 wA 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.
E veja o resultado do
GrabPass .

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)
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 passoA 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; } } }
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() { ...
Agora, para alterar a propriedade, atualizaremos o
MaterialPropertyBlock do nosso objeto sem criar cópias do material.
Sobre o SpriteRendererVamos 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.