DEV Community

Cover image for ¿Cómo crear un shader toon con un modelo de luz custom y Shader Graph?
Jesús Caro
Jesús Caro

Posted on • Edited on

¿Cómo crear un shader toon con un modelo de luz custom y Shader Graph?

Antes empezar

Proyecto en GitHub

Informacion util

Proyecto en el que se basa este ejemplo

Introducción

Muchas veces hemos observado en video juegos (estrictamente 3D), efectos en su estilo de render que nos parecieron interesantes, ya sea porque ayudaba a sumergirte mas en la experiencia de juego o por hacernos sentir mas identificados con el "look" del entorno. En este caso particular tomaremos como tema el sombreado ("shading") cartoon o modelo de luz irreal (Fig. 1). Este es un modelo de luz basado en Lambert, el modelo standar o base pare cualquier modelo de luz y el modelo de luz Specular para ayudarnos a reflejar luz.

[Fig. 1]
Alt Text

Desarrollo

Lo primero que necesitaremos es ir a nuestro hub de unity y generar un nuevo proyecto (version: unity 2020) llamado toon-nextgen (Fig. 2). Pulsando Ctrl + n (Cmd + n) se generará una nueva scena, la cual llamaremos toon.

[Fig. 2]
Alt Text

Crear un Grafo de Shader Graph

Lo primero que haremos será crear una carpeta Assets/Shaders (Fig. 3), entraremos dentro de la carpeta y generaremos un grafo haciendo click derecho (Fig. 4) de tipo Unlit Graph (ya que nosotros haremos nuestro propio modelo de luz), este se llamará ToonNextGen.

[Fig. 3]
Alt Text

[Fig. 4]
Alt Text

Shader graph es un sistema de nodos que conforman operaciones de de CG para el sombreado de un objeto, ya sea este 2D o 3D (en realidad siempre es 3D en Unity pero así se llama al render de sprites). Las operaciones mas sencillas que veremos serán Add y Multiply, que nos serviran para mezclar capas de colores, ya que al final es lo que estamos haciendo combinar operaciones de colores para dar un resultado en el render.

Empezaremos por dar doble click a nuestro grafo, debería abrirse un banco de trabajo (Fig. 5) con una cuadrícula, una vista de propiedades a la izquierda y una vista de preview de nuestro shader en un modelo standar (en este caso una esfera). Con la rueda del mouse podemos hacer zoom in/zoom out, tambien podemos mover con el click sostenido todos los nodos incluidos preview y properties, ademas de poder re escalar estas dos ultimas haciendo click en la linea gris de la parte inferior derecha. Lo primero en lo que debemos concentrarnos es en el nodo maestro "Unlit Master" (Fig. 6) del cual para este proyecto solo usaremos la entrada "color".

[Fig. 5]
Alt Text

[Fig. 6]
Alt Text

Color y textura

Para esta práctica suaremos un modelo 3D inspirado en the legend of zelda creado por Christoph Schoch Link BOTW model rigged. Este modelo lo guardaremos en un folder dentro de Assets que llamaremos "Models". De este proyecto solo necesitaremos la carpeta de texturas y mesh. Por default los materiales estarán en negro, esto no importa ya que los cambiaremos por nuestro shader.

Devuelta en nuestro grafo, necesitaremos dos nodos, "Color" y un "Sample Texture 2D", para ello hay que agregar dos propiedades con el boton [+] en el panel de propiedades (Fig. 7) Agregaremos un color y una texture 2D que nombraremos Albedo y Main Texture respectivamente. Combinaremos los el color y la textura pulsando la barra espaciadora, hay que buscar el nodo multiply, Sample Texture 2D. En la entada de textura arrastraremos nuestra propiedad Main Texture y la conectaremos, la salida de este nodo lo multiplicaremos con la propiedad albedo (debes arrastrar la propiedad albedo y conectarla), la salida de multiply debe conectarse a la entrada color de nuestro nodo maestro, de aquí en adelante ya sabes como usar propiedade, conectar y buscar nodos (Fig. 8).

[Fig. 7]
Alt Text

[Fig. 8]
Alt Text

Lambert y dirección de luz

Para poder generar un modelo de luz debemos comprender como se genera el sombreado basico de un cuerpo, esto puede explicarse por medio de Lambert: El brillo aparente de una superficie lambertiana para un observador es el mismo independientemente del ángulo de visión del observador. Más técnicamente, la luminancia de la superficie es isotrópica, y la intensidad luminosa obedece la ley del coseno de Lambert. La reflectancia lambertiana lleva el nombre de Johann Heinrich Lambert, quien introdujo el concepto de difusión perfecta en su libro Photometria de 1760.

