Shaders: El freestyle de los programadores gráficos

Programando un motor 3D desde cero - Brakeza3D

Shaders: El freestyle de los programadores gráficos

Buenas a tod@s! Han pasado muchos meses desde la última vez que pude ponerme tranquilamente a escribir algunos de los avances que he ido incorporando a lo largo de este año. Ha sido un año dificil con un cambio de trabajo por en medio que me obligó a retrasar mis projectos personales…pero aquí estamos, nunca he dejado de aportar fixes y mejoras, asi que a llorar a la llorería y empezamos!

Hoy quiero hablar de los shaders, qué son, de cómo surge la necesidad y de cómo se afrontó el reto de implementar dicha funcionalidad en Brakeza3D.

En ocasiones podrás verte ante la necesidad o el deseo de añadir algún efecto visual disruptivo, o simplemente algún cálculo que por su naturaleza es costoso en términos de computación. Los efectos visuales son en general costosos de procesar, pensad que cada pixel en pantalla requerirá de uno o varios cálculos para obtener el resultado deseado. Es un reto simplemente INASUMIBLE para una CPU y menos en tiempo real, que sería el escenario de un videojuego.

Necesitamos por tanto un mecanismo que nos permita ejecutar en nuestro hardware acelerador, ese tipo de tareas.

Qué es un Shader

Partamos de la definición actual que todos podemos encontrar por Internet: Un shader es un programa informático que se ejecuta en un procesador gráfico de una computadora. 

Un shader no es más que un programa que se ejecutará en la GPU  que nos permite realizar operaciones sobre la información que se le envía. Por ejemplo con OpenGL, se implementan en un lenguaje propio llamado GLSL, el cual es muy similar a C, en DirectX, existe HLSL, muy similar.

Según la naturaleza del shaders, los habrá que efectuen cálculos sobre geometría, otros actuarán sobre un buffer de imagen, otros directamente sobre la salida de video (otro buffer al fin y al cabo), etc…

En general son pequeños fragmentos de programa, que además, por su naturaleza, desearemos que se ejecuten a TODA velocidad, por eso se diseñan específicamente para GPU, con un enfoque y estructura particulares.

OpenCL y sus kernels

En Brakeza3D, se ha optado por utilizar OpenCL para el acceso a la GPU. 

Mediante OpenCL podremos crear nuestros programas, llamados kernels, que alojaremos en la GPU y ejecutaremos a nuestro antojo en nuestro motor.

Obviamente profundizar en todo el espectro de posibilidades de OpenCL no es el propósito de este tutorial. Simplemente cabe destacar que es una interface de acceso a hardware acelerador, la cual permite crear programas “en ficheros separados”, que se cargarán en nuestra aplicación, pudiendo parametrizar en tiempo de ejecución los argumentos que recibe dicho kernel. El líneas generales cada kernel, se ejecutará en lo que se denominan “unidades de trabajo”, las cuales comparten una memoria en común.

¿De qué estamos hablando?

En la siguiente imagen, se observa un par de rayos laser y aunque no se aprecie en movimiento, dicho rayo laser incorpora un fundido con un ‘ruido’ que se mueve en una dirección para simular movimiento.

Es un ejemplo básico de un shader, que genera, pues eso, un efecto rayo laser. El rayo laser, no forma parte del render 3D, es algo que se añade a posterior. (de ahí lo de post-procesado)

Un shader de procesamiento de imagen, por lo general, trabajará con cada pixel como si de una unidad de trabajo independiente se tratase, ejecutando todos los pixels simultáneamente.

Trabajar con shaders de procesamiento de imágenes no es trivial, es conveniente familiarizarse con algunas de las funciones más habituales y saber conjugarlas a tu favor para conseguir los efectos deseados. Además sería interesante ser una persona creativa.

Shaders y más shaders

Probablemente alguno ya lo haya pensado, pero si, los shaders se pueden ejecutar en cascada, es decir, proporcionando a la entrada de uno la salida del anterior, consiguiendo asi apilar efectos, que de otra forma sería imposible. En cierta medida, se pueden visualizar los shaders de post-procesamiento, como capas de photoshop que añadimos a una imagen para ir modificando su aspecto final.

