Una guía para principiantes para la codificación de sombreadores gráficos
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Aprender a escribir sombreadores gráficos es aprender a aprovechar el poder de la GPU, con sus miles de núcleos funcionando todos en paralelo. Es un tipo de programación que requiere una mentalidad diferente, pero desbloquear su potencial vale la pena el problema inicial.
Prácticamente todas las simulaciones gráficas modernas que se ven funcionan de algún modo con código escrito para la GPU, desde los efectos de iluminación realistas en los innovadores juegos AAA hasta los efectos 2D de posprocesamiento y las simulaciones de fluidos.



El objetivo de esta guía
La programación de Shader a veces aparece como una magia negra enigmática y, a menudo, es mal interpretada. Hay muchas muestras de códigos que le muestran cómo crear efectos increíbles, pero ofrecen poca o ninguna explicación. Esta guía tiene como objetivo cerrar esa brecha. Me centraré más en los aspectos básicos de la escritura y la comprensión del código de sombreado, para que pueda modificar, combinar o escribir uno propio desde cero.
Esta es una guía general, por lo que lo que aprenda aquí se aplicará a todo lo que pueda ejecutar sombreadores.
Entonces, ¿qué es un sombreador?
Un sombreador es simplemente un programa que se ejecuta en la tubería de gráficos y le dice a la computadora cómo representar cada píxel. Estos programas se llaman sombreadores porque a menudo se usan para controlar la iluminación y los efectos de sombreado, pero no hay ninguna razón por la que no puedan manejar otros efectos especiales.
Los sombreadores están escritos en un lenguaje de sombreado especial. No se preocupe, no tiene que salir y aprender un idioma completamente nuevo; usaremos GLSL (OpenGL Shading Language) que es un lenguaje en forma de C. (Hay muchos lenguajes de sombreado para diferentes plataformas, pero como todos están adaptados para ejecutarse en la GPU, todos son muy similares)
¡Vamos a avanzar!
Usaremos ShaderToy para este tutorial. Esto le permite comenzar a programar sombreadores directamente en su navegador, ¡sin la molestia de configurar nada! (Utiliza WebGL para renderizar, por lo que necesitará un navegador que pueda admitir eso.) Crear una cuenta es opcional, pero útil para guardar su código.
Nota: ShaderToy está en beta en el momento de escribir
este artículo. Algunos detalles pequeños de UI / sintaxis pueden ser
ligeramente diferentes.
Al hacer clic en New Shader, debería ver algo como esto:



La pequeña flecha negra en la parte inferior es lo que haces clic para compilar tu código.
¿Qué esta pasando?
Estoy a punto de explicar cómo funcionan los sombreadores en una oración. ¿Estás listo? ¡Aquí va!
El
único propósito de un sombreador es devolver cuatro números: r
, g
, b
y
a
.
Eso es todo lo que hace o puede hacer. La función que ve delante de usted se ejecuta para cada píxel en pantalla. Devuelve esos cuatro valores de color, y ese se convierte en el color de ese píxel. Esto es lo que se llama Pixel Shader (a veces denominado Shader Fragment).
Con
eso en mente, intentemos convertir nuestra pantalla en un rojo sólido. Los
valores rgba (rojo, verde, azul y "alfa", que define la transparencia)
van de 0
a 1
, por lo que todo lo que tenemos que hacer es devolver r,g,b,a = 1,0,0,1
. ShaderToy espera que el color de píxel final se
almacene en fragColor
.
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
fragColor = vec4(1.0,0.0,0.0,1.0); |
4 |
}
|
¡Felicitaciones! ¡Este es tu primer sombreador funcionando!
Desafío: ¿Puedes cambiarlo a un color gris sólido?
vec4
es solo un tipo de datos, por lo que podríamos
haber declarado nuestro color como una variable, así:
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec4 solidRed = vec4(1.0,0.0,0.0,1.0); |
4 |
fragColor = solidRed; |
5 |
}
|
Aunque esto no es muy emocionante. Tenemos el poder de ejecutar código en cientos de miles de píxeles en paralelo y los estamos configurando todos del mismo color.
Tratemos de renderizar un degradado en la pantalla. Bueno, no podemos hacer mucho sin conocer algunas cosas sobre el píxel que estamos afectando, como su ubicación en la pantalla ...
Shader Inputs
El
sombreador de píxeles pasa algunas variables para que pueda usar. El
más útil para nosotros es fragCoord
, que contiene las coordenadas x e y
(y z, si estás trabajando en 3D) del píxel. Intentemos
convertir todos los píxeles en la mitad izquierda de la pantalla en
negro, y todos los que están en la mitad derecha en rojo:
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel |
4 |
vec4 solidRed = vec4(0,0.0,0.0,1.0);//This is actually black right now |
5 |
if(xy.x > 300.0){//Arbitrary number, we don't know how big our screen is! |
6 |
solidRed.r = 1.0;//Set its red component to 1.0 |
7 |
}
|
8 |
fragColor = solidRed; |
9 |
}
|
Nota:
para cualquier vec4
, puede acceder a sus componentes a través de obj.x
, obj.y
, obj.z
y obj.w
o vía obj.r
, obj.g
, obj.b
, obj.a
. Son equivalentes; es solo una forma conveniente de nombrarlos para hacer
que su código
sea más legible, de modo que cuando otros vean obj.r
, entiendan que obj
representa un color.
¿Ves un problema con el código de arriba? Intente hacer clic en el botón ir a pantalla completa en la parte inferior derecha de la ventana de vista previa.
La proporción de la pantalla que es roja variará según el tamaño de la pantalla. Para garantizar que exactamente la mitad de la pantalla esté en rojo, necesitamos saber qué tan grande es nuestra pantalla. El tamaño de la pantalla no está integrado en una variable como la ubicación de píxeles, porque generalmente depende de usted, el programador que creó la aplicación, configurar eso. En este caso, son los desarrolladores de ShaderToy quienes configuran el tamaño de la pantalla.
Si algo no está integrado en una variable, puede enviar esa información desde la CPU (su programa principal) a la GPU (su sombreador). ShaderToy lo maneja por nosotros. Puede ver todas las variables que se pasan al sombreador en la pestaña Shader Inputs. Las variables pasadas de CPU a GPU se llaman uniformes en GLSL.



