Advertisement
  1. Game Development
  2. Shaders

Una guía para principiantes para la codificación de sombreadores gráficos

Scroll to top
Read Time: 11 min
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders: Part 2

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.

Una escena en Minecraft, antes y después de aplicar algunos sombreadores.

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:

Su interfaz puede verse ligeramente diferente si no está conectado.

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:

ShaderToy tiene cuatro canales de entrada.

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:

Recuerde, el píxel superior izquierdo en nuestra pantalla es (0,1) mientras que el píxel superior izquierdo de la textura es (0,0), por lo que debemos voltear el eje y.

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.