¿Qué son los shaders?

Introducción

So, you have decided to give shaders a try. You have likely heard that they can be used to create interesting effects that run incredibly fast. You have also likely heard that they are terrifying. Both are true.

Shaders can be used to create a wide range of effects (in fact everything drawn in a modern rendering engine is done with shaders).

Writing shaders can also be very difficult for people unfamiliar with them. Godot tries to make writing shaders a little easier by exposing many useful built-in features and handling some of the lower-level initialization work for you. However, GLSL (the OpenGL Shading Language, which Godot uses) is still unintuitive and restricting, especially for users who are used to GDScript.

But what are they?

Shaders are a special kind of program that runs on Graphics Processing Units (GPUs). Most computers have some sort of GPU, either one integrated into their CPU or discrete (meaning it is a separate hardware component, for example, the typical graphics card). GPUs are especially useful for rendering because they are optimized for running thousands of instructions in parallel.

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

The light() function won't be run if the vertex_lighting render mode is enabled, or if Rendering > Quality > Shading > Force Vertex Shading is enabled in the Project Settings. (It's enabled by default on mobile platforms.)

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.

That is where shaders come in. The GPU will call the shader a bunch of times simultaneously, and then operate on different bits of data (vertices, or pixels). These bunches of data are often called wavefronts. A shader will run the same for every thread in the wavefront. For example, if a given GPU can handle 100 threads per wavefront, a wavefront will run on a 10×10 block of pixels together. It will continue to run for all pixels in that wavefront until they are complete. Accordingly, if you have one pixel slower than the rest (due to excessive branching), the entire block will be slowed down, resulting in massively slower render times.

This is different from CPU-based operations. On a CPU, if you can speed up even one pixel, the entire rendering time will decrease. On a GPU, you have to speed up the entire wavefront to speed up rendering.