Stylized Fog Shader: replicando Firewatch
Me encantan los shaders. Uno de los primeros experimentos que realice al comenzar a transitar el camino de Technical Artist fue este increíble stylized fog shader al estilo Firewatch. En este post voy a enseñarte como conseguirlo utilizando Unreal Engine.
Escena y materiales en Unreal Engine
Comenzaremos creando una escena básica en Unreal Engine con algunas formas geométricas dispersas. Nada demasiado elaborado, solo las utilizaremos para demostrar el efecto de distancia de nuestro shader.
Ahora vamos a crear los materiales necesarios para comenzar nuestro shader de fog estilizado. En el Content Browser vamos a generar un Material llamado M_StylizedFog, una Material Instance derivada de M_stylizedFog y una Material Fuction llamada MF_StylizedFog. Estos elementos se encuentran en el menú contextual, al hacer clic derecho sobre el Content Browser > Materials.
Lo siguiente será agregar un objeto Post Process Volume a nuestra escena. Al seleccionarlo iremos al panel Details (a la derecha) y marcaremos la casilla Infinite Extent que se encuentra en la pestaña Post Process Volume Settings. Además, en Rendering Features agregaremos nuestra Material Instance (MI) como elemento de la lista.
Una vez creados nuestros assets preparativos comenzaremos por editar MF_StylizedFog.
Material Function: color y profundidad de la escena
Una Material Function (MF) es un tipo de Material dentro de Unreal Engine que nos permite encapsular operaciones y logica del funcionamiento del shader. Las Material Functions se pueden reutilizar según lo necesitemos, así como lo hacen las funciones en programación, de ahí el nombre.
Las Material Functions tienen datos de entrada con los que, al igual que en programación por código, realizan operaciones y devuelven datos de salida (un valor) al shader principal que las invoca.
Nuestra MF necesitará dos datos de entrada y dos datos de salida, ya que este shader necesita computar operaciones para aplicar el color al fog y calcular la distancia a la cual se aplica dicho color. Así es, este efecto se basa en capas de colores actuando en conjunto.
Aplicando la capa de color a la escena
Como primer paso dentro de nuestra MF, vamos a utilizar el nodo de salida que viene creado por defecto y le cambiaremos el nombre a “Color”. Esto lo podemos hacer seleccionando el nodo y yendo al panel de detalles de la izquierda, en la casilla Output Name.
Comenzaremos con el nodo SceneTexture. Una vez creado este nodo y después de seleccionarlo, en el panel de detalles (a la izquierda) encontraremos una opción desplegable llamada Scene Texture Id, de la cual debemos seleccionar PostProcessInput0, ya que nuestro shader será aplicado como un efecto de Post Procesado.
Este nodo se encarga de proveernos con el color de cada pixel en la escena, y dado que el input del post processing proviene del pase de renderizado anterior, este nodo nos ayuda a mezclar el color de los píxeles con cada uno de estos pases.
Lo siguiente es enmascarar los canales RGB del output de color, ya que no necesitamos el canal alpha de dicha propiedad. Esto lo logramos con el nodo Component Mask. Dentro de las propiedades de este nodo (panel izquierdo) podemos encontrar los distintos canales que podemos enmascarar.
Ahora vamos a necesitar el color con el cual vamos a mezclar los píxeles de nuestra escena. Para ello utilizaremos el nodo Function Input. Este es un parámetro de entrada para nuestra función. Al igual que el Function Output, cambiaremos su nombre a “Color” y su propiedad Input Type a Function Input Vector 4.
Al igual que con el nodo Scene Texture vamos a enmascarar el resultado de salida de este nodo, esta vez utilizando los cuatro canales (RGBA) pero dividiendo el enmascarado en dos nodos: uno para obtener el color (RGB) y otro para obtener solo el canal alpha (A).
Ahora solo nos queda hacer una interpolación lineal entre los nodos que creamos. Una interpolación lineal permite obtener un “promedio” entre dos valores a través de un tercer valor que actúa como mascara.
Nuestra interpolación lineal será entre nuestra Scene Texture y nuestra Function Input, utilizando el canal alpha de esta última como la máscara de control. Usaremos el nodo Linear Interpolate para ello.
En shaders los valores que se utilizan para realizar operaciones son rangos normalizados (de 0-1) casi siempre.
El nodo Component Mask que almacena el canal alpha del Input Color controla el porcentaje de mezclado. Si es 0 el color será el valor provisto por Scene Texture; si es 1 el color será el provisto por Input Color. Todos los valores que pasen entre 0 y 1 serán una mezcla promediada entre dichos nodos.
En resumen: aplicamos color a la escena obteniendo el color del pixel actual (del pase de render anterior) de la escena y aplicando una mezcla nueva de color sobre dicho pixel con un nuevo color de entrada. Ya que comprendemos esta parte solo nos queda conectar nuestro nodo Lerp a nuestro Output Color.
Calculando la profundidad de la escena
Ya tenemos nuestro color “aplicado” a la escena, pero aún nos hace falta controlar la distancia a la cual este color va a comenzar a crear el efecto de fog.
Para ello crearemos un nuevo Function Input y Function Outoput dentro de nuestra MF. Cambiaremos su nombre a “Distance” y a nuestro parámetro de entrada le colocaremos un Input Type de Function Input Scalar.
Ahora vamos a obtener la profundidad de nuestra escena utilizando el nodo Scene Depth. Este nodo nos proporciona una tipo de interpolación lineal entre nuestra cámara y los objetos que se renderizan en pantalla, usando la distancia entre estos como mascara de control.
Realizaremos una división entre el nodo Scene Depth e Input Distance. Esta división servirá para controlar la distancia a la cual, a partir de la profundidad de la escena, el efecto de fog comenzara a renderizarse en pantalla, utilizando el parámetro de entrada “Distance” como valor divisor.
Por último conectaremos nuestra división a un nodo Saturate. En ocasiones las operaciones con shaders puede devolvernos valores no normalizados que van desde -1 hasta 1. Saturate ayuda a anclar y re-mapear el resultado a un rango normalizado entre 0 y 1 para no contener valores negativos.
Finalmente, conectamos nuestro nodo Saturate con nuestro Output Distance.
Ahora nuestra MF está completa y lista para ser implementada en nuestro shader principal, el cual es el siguiente paso a seguir. Así es como debe lucir la Material Function en su totalidad.
Material: implementando nuestra Material Function para crear fog estilizado
Ya tenemos nuestra Material Function completa. Ahora debemos construir nuestro material principal para completar nuestro efecto y para ello vamos a editar nuestro material M_StylizedFog.
Vamos a utilizar este shader como un efecto de Post Processing, así que vamos a modificar el Material Domain cambiándolo a Post Process. Ahora cambiaremos la opción Blendable Location a Before Traslucency. Estas opciones se encuentran en el panel de detalles de nuestro material (izquierda).
El primer paso de nuestro shader es uno ya conocido. Vamos a obtener el color del pixel de nuestra escena en pantalla al igual que hicimos en MF_StylizedFog y a obtener sus componentes RGB.
Ahora crearemos un nodo de tipo Vector Parameter al cual llamaremos “Color 1″. Este nodo representa valores en un vector de cuatro dimensiones que pueden ser utilizados como colores (RGB) o valores escalares, tú eliges su finalidad. En este caso lo usaremos para representar color.
Necesitamos obtener los cuatro canales de nuestro Color 1. Podemos hacer esto utilizando el pin superior del nodo, pero por defecto Unreal nos devuelve únicamente los canales RGB de esta forma, así que debemos componer nuestro propio nodo de cuatro canales. Para lograr esto necesitamos hacer uso del nodo Append Vector.
Append Vector funciona como un “compuesto”. Los valores que ingresemos en este nodo se combinaran para crear un vector nuevo. Al combinar el pin de tres dimensiones/canales de nuestro nodo Color 1 con el pin de una dimensión del canal alpha, obtendremos un nuevo vector compuesto de cuatro dimensiones: RGB + A = RGBA.
A continuación vamos a crear un nodo Scalar Parameter, el cual nos servirá como controlador de la distancia de renderizado de nuestro sistema de fog. Nombraremos a este nodo como “Color 1 Distance”.
Aquí es donde llega la magia. Crearemos un nodo llamado Material Function Call; una vez creado y seleccionado, revisaremos sus propiedades y en el dropdown de Material Function seleccionaremos nuestra MF previamente creada.
Conectaremos nuestros nodos previos. Como último paso para esta parte vamos a realizar un Lerp entre nuestra Scene Texture y el output de nuestra MF actual. Este resultado lo conectaremos a nuestro Master Material. Debería verse de la siguiente manera.
Si abrimos nuestra MI para editarla, nos daremos cuenta de que tenemos disponibles nuestros parámetros para jugar con ellos. En este caso vamos a colocar un color de fog naranja.
Al terminar de editarla debemos guardar nuestro asset y observar el cambio en nuestra escena.
Aún hace falta una cosa importante. Un efecto similar al de Firewatch, utiliza distintas capas de colores de fog. Para conseguirlo configuraremos nuestro shader principal a modo de cascada, con interpolaciones lineales de acuerdo al número de colores que necesitemos o queramos crear. Se vería algo así:
Recuerda configurar y jugar con los valores de la Material Instance para crear diferentes efectos. En mi caso, este es el resultado final:
Siguientes pasos
Ahora ya sabes como crear un efecto de neblina similar al estilo implementado en Firewatch, pero… ¡Esto no termina aquí!
Esta base funciona muy bien, pero puedes mejorar este efecto, por ejemplo: ¿sabías que puedes mejorar la intensidad de los colores? Intenta lograrlo a modo de reto personal y como pista: una multiplicación puede hacer maravillas.
Si quieres aprender un poco más de Unreal, te recomiendo este post en el cual te enseño a crear empaquetamiento de texturas para ahorrar recursos. Sigue practicando y experimenta mucho con los nodos, colores, funciones y variantes que se te ocurran. Hasta la próxima.