Modifiquemos nuestro código anterior para obtener correctamente el centro de la pantalla. Tendremos que usar la entrada de shader iResolution
:
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel |
4 |
xy.x = xy.x / iResolution.x; //We divide the coordinates by the screen size |
5 |
xy.y = xy.y / iResolution.y; |
6 |
// Now x is 0 for the leftmost pixel, and 1 for the rightmost pixel
|
7 |
vec4 solidRed = vec4(0,0.0,0.0,1.0); //This is actually black right now |
8 |
if(xy.x > 0.5){ |
9 |
solidRed.r = 1.0; //Set its red component to 1.0 |
10 |
}
|
11 |
fragColor = solidRed; |
12 |
}
|
Si intenta ampliar la ventana de vista previa esta vez, los colores deben dividir perfectamente la pantalla a la mitad.
De una división a un gradiente
Convertir esto en un gradiente debería ser
bastante fácil. Nuestros valores de color van de 0
a 1
, y nuestras
coordenadas ahora también van de 0
a 1
.
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec2 xy = fragCoord.xy; //We obtain our coordinates for the current pixel |
4 |
xy.x = xy.x / iResolution.x; //We divide the coordinates by the screen size |
5 |
xy.y = xy.y / iResolution.y; |
6 |
// Now x is 0 for the leftmost pixel, and 1 for the rightmost pixel
|
7 |
vec4 solidRed = vec4(0,0.0,0.0,1.0); //This is actually black right now |
8 |
solidRed.r = xy.x; //Set its red component to the normalized x value |
9 |
fragColor = solidRed; |
10 |
}
|
¡Y voilá!
Desafío: ¿Puedes convertir esto en un gradiente vertical? ¿Qué hay de la diagonal? ¿Qué tal un degradado con más de un color?
Si juegas con esto
lo suficiente, puedes decir que la esquina superior izquierda tiene
coordenadas (0,1)
, no (0,0)
. Esto es importante tenerlo en
cuenta.
Dibujando Imágenes
Jugar con los colores es divertido, pero si queremos hacer algo impresionante, nuestro sombreador tiene que ser capaz de tomar la información de una imagen y modificarla. De esta forma podemos crear un sombreador que afecte a toda la pantalla del juego (como un efecto de fluido subacuático o corrección de color) o que solo afecte ciertos objetos de determinadas formas según las entradas (como un sistema de iluminación realista).
Si estuviéramos programando en una plataforma normal, tendríamos que enviar nuestra imagen (o textura) a la GPU como un uniforme, de la misma manera que habría enviado la resolución de la pantalla. ShaderToy se ocupa de eso por nosotros. Hay cuatro canales de entrada en la parte inferior:



Haga clic en iChannel0 y seleccione cualquier textura (imagen) que desee.
Una vez hecho esto, ahora tienes una imagen que se está pasando a
tu sombreador. Sin embargo, hay un problema: no hay función DrawImage()
. Recuerde, lo único que el sombreador de píxeles puede hacer es
cambiar el color de cada píxel.
Entonces, si solo podemos devolver un color, ¿cómo dibujamos nuestra textura en la pantalla? Necesitamos de alguna manera mapear el píxel actual en el que está nuestro sombreador, al píxel correspondiente en la textura:



Podemos hacer esto usando la
función texture2D(texture,coordinates)
, que toma una textura y un par
de coordenadas (x, y)
como entradas, y devuelve el color de la textura
en esas coordenadas como vec4
.
Puede unir las coordenadas a la pantalla de la manera que desee. Podrías dibujar toda la textura en un cuarto de la pantalla (omitiendo píxeles, escalando de forma efectiva) o simplemente dibujar una parte de la textura.
Para nuestros propósitos, solo queremos ver la imagen, por lo que igualaremos los píxeles 1: 1:
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec2 xy = fragCoord.xy / iResolution.xy;//Condensing this into one line |
4 |
xy.y = 1.0 - xy.y; |
5 |
vec4 texColor = texture2D(iChannel0,xy);//Get the pixel at xy from iChannel0 |
6 |
fragColor = texColor;//Set the screen pixel to that color |
7 |
}
|
¡Con eso, tenemos nuestra primera imagen!



¡Ahora que está sacando datos de una textura correctamente, puede manipularlos como quiera! Puedes estirarlo y escalarlo, o jugar con sus colores.
Probemos modificando esto con un degradado, similar a lo que hicimos arriba:
1 |
texColor.b = xy.x; |



¡Felicidades, acabas de crear tu primer efecto de posprocesamiento!
Desafío: ¿Puedes escribir un sombreador que convierta una imagen en blanco y negro?
Tenga en cuenta que, aunque es una imagen estática, lo que está viendo delante de usted está sucediendo en tiempo real. Puede ver esto usted mismo al reemplazar la imagen estática con un video: haga clic nuevamente en la entrada iChannel0 y seleccione uno de los videos.
Agregar algún movimiento
Hasta ahora, todos nuestros efectos han
sido estáticos. Podemos hacer cosas mucho más interesantes haciendo uso
de las entradas que nos da ShaderToy. iGlobalTime
es una variable en
constante aumento; podemos usarlo como semilla para hacer efectos
periódicos. Tratemos de jugar un poco con los colores:
1 |
void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
2 |
{
|
3 |
vec2 xy = fragCoord.xy / iResolution.xy; // Condensing this into one line |
4 |
xy.y = 1.0-xy.y; // Flipping the y |
5 |
vec4 texColor = texture2D(iChannel0,xy); // Get the pixel at xy from iChannel0 |
6 |
texColor.r *= abs(sin(iGlobalTime)); |
7 |
texColor.g *= abs(cos(iGlobalTime)); |
8 |
texColor.b *= abs(sin(iGlobalTime) * cos(iGlobalTime)); |
9 |
fragColor = texColor; // Set the screen pixel to that color |
10 |
}
|
Hay
funciones seno y coseno integradas en GLSL, así como muchas otras
funciones útiles, como obtener la longitud de un vector o la distancia
entre dos vectores. Se supone que los colores no son negativos, por lo que nos aseguramos
de obtener el valor absoluto mediante el uso de la función abs
.
Desafío: ¿Puedes hacer un sombreado que cambie una imagen de blanco y negro a color?
Una nota sobre la depuración de sombreadores
Si bien es posible que esté acostumbrado a recorrer su código e imprimir los valores de todo para ver qué está sucediendo, eso no es posible cuando se escriben sombreadores. Es posible que encuentre algunas herramientas de depuración específicas para su plataforma, pero en general su mejor opción es establecer el valor que está probando en algo gráfico que pueda ver en su lugar.
Conclusión
Estos son solo los conceptos básicos para trabajar con shaders, pero sentirte cómodo con estos fundamentos te permitirá hacer mucho más. Explore los efectos en ShaderToy y vea si puede entender o replicar algunos de ellos.
Una cosa que no mencioné en este tutorial es Vertex Shaders. Todavía están escritos en el mismo idioma, excepto que se ejecutan en cada vértice en lugar de cada píxel, y devuelven una posición y un color. Vertex Shaders suele ser responsable de proyectar una escena en 3D en la pantalla (algo que está integrado en la mayoría de las tuberías de gráficos). Los sombreadores de píxeles son responsables de muchos de los efectos avanzados que vemos, por eso son nuestro foco.
Desafío final: ¿Puedes escribir un sombreador que elimine la pantalla verde en los videos en ShaderToy y agrega otro video como fondo al primero?
¡Eso es todo por esta guía! Agradecería mucho sus comentarios y preguntas. Si hay algo específico sobre lo que desea obtener más información, por favor deje un comentario. Las guías futuras podrían incluir temas como los conceptos básicos de los sistemas de iluminación, o cómo hacer una simulación fluida o configurar shaders para una plataforma específica.