En un resumen, lambert define la caida (falloff) de la sombra como el producto punto entre la direccion de la fuente de luz y la normal del cuerpo (la normal es una linea que nace desde el cuerpo hasta su sentido contrario como vector, osea en dirección hacia al frente o forward de donde se golpea con el rayo de luz).
Lambert

Empezaremos por obtener la dirección de la luz, lamentablemente Unity Technologies aun no ha creado un nodo para obtener las características de la fuente de luz principal con shader graph, por lo que tendremos que genera un script de HLSL (High Level Shader Language). En el proyecto debemos generar un archivo de texto o con extension hlsl (en realidad lo que hace shader graph es leer el contenido del archivo, es shader graph quien le da formato y lo compila, similar a glsl cuando lo cargas), en mi caso lo haré desde powershell ya que me es más práctico de esta forma.
Alt Text

Alt Text

Para comprender como funciona el scripting en shader graph te recomendamos antes leer esto:
Built-in Shader Functions to URP
Built-in Shader Helper Functions
Custom Function Node

Ahora debes abrir el archivo con tu editor de codigo preferido (en este caso usamos Visual Studio Code) y escribiras este script:

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten)
{
    #if SHADERGRAPH_PREVIEW
        Direction = half3(0.5, 0.5, 0);
        Color = 1;
        DistanceAtten = 1;
        ShadowAtten = 1;
    #else
        #if SHADOWS_SCREEN
            half4 clipPos = TransformWorldToHClip(WorldPos);
            half4 shadowCoord = ComputeScreenPos(clipPos);
        #else
            half4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
        #endif
            Light mainLight = GetMainLight(shadowCoord);
            Direction = mainLight.direction;
            Color = mainLight.color;
            DistanceAtten = mainLight.distanceAttenuation;
        #if !defined(_MAIN_LIGHT_SHADOWS) || defined(_RECEIVE_SHADOWS_OFF)
            ShadowAtten = 1.0h;
        #endif
        #if SHADOWS_SCREEN
            ShadowAtten = SampleScreenSpaceShadowmap(shadowCoord);
        #else
            ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
            half shadowStrength = GetMainLightShadowStrength();
            ShadowAtten = SampleShadowmap(shadowCoord, TEXTURE2D_ARGS(_MainLightShadowmapTexture,
            sampler_MainLightShadowmapTexture),
            shadowSamplingData, shadowStrength, false);
        #endif
    #endif
}

Enter fullscreen mode Exit fullscreen mode

Para crear nuestro nodo MainLight debemos generar un nuevo sub graph al que llamaremos MainLight.
Alt Text

Una vez dentro del shader debemos crear un Custom Function, el nodo ya existe. Este nodo necesitará las siguientes entradas y salidas (pulsa el simbolo de engrane para abrir el panel). Para agregar o quitar entradas y salidas pulsa el boton menos y mas en su respectiva sección. Debes poner nombre a la función en este caso MainLight sin espacios (si separas con espacio dará error), por ultimo elige el type como file y en la casilla de abajo arrastra tu archivo con tu script de hlsl.

Alt Text

Una vez configurada tu Custom Function conecta un nodo de position al WorldPos (es la unica entrada) y en el nodo de salida de tu bus grafo necesitaras crear las 4 salidas Direction, Color, DistanceAtten y ShadownAtten (pica al engrane para abrir el panel).
Alt Text

Ahora ya podemos usar nuestra MainLight, lo que haremos es usar la dirección de la luz para crear un producto punto con un Normal Vector, aplicaremos una saturación (para filtrar la intensidad entre 0 y 1, con esto podemos sabes en un rango mas simple si hay o no luz) y lo multiplicaremos por nuestro color principal (la textura y color), como un paso opcional si quieres agrupar nodos puedes seleccionarlos hacer click derecho y elegir Group Selection.
Alt Text
Alt Text

Modelo de luz Specular

Similar a la iluminación difusa, la iluminación especular se basa en el vector de dirección de la luz y los vectores normales del objeto, pero esta vez también se basa en la dirección de la vista desde donde el jugador está mirando. La iluminación especular se basa en las propiedades reflectantes de las superficies. Si pensamos en la superficie del objeto como un espejo, la iluminación especular es más fuerte dondequiera que veamos la luz reflejada en la superficie. Puedes ver este efecto en la siguiente imagen:
Specular

