Crea un efecto de distribución de color en Unity


Este efecto fue inspirado por el episodio de Powerpuff Girls . Quería crear el efecto de la propagación del color en un mundo en blanco y negro, pero implementarlo en las coordenadas del espacio mundial , para ver cómo el color pinta los objetos , y no simplemente extenderse en la pantalla, como en una caricatura.

Creé el efecto en el nuevo Lightweight Rendering Pipeline del motor de Unity, un ejemplo incorporado del Scriptable Rendering Pipeline pipeline. Todos los conceptos se aplican a otras canalizaciones, pero algunas funciones o matrices incorporadas pueden tener nombres diferentes. También utilicé la nueva pila de procesamiento posterior, pero en el tutorial omitiré una descripción detallada de su configuración, porque se describe bastante bien en otros manuales, por ejemplo, en este video .



El efecto del postprocesamiento en escala de grises


Solo como referencia, así es como se ve una escena sin efectos de procesamiento posterior.


Para este efecto, utilicé el nuevo paquete Post-Processing de Unity 2018, que se puede descargar desde el administrador de paquetes. Si no sabe cómo usarlo, le recomiendo este tutorial .

Escribí mi propio efecto extendiendo las clases PostProcessingEffectSettings y PostProcessEffectRenderer escritas en C #, cuyo código fuente se puede ver aquí . De hecho, no hice nada particularmente interesante con estos efectos en el lado de la CPU (en código C #) excepto que agregué un grupo de propiedades generales al Inspector, por lo que no explicaré cómo hacerlo en el tutorial. Espero que mi código hable por sí mismo.

Pasemos al código del sombreador y comencemos con el efecto de escala de grises. En el tutorial, no modificaremos el archivo shaderlab, las estructuras de entrada y el sombreador de vértices, por lo que puede ver su código fuente aquí . En cambio, nos encargaremos del sombreador de fragmentos.

Para convertir un color a una escala de grises, reducimos el valor de cada píxel a un valor de luminancia que describa su brillo . Esto se puede hacer tomando el producto escalar del valor de color de la textura de la cámara y el vector ponderado , que describe la contribución de cada canal de color al brillo general del color.

¿Por qué utilizamos productos escalares? No olvide que los productos escalares se calculan de la siguiente manera:

dot(a, b) = a x * b x + a y * b y + a z * b z

En este caso, multiplicamos cada canal del valor de color por peso . Luego agregamos estos productos para reducirlos a un solo valor escalar. Cuando el color RGB tiene los mismos valores en los canales R, G y B, el color se vuelve gris.

Así es como se ve el código del sombreador:

 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 

Si el sombreador base está configurado correctamente, el efecto de postprocesamiento debería colorear toda la pantalla en escala de grises.




Representación del efecto de color en el espacio mundial.


Como se trata de un efecto de procesamiento posterior, no tenemos información sobre la geometría de la escena en el sombreador de vértices. En la etapa de postprocesamiento, la única información que tenemos es la imagen renderizada por la cámara y el espacio de las coordenadas truncadas para muestrearla. Sin embargo, queremos que el efecto de coloración se extienda a través de los objetos, como si estuviera sucediendo en el mundo, y no solo en una pantalla plana.

Para dibujar este efecto en la geometría de la escena, necesitamos las coordenadas del espacio mundial de cada píxel. Para pasar de las coordenadas del espacio de coordenadas truncadas a las coordenadas del espacio mundial , necesitamos realizar una transformación del espacio de coordenadas .

Por lo general, para pasar de un espacio de coordenadas a otro, se necesita una matriz que defina la transformación del espacio de coordenadas A al espacio B. Para ir de A a B, multiplicamos el vector en el espacio de coordenadas A por esta matriz de transformación. En nuestro caso, realizaremos la siguiente transición: el espacio de coordenadas truncadas (espacio de clip) -> ver espacio (ver espacio) -> espacio mundial (espacio mundial) . Es decir, necesitamos la matriz clip-to-view-space y la matriz view-to-world-space que proporciona Unity.

Sin embargo, las coordenadas de Unity del espacio de coordenadas truncado no tienen un valor z que determine la profundidad del píxel o la distancia a la cámara. Necesitamos este valor para movernos desde el espacio de coordenadas truncadas al espacio de especies. ¡Comencemos con esto!

Obtener el valor del búfer de profundidad


Si la canalización de representación está habilitada, dibuja una textura en la ventana gráfica que almacena los valores z en una estructura llamada búfer de profundidad . ¡Podemos muestrear este búfer para obtener el valor z faltante de nuestro espacio de coordenadas de coordenadas truncadas!

Primero, asegúrese de que el búfer de profundidad se renderice haciendo clic en la sección "Agregar datos adicionales" de la cámara en el Inspector y verificando que la casilla "Requiere textura de profundidad" esté marcada. También asegúrese de que la opción Permitir MSAA esté habilitada para la cámara. No sé por qué es necesario comprobar este efecto, pero lo es. Si se dibuja el búfer de profundidad, en el depurador de cuadros debería ver la etapa "Prepasar profundidad" .

Cree una muestra _CameraDepthTexture en el archivo hlsl

 TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 

Ahora escribamos la función GetWorldFromViewPosition y por ahora la usaremos para verificar el búfer de profundidad . (Más tarde lo ampliaremos para obtener una posición en el mundo).

 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 

En el sombreador de fragmentos, dibuje el valor de la muestra de textura de profundidad.

 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 

Así son mis resultados cuando solo hay una llanura montañosa en la escena (apagué todos los árboles para simplificar aún más la prueba de los valores del espacio mundial). Su resultado debería ser similar. Los valores en blanco y negro describen la distancia desde la geometría a la cámara.


