Escribiendo sombreadores en Unity. GrabPass, PerRendererData

Hola Me gustaría compartir mi experiencia escribiendo sombreadores en Unity. Comencemos con el sombreador de desplazamiento / refracción en 2D, considere la funcionalidad utilizada para escribirlo (GrabPass, PerRendererData) y también preste atención a los problemas que necesariamente surgirán.

La información es útil para aquellos que tienen una idea general de los sombreadores y trataron de crearlos, pero no están familiarizados con las capacidades que proporciona Unity y no saben qué lado abordar. Echa un vistazo, tal vez mi experiencia te ayudará a resolverlo.



Este es el resultado que queremos lograr.

imagen

Preparación


Primero, crea un sombreador que simplemente dibuje el sprite especificado. Él será nuestra base para futuras manipulaciones. Algo se le agregará, algo por el contrario se eliminará. Será diferente del estándar "Sprites-Default" por la ausencia de algunas etiquetas y acciones que no afectarán el 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 mostrar
El fondo es realmente transparente, intencionalmente oscurecido.

imagen

La pieza de trabajo resultante.

imagen

Grabpass


Ahora nuestra tarea es hacer cambios a la imagen actual en la pantalla, y para esto necesitamos obtener una imagen. Y el pasaje GrabPass nos ayudará con esto. Este pasaje capturará la imagen de la pantalla en textura _GrabTexture . La textura contendrá solo lo que se dibujó antes de que nuestro objeto con este sombreador se renderizara.

Además de la textura en sí, necesitamos las coordenadas del escaneo para obtener el color del píxel. Para hacer esto, agregue coordenadas de textura adicionales a los datos del sombreador de fragmentos. Estas coordenadas no están normalizadas (los valores no están en el rango de 0 a 1) y describen la posición de un punto en el espacio de la cámara (proyección).

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

Y en el sombreador de vértices, llénalos.

 o.grabPos = ComputeGrabScreenPos (o.vertex); 

Para obtener el color de _GrabTexture , podemos usar el siguiente método si usamos coordenadas no normalizadas

 tex2Dproj(_GrabTexture, i.grabPos) 

Pero usaremos un método diferente y normalizaremos las coordenadas nosotros mismos, usando la división en perspectiva, es decir. dividiendo a todos los demás en el componente w.

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

w componente
La división en un componente w es necesaria solo cuando se usa la perspectiva, en la proyección ortográfica siempre será 1. De hecho, w almacena el valor de la distancia, apunta a la cámara. Pero no es profundidad: z , cuyo valor debe estar en el rango de 0 a 1. Trabajar con profundidad es digno de un tema separado, por lo que volveremos a nuestro sombreador.

La división en perspectiva también se puede realizar en el sombreador de vértices, y los datos ya preparados se pueden transferir al sombreador de fragmentos.

 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; } 

Agregue un sombreador de fragmentos, respectivamente.

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

Desactiva el modo de mezcla especificado, porque ahora estamos implementando nuestro modo de fusión dentro del sombreador de fragmentos.

 //Blend SrcAlpha OneMinusSrcAlpha Blend Off 

Y mira el resultado de GrabPass .

imagen

Nada parece haber sucedido, pero no es así. Para mayor claridad, introducimos un ligero cambio, para esto agregaremos el valor de la variable a las coordenadas de textura. Para que podamos modificar la variable, agregue una nueva propiedad _DisplacementPower .

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

Y de nuevo, realice cambios en el sombreador de fragmentos.

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

¡Op hop y resultado! Imagen con turno.



Después de un cambio exitoso, puede proceder a una distorsión más compleja. Utilizamos texturas preparadas previamente que almacenarán la fuerza de desplazamiento en el punto especificado. Color rojo para el valor de desplazamiento en el eje x, y verde en el eje y.

Texturas utilizadas para distorsión



Empecemos Agregue una nueva propiedad para almacenar la textura.

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

Y una variable.

 sampler2D _DisplacementTex; 

En el sombreador de fragmentos obtenemos los valores de desplazamiento de la textura y los agregamos a las coordenadas de textura.

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

Ahora, al cambiar los valores del parámetro _DisplacementPower , no solo cambiamos la imagen original, sino que la distorsionamos.



Superposición


Ahora en la pantalla solo hay una distorsión del espacio, y el sprite, que mostramos al principio, está ausente. Lo devolveremos a su lugar. Para hacer esto, utilizaremos una mezcla difícil de colores. Tome algo más, como el modo de fusión de superposición. Su fórmula es la siguiente:



donde S es la imagen original, C es correctivo, es decir, nuestro sprite, R es el resultado.

Transfiere esta fórmula a nuestro sombreador.

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

El uso de operadores condicionales en un sombreador es un tema bastante confuso. Mucho depende de la plataforma y la API de gráficos utilizada. En algunos casos, las declaraciones condicionales no afectarán el rendimiento. Pero siempre vale la pena tener una reserva. El operador condicional se puede reemplazar utilizando las matemáticas y los métodos disponibles. Usamos la siguiente construcción

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

Función de paso
La función de paso devolverá 1 si x es mayor o igual que y . Y 0 si x es menor que y .