Calculamos un vector de reflexión reflejando la dirección de la luz alrededor del vector normal. Luego calculamos la distancia angular entre este vector de reflexión y la dirección de la vista. Cuanto más cerca esté el ángulo entre ellos, mayor será el impacto de la luz especular. El efecto resultante es que vemos un poco de luz cuando miramos la dirección de la luz reflejada a través de la superficie.

Para iniciar con el modelo de luz debemos generar un nuevo HLSL que nombraremos DirectSpecular.
Alt Text

Dentro de este script escribiremos lo siguiente:

void DirectSpecular_half(half3 Specular, half Smoothness, half3 Direction, half3 Color, half3 WorldNormal, half3 WorldView, out half3 Out)
{
    #if SHADERGRAPH_PREVIEW
        Out = 0;
    #else
        Smoothness = exp2(10 * Smoothness + 1);
        WorldNormal = normalize(WorldNormal);
        WorldView = SafeNormalize(WorldView);
        Out = LightingSpecular(Color, Direction, WorldNormal, WorldView, half4(Specular, 0), Smoothness);
    #endif
}
Enter fullscreen mode Exit fullscreen mode

Una vez tengamos nuestro script listo, deberemos crear otro Sub Graph al que nombraremos DirectSpecular. Dentro del grafo debemos generar una Custom Function y llenarlo de la siguiente manera:
Alt Text

Hay que generar Specular (color), Smoothness (Vector1), Direction (Vector3) y Color (Es el color de la luz).
Alt Text

Por ultimo hay que conectar las propiedades a las entradas de nuestra Custom Function y agregar los nodos Normal Vector y View Direction a su respectiva entrada.
Alt Text

Ahora ya podemos usar nuestro nodo DirectSpecular en el grafo principal. Para implementarlo bastará con enviarle la dirección de la luz desde Main Light y calcular el color y sombras de esta con la siguiente operación:
Alt Text

Debes agregar los propiedades Specular (Color) y Smoothness (vector1).
Alt Text

Ahora, el Lambert que calculamos anterior mente debe multiplicarse con el color de la luz que obtuvimos, el resultado de esta operacion su sumará con la salida de Direct Specular, con esto deberiamos ver el efecto de reflejo de luz.

Alt Text
Alt Text

Shader hasta este punto:
Alt Text

Ahora crea un material nuevo y asigna el shader que acabas de crear.
Alt Text

Pon el Specular en color blanco o algún color no obscuro para que se pueda pintar (negro significa la ausencia de color) y agrega un valor mayor a 0 al Smothness (0.5 esta bien pare este ejemplo), podrás ver un brillo sobre la esfera donde golpea la luz.
Alt Text

Toon Shading

El efecto toon es un efecto de luz irreal ya que modifica el sombreado del personaje para hacerlo parecer de baja calidad para entatizar el trazado de estas de una forma mas dura, esto lo ahce por medio de una textura o gradiente de varias tonalidades de gris (entre negro como minimo y blanco como maximo) a esta texura tomaremos su UV y dependiendo de del resultado de lambert, esta textura regresa su color en un punto en X especifico que regres el UV, este color pintara al sombreado, mezclandose de esta forma con nuestro modelo de luz.

ramp

Los nodos a utilizars serán Gradient (para generar nuestra textura Ramp), Remap (para encapzular la luz y la salida de esta, mas adelante explicaremos esto), Sample Gradient (Recibira un gradiente y el valor en la entrada Time para saber en su linea que valer tomará de ese gradiente como color).

Empezaremos por crear un nuevo sub graph llamado Ramp (efecto de rampa o dentado) y generaremos 2 nodos Gradient, ambos tendran 3 tonalidades de gris, con la diferencia de que uno tendra colores marcados y el otro tendré un pequeño soft en el cambio entre colores.

Alt Text
Alt Text

Lo siguiente sería usar un Sample Gradient para convertir la información del gradiente en una salida de color (un vector4 o RGBA).
Alt Text

