High Level Shading Language (HLSL) es un lenguaje de programación que se puede utilizar para programar las tarjetas gráficas modernas, soporta la construcción de shaders con una sintaxis parecida a C, tipos, expresiones, declaraciones y funciones. El editor de materiales de UE4 es una buena herramienta para crear shaders pero algunas veces puede volverse un poco «lioso».
Vale la pena tener en cuenta que este no es un tutorial sobre cómo programar shaders en general, o como escribir HLSL, sino cómo conseguir que estos shaders funcionen en UE4. Para aprender cómo programar shaders o HLSL te recomiendo que eches un ojo a páginas como Neatware HLSL: Introduction o Microsoft MSDN
Empecemos con pequeño ejemplo práctico. Supongamos que queremos implementar un shader para la detección de bordes, para ello vamos a necesitar aplicar un operador Sobel a la imagen.
Básicamente este método implica utilizar operaciones matriciales con los píxeles adyacentes de cada uno de los píxeles de la imagen. Para conseguir esto en un material tendríamos que empezar obteniendo las coordenadas UV adyacentes de cada pixel y vemos que esto ya se empezaría a enredar.
Para evitar esto el editor de materiales de UE4 cuenta con un nodo especial denominado Custom. La expresión Custom nos permite escribir usando código HLSL operaciones para nuestro shader, pudiendo definir múltiples entradas y devolviendo el resultado de la operación.
Podemos comenzar añadiendo algunos parámetros de entrada, necesitamos la imagen, las coordenadas de la textura y sus dimensiones.
Ahora necesitamos añadirle el código HLSL en el campo Code de la sección Detalles.
Para aplicar el operador Sobel necesitamos:
- Calcular luminancia
- Calcular el filtro Sobel vertical
- Calcular el filtro Sobel horizontal
- Calcular el valor final utilizando los resultados vertical y horizontal
La luminancia ajusta el brillo para indicar apropiadamente lo que vemos, ya que el ojo humano no detecta el brillo de manera lineal con el color, un color con mas verde resulta mas brillante para el ojo que un color con más azul. El objetivo de la luminancia es mostrar esta diferencia.
Para calcular la luminancia necesitamos aplicar el producto escalar al color del pixel adyacente y el vector de luminancia (0.30, 0.59, 0.11, 1).
No te olvides de divider el desplazamiento de adyacencia con la dimensión con la que corresponda.
float4 luminance = float4(0.30, 0.59, 0.11, 1); // TOP ROW float s11 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, -1.0f / texH)), luminance); float s12 = dot(Texture2DSample(Tex, TexSampler, UV + float2(0, -1.0f / texH)), luminance); float s13 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, -1.0f / texH)), luminance); // MIDDLE ROW float s21 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, 0)), luminance); float s23 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, 0)), luminance); // LAST ROW float s31 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, 1.0f / texH)), luminance); float s32 = dot(Texture2DSample(Tex, TexSampler, UV + float2(0, 1.0f / texH)), luminance); float s33 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, 1.0f / texH)), luminance);
Ahora podemos aplicar la convolución matricial de ambos kernels Sobel.
Este proceso consiste en multiplicar cada pixel adyacente con el correspondiente valor del kernel y sumar todos los resultados.
float sobel_h = s11 + (2 * s12) + s13 - s31 - (2 * s32) - s33; float sobel_v = s11 + (2 * s21) + s31 - s13 - (2 * s23) - s33;
Finalmente tenemos que utilizar estos valores para determinar el color del pixel en función de un valor frontera de 0.05.
float4 luminance = float4(0.30, 0.59, 0.11, 1); // TOP ROW float s11 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, -1.0f / texH)), luminance); float s12 = dot(Texture2DSample(Tex, TexSampler, UV + float2(0, -1.0f / texH)), luminance); float s13 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, -1.0f / texH)), luminance); // MIDDLE ROW float s21 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, 0)), luminance); float s23 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, 0)), luminance); // LAST ROW float s31 = dot(Texture2DSample(Tex, TexSampler, UV + float2(-1.0f / texW, 1.0f / texH)), luminance); float s32 = dot(Texture2DSample(Tex, TexSampler, UV + float2(0, 1.0f / texH)), luminance); float s33 = dot(Texture2DSample(Tex, TexSampler, UV + float2(1.0f / texW, 1.0f / texH)), luminance); float sobel_h = s11 + (2 * s12) + s13 - s31 - (2 * s32) - s33; float sobel_v = s11 + (2 * s21) + s31 - s13 - (2 * s23) - s33; float4 result; if (((sobel_h * sobel_h) + (sobel_v * sobel_v)) > 0.05) { result = float4(0,0,0,1); } else { result = float4(1,1,1,1); } return result;
Ya podemos pegar este código en nuestro nodo Custom expression.
Utilizar el nodo custom puede resultar util para reducir el enredo de un material pero también evita el plegado de constantes pudiendo resultar en el uso de significativamente mas instrucciones que una version equivalente construida con nodos. El plegado de constantes es una optimización que UE4 emplea por debajo para reducir el número de instrucciones del shader cuando es necesario. Por ejemplo, una expresión encadenada de Time >Sin >Mul por parámetro > Add puede y será colapsada en una sola instrucción el Add final. Esto es posible porque todos las entradas de esta expresión (Time, parámetro) son constantes en toda la llamada para dibujar (draw call), no cambian por píxel. En este momento UE4 no puede colapsar nada que se encuentre dentro de un nodo Custom, lo cual puede producir shader menos eficientes que versiones equivales construidas con nodos ya existentes. Como resultado, es mejor utilizar los nodos Custom cuando te dan acceso a funcionalidades que no son posibles con nodos ya existentes.
Te puede interesar:
Ayudanos con este blog!
El último año he estado dedicando cada vez más tiempo a la creación de tutoriales, en su mayoria sobre desarrollo de videojuegos. Si crees que estos posts te han ayudado de alguna manera o incluso inspirado, por favor considera ayudarnos a mantener este blog con alguna de estas opciones. Gracias por hacerlo posible!