Por ejemplo, si x = 1 ey = 0.5, entonces el resultado de c será 1. Y la siguiente expresión se verá como
r = 1 * a + 0 * b
Porque multiplicando por 0 da 0, entonces el resultado será solo el valor de a .
De lo contrario, si c es 0,
r = 0 * a + 1 * b
Y el resultado final será b .

Reescribe el color para el modo de superposición .

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

Asegúrese de considerar la transparencia del sprite. Para hacer esto, utilizaremos la interpolación lineal entre los dos colores.

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

Código de sombreador de fragmento completo.

 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; } 

Y el resultado de nuestro trabajo.



Característica GrabPass


Se mencionó anteriormente que el pase GrabPass {} captura el contenido de la pantalla en una textura _GrabTexture . Al mismo tiempo, cada vez que se llama a este pasaje, se actualizará el contenido de la textura.
La actualización constante se puede evitar especificando el nombre de la textura en la que se capturará el contenido de la pantalla.
 GrabPass{"_DisplacementGrabTexture"} 

Ahora el contenido de la textura se actualizará solo en la primera llamada del pase GrabPass por fotograma. Esto ahorra recursos si hay muchos objetos usando GrabPass {} . Pero si dos objetos se superponen, los artefactos serán notables, ya que ambos objetos usarán la misma imagen.

Usando GrabPass {"_ DisplacementGrabTexture"}.



Usando GrabPass {}.



Animación


Ahora es tiempo de animar nuestro efecto. Queremos reducir suavemente la fuerza de distorsión a medida que crece la onda expansiva, simulando su extinción. Para hacer esto, necesitamos cambiar las propiedades del material.

Guión para animación
 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; } } } 

Y su configuración


El resultado de la animación.



Perrendererdata


Presta atención a la línea de abajo.

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

Aquí no solo estamos cambiando una de las propiedades del material, sino que estamos creando una copia del material fuente (solo en la primera llamada de este método) y ya estamos trabajando con él. Es una opción bastante funcional, pero si hay más de un objeto en el escenario, por ejemplo, mil, entonces crear tantas copias no conducirá a nada bueno. Hay una mejor opción: utilizar el atributo [PerRendererData] en el sombreador y el objeto MaterialPropertyBlock en el script.

Para hacer esto, agregue un atributo a la propiedad _DisplacementPower en el sombreador.

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

Después de eso, la propiedad ya no se mostrará en el inspector, porque Ahora es individual para cada objeto, que establecerá los valores.



Volvemos al guión y le hacemos cambios.

 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); ... } 

Ahora, para cambiar la propiedad, actualizaremos el MaterialPropertyBlock de nuestro objeto sin crear copias del material.

Sobre SpriteRenderer
Veamos esta línea en el sombreador.

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

SpriteRenderer funciona de manera similar con los sprites. Establece la propiedad _MainTex en su valor utilizando MaterialPropertyBlock . Por lo tanto, en el inspector, la propiedad _MainTex no se muestra para el material, y en el componente SpriteRenderer especificamos la textura que necesitamos. Al mismo tiempo, puede haber muchos sprites diferentes en el escenario, pero solo se usará un material para su representación (si no lo cambia usted mismo).

PerRendererData Feature


Puede obtener MaterialPropertyBlock de casi todos los componentes relacionados con el render. Por ejemplo, SpriteRenderer , ParticleRenderer , MeshRenderer y otros componentes de Renderer . Pero siempre hay una excepción, este es un CanvasRenderer . Es imposible obtener y cambiar propiedades usando este método. Por lo tanto, si escribe un juego en 2D utilizando componentes de la interfaz de usuario, encontrará este problema al escribir sombreadores.

Rotación


Se produce un efecto desagradable cuando se gira la imagen. En el ejemplo de una ola redonda, esto es especialmente notable.

La onda derecha al girar (90 grados) produce otra distorsión.



El rojo indica los vectores obtenidos del mismo punto en la textura, pero con una rotación diferente de esta textura. El valor de compensación sigue siendo el mismo y no tiene en cuenta la rotación.

Para resolver este problema, utilizaremos la matriz de transformación unity_ObjectToWorld . Ayudará a contar nuestro vector desde coordenadas locales a coordenadas mundiales.

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

Pero la matriz también contiene datos sobre la escala del objeto, por lo que al especificar la fuerza de la distorsión, debemos tener en cuenta la escala del objeto en sí.

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

La onda derecha también gira 90 grados, pero la distorsión ahora se calcula correctamente.



Clip


Nuestra textura tiene suficientes píxeles transparentes (especialmente si usamos el tipo de malla Rect ). El sombreador los procesa, lo que en este caso no tiene sentido. Por lo tanto, intentaremos reducir la cantidad de cálculos innecesarios. Podemos interrumpir el procesamiento de píxeles transparentes utilizando el método clip (x) . Si el parámetro que se le pasa es menor que cero, el sombreador finalizará. Pero como el valor alfa no puede ser menor que 0, restaremos un valor pequeño de él. También se puede poner en propiedades ( Recorte ) y usar para cortar las partes transparentes de la imagen. En este caso, no necesitamos un parámetro separado, por lo que solo usaremos el número 0.01 .

Código de sombreador de fragmento completo.

 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; } 

PD: El código fuente para el sombreador y el script es un enlace a git . El proyecto también tiene un pequeño generador de textura para distorsión. El cristal con el pedestal fue tomado del activo - Kit de juego 2D.

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


All Articles