Introduccion
Renderizar gráficos en 3D no es una tarea fácil, pero es extremadamente interesante y emocionante. Este artículo es para aquellos que recién están comenzando a familiarizarse con OpenGL o para aquellos que están interesados en cómo funcionan las tuberías gráficas y qué son. Este artículo no proporcionará instrucciones exactas sobre cómo crear un contexto y una ventana de OpenGL, o cómo escribir su primera aplicación de ventana de OpenGL. Esto se debe a las características de cada lenguaje de programación y a la elección de una biblioteca o marco para trabajar con OpenGL (
usaré C ++ y
GLFW ), especialmente porque es fácil encontrar un tutorial en la red para el idioma que le interesa. Todos los ejemplos dados en el artículo funcionarán en otros idiomas con una semántica de comandos ligeramente cambiada, por qué esto es así, lo contaré un poco más adelante.
¿Qué es OpenGL?
OpenGL es una especificación que define una interfaz de software independiente de la plataforma para escribir aplicaciones utilizando gráficos por computadora bidimensionales y tridimensionales. OpenGL no es una implementación, sino que solo describe los conjuntos de instrucciones que deben implementarse, es decir, es una API
Cada versión de OpenGL tiene su propia especificación, trabajaremos desde la versión 3.3 a la versión 4.6, porque Todas las innovaciones de la versión 3.3 afectan aspectos que son de poca importancia para nosotros. Antes de comenzar a escribir su primera aplicación OpenGL, le recomiendo que averigüe qué versiones admite su controlador (puede hacerlo en el sitio del proveedor de su tarjeta de video) y actualice el controlador a la última versión.
Dispositivo OpenGL
OpenGL se puede comparar con una máquina de estado grande, que tiene muchos estados y funciones para cambiarlos. El estado de OpenGL básicamente se refiere al contexto de OpenGL. Mientras trabajamos con OpenGL, veremos varias funciones de cambio de estado que cambiarán el contexto y realizaremos acciones dependiendo del estado actual de OpenGL.
Por ejemplo, si le damos a OpenGL el comando de usar líneas en lugar de triángulos antes de renderizar, OpenGL usará las líneas para todas las representaciones posteriores hasta que cambiemos esta opción o cambiemos el contexto.
Objetos en OpenGL
Las bibliotecas OpenGL están escritas en C y tienen numerosas API para diferentes lenguajes, pero sin embargo son bibliotecas C. Muchas construcciones de C no se traducen a lenguajes de alto nivel, por lo que OpenGL se desarrolló utilizando una gran cantidad de abstracciones, una de estas abstracciones son los objetos.
Un objeto en OpenGL es un conjunto de opciones que determina su estado. Cualquier objeto en OpenGL puede describirse por su id y el conjunto de opciones de las que es responsable. Por supuesto, cada tipo de objeto tiene sus propias opciones y un intento de configurar opciones inexistentes para el objeto dará lugar a un error. Ahí reside el inconveniente de usar OpenGL: se describe un conjunto de opciones con una estructura similar en C, cuyo identificador suele ser un número, lo que no permite que el programador encuentre un error en la etapa de compilación, porque el código erróneo y correcto es semánticamente indistinguible.
glGenObject(&objectId); glBindObject(GL_TAGRGET, objectId); glSetObjectOption(GL_TARGET, GL_CORRECT_OPTION, correct_option);
Encontrará ese código muy a menudo, por lo que cuando se acostumbre a cómo es configurar una máquina de estado, será mucho más fácil para usted. Este código solo muestra un ejemplo de cómo funciona OpenGL. Posteriormente, se presentarán ejemplos reales.
Pero hay ventajas. La característica principal de estos objetos es que podemos declarar muchos objetos en nuestra aplicación, establecer sus opciones, y cada vez que comenzamos las operaciones usando el estado OpenGL, simplemente podemos vincular el objeto con nuestra configuración preferida. Por ejemplo, esto puede ser objetos con datos del modelo 3D o algo que queremos dibujar en este modelo. Ser propietario de varios objetos facilita el cambio entre ellos durante el proceso de renderizado. Con este enfoque, podemos configurar muchos objetos necesarios para renderizar y usar sus estados sin perder tiempo valioso entre cuadros.
Para comenzar a trabajar con OpenGL necesita familiarizarse con varios objetos básicos sin los cuales no podemos mostrar nada. Usando estos objetos como ejemplo, entenderemos cómo vincular datos e instrucciones ejecutables en OpenGL.
Objetos base: sombreadores y programas de sombreadores. =
Shader es un pequeño programa que se ejecuta en un acelerador de gráficos (GPU) en un punto determinado de la tubería de gráficos. Si consideramos los sombreadores de manera abstracta, podemos decir que estas son las etapas de la tubería de gráficos, que:
- Sepa dónde obtener datos para el procesamiento.
- Sepa cómo procesar datos de entrada.
- Saben dónde escribir datos para su posterior procesamiento.
Pero, ¿cómo se ve la tubería gráfica? Muy simple, así:

Hasta ahora, en este esquema, solo estamos interesados en la vertical principal, que comienza con la Especificación de Vértice y termina con Frame Buffer. Como se mencionó anteriormente, cada sombreador tiene sus propios parámetros de entrada y salida, que difieren en el tipo y número de parámetros.
Describimos brevemente cada etapa de la tubería para comprender lo que hace:
- Sombreador de vértices: necesario para procesar datos de coordenadas 3D y todos los demás parámetros de entrada. Muy a menudo, el sombreador de vértices calcula la posición del vértice en relación con la pantalla, calcula las normales (si es necesario) y genera datos de entrada a otros sombreadores.
- Sombreador de teselación y sombreador de control de teselación: estos dos sombreadores son responsables de detallar las primitivas que provienen del sombreador de vértices y preparar los datos para el procesamiento en el sombreador geométrico. Es difícil describir de lo que son capaces estos dos sombreadores en dos oraciones, pero para que los lectores tengan una pequeña idea, les daré un par de imágenes con niveles de superposición bajos y altos:
Te aconsejo que leas este artículo si quieres saber más sobre la teselación. En esta serie de artículos cubriremos la teselación, pero no será pronto. - Sombreador geométrico: es responsable de la formación de primitivas geométricas a partir de la salida del sombreador de teselación. Usando el sombreador geométrico, puede crear nuevas primitivas a partir de las primitivas básicas de OpenGL (GL_LINES, GL_POINT, GL_TRIANGLES, etc.), por ejemplo, usando el sombreador geométrico, puede crear un efecto de partículas describiendo la partícula solo por color, centro del racimo, radio y densidad.
- El sombreador de rasterización es uno de los sombreadores no programables. Hablando en un lenguaje comprensible, traduce todas las primitivas gráficas de salida en fragmentos (píxeles), es decir determina su posición en la pantalla.
- El sombreador de fragmentos es la última etapa de la tubería de gráficos. El sombreador de fragmentos calcula el color del fragmento (píxel) que se establecerá en el búfer de fotogramas actual. Muy a menudo, en el sombreador de fragmentos, se calculan el sombreado y la iluminación del fragmento, el mapeo de texturas y los mapas normales: todas estas técnicas le permiten lograr resultados increíblemente hermosos.
Los sombreadores OpenGL están escritos en un lenguaje GLSL especial tipo C desde el cual se compilan y vinculan a un programa de sombreadores. Ya en esta etapa, parece que escribir un programa de sombreado es una tarea extremadamente lenta, porque debe determinar los 5 pasos de la tubería de gráficos y vincularlos. Afortunadamente, esto no es así: los sombreadores de teselación y geometría se definen en la tubería de gráficos de forma predeterminada, lo que nos permite definir solo dos sombreadores: el vértice y los fragmentos (a veces llamado sombreador de píxeles). Es mejor considerar estos dos sombreadores con un ejemplo clásico:
Sombreador de vértices #version 450 layout (location = 0) in vec3 vertexCords; layout (location = 1) in vec3 color; out vec3 Color; void main(){ gl_Position = vec4(vertexCords,1.0f) ; Color = color; }
Sombreador de fragmentos #version 450 in vec3 Color; out vec4 out_fragColor; void main(){ out_fragColor = Color; }
Ejemplo de ensamblaje del sombreador unsigned int vShader = glCreateShader(GL_SHADER_VERTEX);
Estos dos sombreadores simples no calculan nada, simplemente pasan los datos por la tubería. Prestemos atención a cómo están conectados los sombreadores de vértices y fragmentos: en el sombreador de vértices, se declara la variable Color en la que se escribirá el color después de ejecutar la función principal, mientras que en el sombreador de fragmentos se declara exactamente la misma variable con el calificador in, es decir. como se describió anteriormente, el sombreador de fragmentos recibe datos del vértice mediante un simple empuje de los datos a través de la tubería (pero en realidad no es tan simple).
Nota: Si no declara e inicializa una variable de tipo vec4 en el sombreador de fragmentos, no se mostrará nada en la pantalla.
Los lectores atentos ya han notado la declaración de variables de entrada del tipo vec3 con calificadores de diseño extraños al comienzo del sombreador de vértices, es lógico suponer que esto es entrada, pero ¿de dónde lo obtenemos?
Objetos base: búferes y matrices de vértices
Creo que no vale la pena explicar qué son los objetos de búfer, mejor consideraremos cómo crear y llenar un búfer en OpenGL.
float vertices[] = {
No hay nada difícil en esto, adjuntamos el búfer generado al objetivo deseado (más adelante descubriremos cuál) y cargamos los datos que indican su tamaño y tipo de uso.
GL_STATIC_DRAW: los datos en el búfer no se cambiarán.
GL_DYNAMIC_DRAW: los datos en el búfer cambiarán, pero no con frecuencia.
GL_STREAM_DRAW: los datos en el búfer cambiarán con cada llamada de sorteo.
Es genial, ahora nuestros datos se encuentran en la memoria de la GPU, el programa de sombreado está compilado y vinculado, pero hay una advertencia: ¿cómo sabe el programa dónde obtener los datos de entrada para el sombreador de vértices? Descargamos los datos, pero no indicamos de dónde los obtendría el programa de sombreado. Este problema se resuelve con un tipo separado de objetos OpenGL: matrices de vértices.

La imagen está tomada de este tutorial .
Al igual que con los buffers, las matrices de vértices se ven mejor utilizando su ejemplo de configuración.
unsigned int VBO, VAO; glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glGenVertexArrays(1, &VAO); glBindVertexArray(VAO);
Crear matrices de vértices no es diferente de crear otros objetos OpenGL, el más interesante comienza después de la línea:
glBindVertexArray(VAO);
Una matriz de vértices (VAO) recuerda todos los enlaces y configuraciones que se realizan con él, incluida la unión de objetos de búfer para la descarga de datos. En este ejemplo, solo hay un objeto de este tipo, pero en la práctica puede haber varios. Después de eso, se configura el atributo de vértice con un número específico:
glBindBuffer(GL_ARRAY_BUFFER, VBO); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
¿Dónde conseguimos este número? ¿Recuerda los calificadores de diseño para las variables de entrada del sombreador de vértices? Son ellos quienes determinan a qué atributo de vértice se vinculará la variable de entrada. Ahora repase brevemente los argumentos de la función para que no haya preguntas innecesarias:
- El número de atributo que queremos configurar.
- La cantidad de artículos que queremos llevar. (Dado que la variable de entrada del sombreador de vértices con diseño = 0 es de tipo vec3, entonces tomamos 3 elementos de tipo flotante)
- Tipo de artículos.
- ¿Es necesario normalizar los elementos, si es un vector?
- El desplazamiento para el siguiente vértice (dado que tenemos las coordenadas y los colores ubicados secuencialmente y cada uno tiene el tipo vec3, entonces cambiamos por 6 * sizeof (float) = 24 bytes).
- El último argumento muestra qué desplazamiento tomar para el primer vértice. (para coordenadas, este argumento es de 0 bytes, para colores de 12 bytes)
Todo ahora estamos listos para renderizar nuestra primera imagen.
Recuerde vincular el VAO y el programa de sombreado antes de invocar el render.
{
Si hiciste todo bien, entonces deberías obtener este resultado:

El resultado es impresionante, pero ¿de dónde vino el relleno de degradado en el triángulo, porque indicamos solo 3 colores: rojo, azul y verde para cada vértice individual? Esta es la magia del sombreador de rasterización: el hecho es que el valor de Color que establecemos en el vértice no está entrando en el sombreador de fragmentos. Transmitimos solo 3 vértices, pero se generan muchos más fragmentos (hay exactamente tantos fragmentos como píxeles rellenos). Por lo tanto, para cada fragmento, se toma el promedio de los tres valores de Color, dependiendo de qué tan cerca esté de cada uno de los vértices. Esto se ve muy bien en las esquinas del triángulo, donde los fragmentos toman el valor de color que indicamos en los datos del vértice.
Mirando hacia el futuro, diré que las coordenadas de textura se transmiten de la misma manera, lo que facilita la superposición de texturas en nuestras primitivas.
Creo que vale la pena terminar este artículo, lo más difícil está detrás de nosotros, pero lo más interesante apenas está comenzando. Si tiene preguntas o si vio un error en el artículo, escríbalo en los comentarios, se lo agradeceré.
En el próximo artículo, veremos las transformaciones, aprenderemos sobre variables unifrom y aprenderemos cómo imponer texturas en primitivas.