Introducción a la Programación GLSL

OpenGL Shading Language (GLSL) está basado en ANSI C, muchas de las características de C están presentes en GLSL con algunas diferencias que los hacen más simple, por ejemplo no contamos con punteros y se añaden nuevos tipos de datos para el manejo de vectores, matrices y texturas.

Shader en OpenGL

Un shader es un pequeño programa que se ejecuta en la GPU (Graphics Processing Unit), lo escribimos usando el lenguaje GLSL, existen otros lenguajes como: Cg (solo para procesadores gráficos NVidia) y HLSL (para DirectX).

Las versiones antiguas de OpenGL utilizan el fixed-pipeline, un conjunto de funciones fijas, solo podemos cambiar los parámetros que enviamos a dichas funciones: posición, color, etc., con OpenGL Moderno, versión 3.3 o superior “para nuestros propósitos”, tenemos la disponibilidad de programar completamente cada función usando shaders a esto se le llama programable-pipeline.

Tipos de Shader

El pipeline de OpenGL define una serie de etapas necesarias para desplegar una imagen en pantalla, algunas de ellas son programables y nos permiten manipular cierto tipo de información para lograr el resultado deseado, en cada etapa programable tendremos acceso a un conjunto de datos pertenecientes a la escena que estamos dibujando, estos datos varían dependiendo de la etapa en la que nos encontremos.

opengl-simple-pipeline
Esta es una versión simplificada del pipeline OpenGL, este se activa cuando desde la CPU usamos alguno de los comandos de dibujo como glDrawArrays(), el Vertex Shader y el Fragment Shader son programables, debemos mencionar que ha medida que ha ido evolucionando la API OpenGL se han agregado nuevas etapas programables al pipeline, sin embargo estas son opcionales, por lo que de momento solo mencionaremos el Vertex y Fragment Shader que son necesarios para trabajar con OpenGL Moderno.

Vertex Shader

vertex shaderUn modelo 3D puede estar compuesto por miles de vértices, cada vértice contiene información asociada a un punto en el espacio 3D como: posición, color, normal, textura, etc., el vertex shader será el encargado de manipular cada uno de los vértices que componen el modelo que deseamos dibujar, este se ejecutará una vez por cada vértice.

En esta etapa por lo general aplicamos transformaciones al modelo como: escalado, traslación o rotación, o podemos simplemente pasar la información tal cual a la siguiente etapa.

Las GPU modernas poseen múltiples núcleos de procesamiento, pueden ser cientos o miles de ellos dependiendo de la tarjeta gráfica, una GPU con 2,000 núcleos podría ejecutar igual número de instancias del shader en paralelo lo que permite procesar un modelo como el que vemos en la imagen, que está compuesto por más de 20,000 vértices, a una velocidad sorprendente.

Fragment Shader

fragment-shaderEs la última etapa en el pipeline OpenGL que podemos programar, el fragment shader es el encargado de procesar cada uno de los fragmentos producidos por las etapas anteriores, al igual que el vertex shader se ejecutara una vez por cada fragmento disponible, también se ejecutan varias instancias en paralelo para optimizar el rendimiento, en su modo más simple el fragment shader genera el color final de cada fragmento.

Un fragmento es una estructura de datos que contiene información como: posición, color, profundidad, etc., este será usado para determinar el color final del píxel ubicado en su correspondiente posición, pueden existir varios fragmentos en la misma posición de píxel, en este caso el color final será calculado dependiendo de la configuración de las etapas posteriores, por ejemplo, si el blending está activado el color de los fragmentos será mezclado para obtener el color final del píxel en dicha posición.

En esta etapa una tarea común es aplicar las texturas al modelo y calcular los efectos de iluminación sobre el mismo.

Compilar Shaders

Los shaders son programas que residen en la GPU y como todo programa antes utilizarse debe ser compilado, el proceso en similar a la compilación de un programa C/C++, primero compilamos el código para generar el shader object y luego usamos el enlazador para crear el ejecutable shader program.

glsl shader compiler
Estos son los pasos para crear un shader program:

Primero cargamos el código fuente del shader, puede ser de un archivo externo o una cadena de caracteres incluida dentro del código C/C++ de la aplicación, creamos el shader object usando la función glCreateShader() indicando el tipo de shader que deseamos crear (GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, etc.), la función glShaderSource() asocia el código fuente al shader object creado previamente, para terminar usaremos glCompileShader() para compilar.

El segundo paso es crea el shader program, contamos con glCreateProgram() para esta tarea, este método crea un programa vacío, y devuelve su identificador, usaremos glAttachShader() para asociar los shader object creados previamente al nuevo shader program que acabamos de crear, finalmente la función glLinkProgram() crea programa ejecutable.

Al finalizar podemos eliminar los shader object creados ya que no los necesitaremos.

GLuint compile_shaders(void)
{
    GLuint vertex_shader;
    GLuint fragment_shader;
    GLuint program;
    
    //  codigo para vertex shader
    static const GLchar * vertex_shader_source[] =
    {
        "#version 430 core                        \n"
        "                                         \n"
        "void main(void)                          \n"
        "{                                        \n"
        " gl_Position = vec4(0.0, 0.0, 0.5, 1.0); \n"
        "}                                        \n"
    };
    
    // codigo para fragment shader
    static const GLchar * fragment_shader_source[] =
    {
        "#version 430 core                  \n"
        "                                   \n"
        "out vec4 color;                    \n"
        "                                   \n"
        "void main(void)                    \n"
        "{                                  \n"
        " color = vec4(0.0, 0.8, 1.0, 1.0); \n"
        "}                                  \n"
    };
    
    // crear y compilar el vertex shader object
    vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);
    glCompileShader(vertex_shader);
    
    // crear and compilar el fragment shader object
    fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);
    glCompileShader(fragment_shader);
    
    // crear el shader program, asociar los shader objects y generar el ejecutable
    program = glCreateProgram();
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);
    glLinkProgram(program);
    
    // eliminar los shader objects
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

    return program;
}

Esta función devuelve el identificador del programa, para utilizarlo debemos invocar la función glUseProgram(program) indicando el programa que deseamos activar, usaremos glUseProgram(0) para desactivar su uso.

Con esto estamos listos para empezar a programar con shaders, en el siguiente tutorial empezaremos a conocer la sintaxis de GLSL y crearemos nuestro primer programa.

OpenGL 3.3 cuenta con tres tipos de shader: vertex, geometry y fragment shader, en la version 4.0 se introducen dos adicionales: tessellation control y tessellation evaluation shader.

Comentarios

Temas relacionados

Entradas populares de este blog

tkinter Grid

tkinter Canvas

Histogramas OpenCV Python

Python Binance API