HLSL_and_UE4_tutorial_featured

en Tutoriales, UE4

Introducción al uso de HLSL y UE4

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: IntroductionMicrosoft 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.

valve_pre_sobel

Imagen a color de una válvula

valve_post_sobel

Operador Sobel aplicado 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.

tangled_material

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.

material_editor_custom

Menu contextual del editor de materiales

custom_node_details

Detalles del nodo Custom

Podemos comenzar añadiendo algunos parámetros de entrada, necesitamos la imagen, las coordenadas de la textura y sus dimensiones.

custom_sobel_details

Detalles del Custom sobel

cusstom_sobel_node

Nodo Custom sobel

custom_sobel_material

Ahora necesitamos añadirle el código HLSL en el campo Code de la sección Detalles.

custom_sobel_code

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

UV_offset
Desplazamiento de adyacencia

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.

sovel_horizontal

Sobel horizontal kernel

sobel_vertical

Sobel vertical kernel

convolution_sobel
3*(+1) + 4*(+2) + 5*(+1) + 6*(0) + 7*(0) + 8*(0) + 9*(-1) + 10*(-2) + 11*(-1) = -24

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.

sobel_material_result

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.

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!

Escribe un comentario

Comentario