Para enviar el valor de la textura (el tiempo en el que el gradiente tiene valor) debemos usar un remap. Remap es un nodo que recibe un valor vector1 (los vector1 son valores de una unidad, pueden ser float, half, etc) y este valor puede encapsular o filtrar lo que recibes en un mínimo y un máximo, a su vez a este valor recibido sera devuelto haciéndole una interpolación lineal el cual su resultado también se encapsula entre un valor mínimo y máximo. Lo que hace la interpolación lineal es tomar un valor (lo que estamos recibiendo) en este caso será la intensidad de la luz (NdotL) y ponerlo a razón de una recta de valores, con esto haremos el efecto de tomar los valores de la textura, llegando al mínimo valor posible dentro de nuestro gradiente (negro) o al máximo (blanco). Una textura por reglar el máximo valor posible llega a 1 y su minimo es 0 (siempre es asi, en glsl es igual), para la intensidad de la luz solo tomaremos de -1 hasta 1 como filtro ya que el objetivo es lograr un efecto mas tosco o rudo.
Alt Text

Por último nos queda asignar dos salidas al nodos maestro del subgrafo ambas serán vector4, Hard Ramp y el otro Soft Ramp, esto para tener ambos efectos y decidir en el grafo principal cuando usar uno u otro.

Alt Text

Ahora ya podemos usar nuestro Ramp en el grafo principal, para ello deberemos agregar una keyword boolean para controlar si es soft o hard ramp, ademas deberemos conectar la salidad de nuestro NdotL a la entrada de light intensity, por ultimo multiplicamos nuestro efecto con el ultimo nodo que ya trae el modelo de luz Specular.
Alt Text
Alt Text
Alt Text
Alt Text
Alt Text

Reflejo de luz cartoon

Nuestro especular devuelve la intesidad de la luz como reflejo en un área dependiendo del smoothness y el color de este, pero en las producciones un poco mas modernas utilizan reflejos con trazos para hacer parecer que el reflejo es hecho a mano con lapiz o cualquier otro efecto relacionado al dibujo.

Para ello primero crearemos una textura en photoshop o cualquier herramienta de edición de imágenes que permita el diseño de estas, esta debe guardarse dentro de la carpeta shader (o dentro de cualquier lugar dentro de assets para pdoer acceder a ella). La imagen debe ser en fondo negro y lo que será visible en color blanco para que la luz pueda mezclarse con el color de la textura. También podrías crear la textura con un nodo de ruido o voronoi, etc, hay muchas soluciones.

Nuestra textura:
Alt Text

Una vez tengamos lista la textura, crearemos un nuevo sub graph llamado Dabs, dentro de este grafo comenzaremos por incluir el nodo Sample Texture 2D y conectarle una textura (podemos crear una propiedad o asignarla directamente en su entrada Texture).
Alt Text
Alt Text

Ahora, para dar el efecto de que es un trazo, utilizaremos 2 nodos mas, Rotate y Tiling and offset. Rotate permite girar la textura en su eje para acomodarla de tal forma que la dejemos en diagonal (queremos hacer que el reflejo parezca pintadp al lapiz) y al tilling duplicaremos la cantidad de lineas y estiraremos estas misma.
Alt Text

Para utilizar nuestro nuevo nodo Dabs bastará con multiplicarlo por el Direct Specular para mezclarlo con el area del reflejo y conectar el resultado al Add con nuestro lambert.
Alt Text

Resultado:
Alt Text

En este punto podemos aprovechar para cambiar los materiales de nuestro modelo depruebas por los de toon, simplemente selecciona los materiales y agrega la textura correspondiente o genera materiales nuevos. Utilizaremos un material para el cabello, el cuerpo, la guarda del hombro, los ojos, manos. y la capa. Podemos probar moviendo un poco la luz discrecional de la escena.
Alt Text

Side light

En varias producciones que usan toon shading podemos ver un efecto en el cual dibujan la silueta del cuerpo a un lado de donde apunta la luz, con la característica de que este toma como base el punto mas alto desde donde se observa.
Alt Text

Para realizar este efecto como ya lo hemos hecho anteriormente, crea otro sub graph y nombralo Sidelight, dentro necesitaremos seguir ciertos pasos:

1)Deberemos calcular un NdotL (producto punto entre la normal y el light dir) de nuevo o podriamos hacer un sub grafo que contenga esta operación, cualquier opción es viable; también un VdotL (producto punto entre dirección de la vista del espectador y la dirección de la luz).
Alt Text

El view direction lo invertimos con negate para que nos quede en una dirección forward como lo hace el normal vector.