Estos son algunos pasos que puede seguir si encuentra problemas:

  • Asegúrese de que la cámara tenga habilitada la representación de profundidad.
  • Asegúrese de que la cámara tenga habilitado MSAA.
  • Intente cambiar el plano cercano y lejano de la cámara.
  • Asegúrese de que los objetos que espera ver en el búfer de profundidad usan un sombreador con un paso de profundidad. Esto asegura que el objeto atrae al búfer de profundidad. Todos los sombreadores estándar en LWRP hacen esto.

Obteniendo valor en el espacio mundial


Ahora que tenemos toda la información necesaria para el espacio de coordenadas truncadas , transformemos al espacio de especies y luego al espacio mundial .

Tenga en cuenta que las matrices de transformación requeridas para estas operaciones ya están en la biblioteca SRP. Sin embargo, están contenidos en la biblioteca C # del motor de Unity, por lo que los inserté en el sombreador en la función Render del script ColorSpreadRenderer :

 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 

Ahora ampliemos nuestra función GetWorldFromViewPosition.

Primero, necesitamos obtener la posición en la vista multiplicando la posición en el espacio de coordenadas truncado por la matriz de proyección inversa . También necesitamos hacer más magia vudú con una posición en la pantalla, que está relacionada con la forma en que Unity almacena su posición en el espacio de las coordenadas truncadas.

Finalmente, podemos multiplicar la posición en la ventana gráfica por ViewToWorldMatrix para obtener la posición en el espacio mundial .

 float3 GetWorldFromViewPosition (VertexOutput i) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

Hagamos una comprobación para asegurarnos de que las posiciones en el espacio global son correctas. Para hacer esto, escribí un sombreador que devuelve solo la posición de un objeto en el espacio mundial ; Este es un cálculo bastante simple basado en un sombreador regular, cuya exactitud se puede confiar. Desactive el efecto del postprocesamiento y tome una captura de pantalla de este sombreador de prueba para el espacio mundial . Mi después de aplicar el sombreador a la superficie de la tierra en la escena se ve así:


(Tenga en cuenta que los valores en el espacio mundial son mucho mayores que 1.0, así que no se preocupe de que estos colores tengan sentido; en su lugar, asegúrese de que los resultados sean los mismos para las respuestas "verdaderas" y "calculadas"). A continuación, volvamos a la prueba el objeto es material ordinario (y no el material de prueba del espacio mundial) y luego vuelve a activar el efecto de procesamiento posterior. Mis resultados se ven así:


Esto es completamente similar al sombreador de prueba que escribí, es decir, ¡los cálculos del espacio mundial probablemente sean correctos!

Dibujando un círculo en el espacio mundial


Ahora que tenemos posiciones en el espacio mundial , ¡podemos dibujar un círculo de color en la escena! Necesitamos establecer el radio dentro del cual el efecto dibujará color. Afuera, el efecto representará la imagen en escala de grises. Para configurarlo, debe ajustar los valores para el radio del efecto ( _MaxSize ) y el centro del círculo (_Center). Establecí estos valores en la clase C # ColorSpread para que sean visibles en el inspector. Expandamos nuestro sombreador de fragmentos forzándolo a verificar si el píxel actual está dentro del radio del círculo :

 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

Finalmente, podemos dibujar el color en función de si está dentro de un radio en el espacio mundial . ¡Así es como se ve el efecto base!




Agregar efectos especiales


Veré un par de técnicas más utilizadas para hacer que el color se extienda por el suelo. El efecto completo tiene mucho más, pero el tutorial ya se ha vuelto demasiado grande, por lo que nos limitaremos a lo más importante.

Animación de ampliación de círculo


Queremos que el efecto se extienda por todo el mundo, es decir, como si estuviera creciendo. Para hacer esto, debe cambiar el radio dependiendo del tiempo.

_StartTime indica la hora a la que el círculo debería comenzar a crecer. En mi proyecto, utilicé un script adicional que le permite hacer clic en cualquier lugar de la pantalla para iniciar el crecimiento del círculo; en este caso, la hora de inicio es igual a la hora en que se hizo clic en el mouse.

_GrowthSpeed ​​establece la velocidad de aumentar el círculo.

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

También necesitamos actualizar la verificación de distancia para comparar la distancia actual con el radio creciente del efecto , y no con _MaxSize.

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

Así es como debería verse el resultado:


Agregando al radio de ruido


Quería que el efecto fuera más como una mancha de pintura, no solo un círculo creciente. Para hacer esto, agreguemos ruido al radio del efecto para que la distribución sea desigual.

Primero necesitamos muestrear la textura en el espacio mundial . Las coordenadas UV de i.screenPos se encuentran en el espacio de la pantalla , y si tomamos muestras en función de ellas, la forma del efecto se moverá con la cámara; así que usemos las coordenadas en el espacio mundial . Agregué el parámetro _NoiseTexScale para controlar la escala de la muestra de textura de ruido , porque las coordenadas en el espacio mundial son bastante grandes.

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

Ahora muestreemos la textura de ruido y agreguemos este valor al radio del efecto. Usé la escala _NoiseSize para tener más control sobre el tamaño del ruido.

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

Así es como se ven los resultados después de algunos ajustes:




En conclusión


¡Puedes seguir las actualizaciones de los tutoriales en mi Twitter y en Twitch me paso codificando transmisiones! (Además, transmito juegos de vez en cuando, así que no te sorprendas si me ves sentado en pijama y jugando Kingdom Hearts 3.)

Agradecimientos

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


All Articles