Por supuesto los motores 3D modernos, hacen un uso harto-intensivo de los shaders.

Aspecto de un kernel OpenCL

Vamos con un ejemplo sencillo:

__kernel void onUpdate(
   int screenWidth,
   int screenHeight,
   __global unsigned int *video,
   __global bool *stencil,
   float r,
   float g,
   float b
){
    int i = get_global_id(0);

    if (stencil[i]) {
        video[i] = createRGB(r, g, b);
    }
}


Es un simple kernel que simula el efecto “parpadeo” de un color la figura de un objeto renderizado en pantalla.

Para conseguir este efecto generalmente se hace uso de un buffer que almacena la máscara que representa los pixeles que forman el objeto renderizado (llamaremos a esto stencil buffer), un simple array booleano, cuyos valores a true, representan los pixels donde se ha renderizado ese objeto.

Además, el shader recibe ancho y alto y RGB del efecto en pantalla pero sin duda el argumento más destacado aquí es el buffer de vídeo, que representa el buffer sobre el que haremos cambios para reflejarlos en pantalla en un futuro.

Por lo demás la lógica del shader es nula en este caso, si el buffer de stencil indica q ese pixel está pintado, pues coloreamos. Si hacemos esto con todos los pixeles, habremos pintado en el buffer de video y del color RGB aportado al kernel, la máscara que forma el stencil buffer. 

Por si alguien tiene realmente dudas en este punto: get_global_id es una función de OpenCL, que nos proporciona el contexto sobre la unidad de trabajo que se está procesando, no vamos a profundizar ahora en este punto.

OpenCL ofrece mecanismos para ejecutar este kernel y parametrizar su función onUpdate, si estáis realmente interesados en mi repositorio de GitHub encontraréis varios shaders implementados. En líneas generales el proceso siempre consiste en:

  • clEnqueueWriteBuffer: Escribir en un buffer nativo de OpenCL (tantos como necesitemos)

  • clSetKernelArg: Setear los argumentos del kernel (tantos argumentos como el kernel requiera)

  • clEnqueueNDRangeKernel: Ejecución del kernel y configuración de unidades de trabajo.

  • clEnqueueReadBuffer: Lectura del buffer OpenCL hacia el host para reutilizar a nuestro antojo.

Siguiendo el ejemplo de arriba, asi podría quedar la parametrización de argumentos de este kernel:

clSetKernelArg(kernel, 0, sizeof(int), &screenWidth);
clSetKernelArg(kernel, 1, sizeof(int), &screenHeight);
clSetKernelArg(kernel, 2, sizeof(cl_mem), (void *)&openClBufferMappedWithVideoInput);
clSetKernelArg(kernel, 3, sizeof(cl_mem), (void *)&opencl_buffer_stencil);
clSetKernelArg(kernel, 4, sizeof(float), &this->color.r);
clSetKernelArg(kernel, 5, sizeof(float), &this->color.g);
clSetKernelArg(kernel, 6, sizeof(float), &this->color.b);

Puedes encontrar el código completo en: 

https://github.com/rzeronte/brakeza3d/tree/master/darkheaz/src/shaders

Cómo continuar sólo?

Si quieres profundizar en nuestra particular implementación de Shaders con OpenCL, en nuestro repositorio de GitHub podrás encontrar ejemplos, si por el contrario deseas formarte partiendo de un entorno refutado, GLSL sería mi recomendación.

Existen además herramientas online que te facilitarán el ponerte manos a la obra, especial atención a shadertoy.com donde podrás encontrar decenas de shaders implementados por otras personas y que podrán ayudarte a encontrar el efecto que estás intentando programar por ti mismo.

Necesitarás practicar creando líneas, curvas, degradados, patrones y figuras sencillas antes de poder conjuntarlo todo para dar el siguiente paso y obtener efectos más elaborados. En este punto mi recomendación es https://thebookofshaders.com/ un libro online que cumple más que de sobra la función de introducción al mundillo

Resumen

Hemos presentado de puntillas el concepto de Shader y mencionado como Brakeza3D los implementa mediante el uso de OpenCL como interface de acceso a la GPU. 

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *