¿Qué son los shaders?

Introducción

Así que has decidido probar los shaders. Seguramente habrás oído que pueden ser utilizados para crear efectos interesantes que se ejecutan increíblemente rápido. También habrás oído que son aterradores. Ambas cosas son ciertas.

Los shaders pueden utilizarse para crear una amplia gama de efectos (de hecho, todo lo que se dibuja en un moderno motor de renderizado se hace con shaders).

Escribir shaders pueden ser muy difíciles para las personas que no están familiarizadas con ellos. Godot trata de hacer que la escritura de shaders sean un poco más fáciles exponiendo muchas características útiles incorporadas y manejando algunos de los trabajos de inicialización de nivel inferior para usted. Sin embargo, el GLSL (el lenguaje de shader OpenGL, que Godot utiliza) sigue siendo poco intuitivo y restrictivo, especialmente para los usuarios que están acostumbrados a GDScript.

¿Pero qué son?

Los shaders son un tipo especial de programa que se ejecuta en unidades de procesamiento gráfico (GPU). La mayoría de los ordenadores tienen algún tipo de GPU, ya sea integrada en su CPU o discreta (lo que significa que es un componente de hardware separado, por ejemplo, la típica tarjeta gráfica). Las GPU son especialmente útiles para el renderizado porque están optimizadas para ejecutar miles de instrucciones en paralelo.

Por lo general, la salida del shader son los pixels colorados del objeto dibujado en la vista. Sin embargo, algunos shaders permiten salidas especializadas (especialmente con los APIs como Vulkan). Los shaders funcionan dentro el canal de shader. El proceso estandar es el canal de shader «vértice -> frgament». El shader vértive se usa para calcular dónde se ubica cada vértice (un punto en un modelo 3D, or la esquína de un Sprite) y el shader fragment calcula el color de los pixels individuales.

Suponemos que quisieras cambiar todos los pixels de una textura a un color determinado. En el CPU escribirías:

for x in range(width):
  for y in range(height):
    set_color(x, y, some_color)

En un shader sólo tienes acceso al interior del bucle, por lo tanto lo que escribes es así:

// function called for each pixel
void fragment() {
  COLOR = some_color;
}

No tienes control sobre como se llama esta función. Por lo tanto hay que crear el shader de manera distinta a la que creas programas en el CPU.

Una consequencia del pipeline de shaders es que se puede acceder los resultados de ejecuciones anteriores del shader; no se puede acceder otros píxeles desde el que se está dibujando; y no se puede escribir fuera de este píxel. Esto permite que el GPU ejecute al shader para distintos píxeles en paralelo; pues no se depende uno al otro. Esta falta de flexibilidad es diseñado para funcionar con el GPU y permite que los shaders sean rapidíssimos.

Lo que pueden hacer

  • colocar vértices muy rapidamente

  • calcular colores muy rapidamente

  • calcular la iluminación muy rapidamente

  • muchíssima matemática

Lo que no pueden hacer

  • dibujar fuera de una malla

  • acceder a otros píxeles desde el píxel actual (o vértices)

  • guarda iteraciones previas

  • actualizar sobre la marcha (bueno, sí que pueden pero hay que compilarlos antes)

Estructura de un shader

En Godot, los shader se componen de tres funciones: la función vertex(), la funcíon fragment() y la función light().

La función vertex() ejecuta todos los vértices en la malla y define la posición y otras variables de los vértices.

La función fragment() ejecuta por cada píxel que está cubierta por la malla. Usa los variables de la función vertex() para ejecutar. Los variables de la función vertex() se interpolan entre los vértices para aportar los valores para la función fragment().

La función light() ejecuta por cada píxel y cada luz. Toma variables de la función fragement() y de ejecuciones anteriores de ella misma.

Para más información sobre cómo funcionan los shaders específicamente en Godot, ver Shaders doc.

Advertencia

La función light() no se ejecutará si el modo de renderización vertex_lighting está habilitado, o si Rendering > Quality > Shading > Force Vertex Shading está habilitado en la configuración del proyecto. (Está habilitada por defecto en las plataformas móviles).

Reseña técnica

La GPU es capáz de generar gráficos mucho más rapido que la CPU por varios razones, la más notable es que pueden ejecutar cálculos masivamente en paralelo. Por lo general, una CPU tiene 4 o 8 núcleos pero un GPU tiene miles. Eso significa que la GPU puede hacer cientos de tareas a la vez. Los diseñedores han aprovechado de esto de manera que permite hacer muchos cálculos con rapidez, pero solo cuando la mayoría o todos de los núcleos están haciendo el mismo cálculo, pero con datos distintos.

Ahí es donde entran los shaders. La GPU llamará al shader un montón de veces simultáneamente, y luego operará con diferentes bits de datos (vértices o píxeles). Estos racimos de datos a menudo se llaman wavefronts. Un shader funcionará igual para cada hilo del wavefront. Por ejemplo, si una GPU dada puede manejar 100 hilos por frente de onda, un frente de onda se ejecutará en un bloque de 10×10 píxeles juntos. Continuará funcionando para todos los píxeles de ese frente de onda hasta que se completen. Por consiguiente, si un píxel es más lento que el resto (debido a una ramificación excesiva), todo el bloque se ralentizará, lo que dará lugar a tiempos de renderización mucho más lentos.

Esto es diferente de las operaciones basadas en la CPU. En una CPU, si puedes acelerar aunque sea un píxel, todo el tiempo de renderizado disminuirá. En una GPU, tienes que acelerar todo el frente de onda para acelerar el renderizado.