2)Subtrae (nodo substract) unas pocas unidades (en este caso 0.3) a tu NdotL, esto para mover un poco hacia arriba donde nace el falloff con esto pones mas alto el punto de donde hay calor (el objetivo del efecto es pintar el halo de lado a la orilla mas alta), depues el resultado multiplicalo por un valor (en nuestro caso 4) para intensificar la intensidad de la luz.
Alt Text

3) Hay que subtraer la salidad del VdotL (este será contrario a cuando este activo el NdotL) con 0.5 está bien para invertir cuando esta visible.
Alt Text

4) Por último nos queda sumar ambos efectos como uno solo, para que uno se sobre ponga al otro cuando sea visible. Ahora este valor tenemos que hacerles dos cosas, primer hay que negarlo (negate) para que el valor sea inverso al color del fresnel que utilizaremos, de eta forma la orilla inversa a donde golpea la luz y de donde ve el espectador sera borrada ya que en ves de zer de valor positivo será negativo. Lo ideal sería usar un clamp para ajustar el resultado de manera que no se extienda la intesidad de la luz, por último usaremos Step para redondear el valor de nuestro efecto en función del area de nuestro fresnel.
Alt Text

Para conectarlo bastará con sumarlo al efecto actual en el grafo principal:
Alt Text

Resultado:
Alt Text

Backlight

Backlight es el halo que se dibuja similar al sidelight pero este se genera a la orilla (similar a un rim light) cuando la luz y la vista de la cámara se cruzan de frente, haciendo el efecto de eclipse.
Alt Text

Habrá que generar un nuevo sub graph llamado Backlight, dentro de el empezaremos por crear un CdotL (produto punto entre la dirección de la camara que esta espectando y la dirección de la luz).
Alt Text

Saturamos la salida de manera que los filtremos entre el 0 y 1, recordemos trabajamos con colores asi que 0 es ausencia de color y su maximo 1 seria el blanco, hay que restar (subtract) unidades menores a 1 y mayores a cero para aumentar el falloff resultante usar este como punto de partida, en este caso un 0.6 esta bien.
Lo siguiente sería multiplicarlo por un vector 1 para controlar el tamaño del falloff (ya que hemos definido en -0.6 el punto de inicio).
Alt Text

Ahora, para terminar, nosotros lo que necesitamos es que el halo se pinte pero solo donde el color se contrario segun nuestro fresnel, la intesidad la calculamos por medio de un CdotL entonces necesitamos que este en vez de ser totalmente negro sea blanco, para ello usaremos one minus el cual nos da una unidad completa y resta la entrada lo que nos daria 1 - nuestra intensidad, lo que haria que ponga el color del lado positivo. El resultado le sumamos una cantidad positiva, en este caso 0.01 para asegurarnos de que quede un muy pequeño para asegurarnos de que el halo blanco inicio justo en la orilla cuando se cancelen los colores con el step de esta salida y el fresnel (puedes jugar con los valores del add para que veas como crece el halo blanco si lo vuelves negativo y es porque usar el step pasa de ser una linea negra invisible a blanco).
Alt Text

Para usar nuestro efecto Backlight hay que sumarlo al efecto final.
Alt Text

Resultado:
Alt Text
Alt Text

Nota:
"Notaremos que el shader tiene un aspecto como si el *falloff fuese muy duro o se estuviera comiendo el color de nuestro unlit, eso es porque directamente multiplicamos el specular que originalmente es negro cuando no hay luz sobre la textura (diffuse), para solucionar esto debemos cambiar la forma en la que mezclamos el color, primeramente moveremos el diffuse (color y textura) y lo conectaremos sumandolo al backlight y sidelight, dejando al specular para conectarse con un add directamente con nuestro Ramp (recordar, el add suma colores, multiply negro (0) con blanco (1) da negro (0), el add seria negro + blanco (0 + 1)
igual a 1 (blanco)"*.
Alt Text
Alt Text

Al final solamente une los efectos con un multiply
Alt Text

Ramp Soft
Alt Text
Ramp Hard
Alt Text

Postprocessing

Para dar el roque final utilizamos el postprocessing:
Alt Text
Alt Text

Efectos usados:
Alt Text
Alt Text

Final(se uso el modo soft ramp):
Alt Text

Proyecto en GitHub

Este es mi primer post, si notas errores o incongruencias o crees que podría estar mejor explicado por favor dímelo para mejorar el contenido n_n (@nekosamauwu)

Top comments (0)