Buffers y Uniforms en OpenGL

Search for a command to run...

No comments yet. Be the first to comment.
Artículos dedicados a los gráficos por computadora, con explicaciones claras de los conceptos clave y ejemplos prácticos de técnicas como renderizado, iluminación, transformaciones, rasterización y más.
Marcos de Referencia para Crear Gráficos 3D
El pixel art es un estilo de arte digital que ha ganado gran popularidad en los últimos años, especialmente en la industria de los videojuegos, donde ha surgido un género completo que adopta esta esté

Transformando la narrativa de la nota roja en datos estructurados para la inteligencia territorial.

El color constituye uno de los fenómenos más importantes y fascinantes de la experiencia humana, y puede abordarse desde dos perspectivas complementarias. Por un lado, se encuentra su dimensión psicol

La transferencia de color (color transfer). Es una técnica del procesamiento de imágenes cuyo objetivo es modificar las propiedades de color de una imagen a partir de otra como referencia, preservando la estructura visual de la escena original. Exist...

Python es un lenguaje muy querido por su facilidad de uso y por la rapidez con la que permite escribir código. Sin embargo, una de sus limitaciones más conocidas es el rendimiento en tareas de cómputo intensivo, pues al tratarse de un lenguaje interp...

En la computación gráfica, el pipeline es simplemente el camino que siguen los datos hasta convertirse en los píxeles que vemos en pantalla. En un artículo anterior conté de manera general cómo funciona este flujo; ahora veremos cómo se aplica dentro de OpenGL.
En OpenGL, la aplicación no dibuja los píxeles directamente. Lo que hacemos es enviar datos al pipeline —posiciones de vértices, colores, texturas, transformaciones— y la GPU se encarga de procesarlos paso a paso hasta producir la imagen final en el framebuffer.
A grandes rasgos, el pipeline de OpenGL pasa por estas etapas:
Aplicación (CPU): desde C++ (u otro lenguaje) definimos la geometría, configuramos buffers y cargamos los shaders.
Vertex Shader: transforma cada vértice aplicando las matrices de modelo, vista y proyección.
Ensamblaje de primitivas: los vértices se agrupan en puntos, líneas o triángulos.
Rasterización: esas primitivas se convierten en fragmentos (los candidatos a ser píxeles), interpolando atributos como color o coordenadas de textura.
Fragment Shader: decide el color de cada fragmento, aplicando iluminación o texturas.
Pruebas y mezcla: antes de dibujar el píxel final, se aplican pruebas (profundidad, stencil) y se combinan colores con el fondo si es necesario.
En las siguientes secciones veremos cómo todo esto se traduce en la práctica con OpenGL: cómo enviamos datos a la GPU, cómo se guardan en buffers y cómo los shaders los usan para construir una escena 3D.
Para que el pipeline de OpenGL funcione, necesitamos pasarle datos desde nuestra aplicación en C++. Estos datos suelen dividirse en dos tipos:
Datos que varían por vértice o por fragmento (por ejemplo, la posición de un vértice o su color).
Datos que permanecen constantes para todos los vértices o fragmentos de una misma invocación de renderizado (por ejemplo, una matriz de transformación o la posición de la cámara).
OpenGL ofrece dos mecanismos distintos para enviar esta información a los shaders: buffers y uniforms.
Los Vertex Buffer Objects (VBOs) permiten guardar en la memoria de la GPU grandes cantidades de datos, como posiciones de vértices, normales, colores o coordenadas de textura. Estos datos se asocian a atributos de vértice en el vertex shader, lo que significa que cada vértice recibe sus propios valores.
Ejemplo conceptual en GLSL:
layout(location = 0) in vec3 position; // posición de cada vértice
layout(location = 1) in vec3 color; // color de cada vértice
Cada llamada a glDrawArrays() o glDrawElements() hará que OpenGL recorra el buffer, entregando a la GPU un conjunto distinto de atributos para cada vértice.
Las uniforms funcionan de manera diferente: son variables globales dentro de un shader y su valor se mantiene constante durante todo un draw call.
A diferencia de los atributos de vértice, no cambian de uno a otro.
Ejemplo en GLSL:
uniform mat4 mv_matrix; // matriz de modelo-vista
uniform mat4 proj_matrix; // matriz de proyección
Estas variables se cargan desde la aplicación mediante funciones como glGetUniformLocation() y glUniformMatrix4fv(). Un caso típico de uso es enviar las matrices de transformación que se aplican a todos los vértices de un objeto en una misma pasada de renderizado.
En OpenGL, los datos geométricos no se envían uno por uno desde la CPU. En vez de eso, se cargan en la memoria de la GPU mediante estructuras llamadas buffers. Más adelante, estos buffers se organizan y se enlazan a los atributos de los shaders usando los Vertex Array Objects (VAOs).
Un Vertex Buffer Object (VBO) es un bloque de memoria en la GPU que contiene datos de vértices. Por ejemplo, un cubo puede representarse como 36 vértices (12 triángulos, 3 vértices cada uno). Estos datos se cargan una única vez y luego la GPU puede acceder a ellos directamente durante el renderizado.
Un ejemplo típico de creación de un VBO es:
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
glGenBuffers: crea un identificador de buffer.
glBindBuffer: lo activa para operar sobre él.
glBufferData: copia los datos desde la RAM del CPU hasta la memoria de la GPU.
El Vertex Array Object (VAO) es un contenedor que guarda la configuración de cómo se deben interpretar los datos del VBO. Esto incluye:
Qué atributos de vértice existen (posición, color, normales, etc.).
Cómo se distribuyen en memoria (stride, offset).
Qué buffer está asociado a cada atributo.
Ejemplo de creación:
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
De esta forma, cada vez que se hace glBindVertexArray(vao[0]), toda la configuración de atributos y buffers queda lista sin tener que repetirla.
Una vez que los datos están cargados en el VBO, hay que indicarle a OpenGL cómo debe entregarlos al vertex shader. Esto se hace con glVertexAttribPointer y glEnableVertexAttribArray.
Ejemplo
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, ...): indica que el atributo en la location 0 del shader corresponde a grupos de 3 floats consecutivos en el buffer (x, y, z).
glEnableVertexAttribArray(0): habilita el atributo de posición para que pueda usarse en el shader.
En el shader se vería así:
layout(location = 0) in vec3 position;
De esta forma, cada vértice del cubo se enviará automáticamente al shader en la variable position.
Es importante distinguir entre:
Etapa de inicialización (init())
Crear los buffers (VBOs).
Cargar los datos en ellos.
Configurar los VAOs y atributos.
Etapa de renderizado por frame (display())
Limpiar los buffers de color y profundidad.
Activar el shader program.
Actualizar uniforms (por ejemplo, las matrices de transformación).
Llamar a glDrawArrays() o glDrawElements().
glDrawArrays(GL_TRIANGLES, 0, 36);
Los VBOs guardan los datos en la GPU.
Los VAOs almacenan la configuración de cómo leer esos datos.
Los vertex attributes conectan directamente esos datos con el shader.
Entender esta relación es fundamental para trabajar con geometría en OpenGL, porque sobre esta base se añaden más atributos (colores, normales, coordenadas de textura) en escenas más complejas.
En OpenGL no todo tiene que cambiar vértice a vértice. Muchas veces necesitamos parámetros que se mantengan constantes durante toda una llamada de dibujo. Para eso existen las variables uniformes (uniforms), que son la forma estándar de enviar datos globales desde la aplicación en C++ a los shaders.
Un uniform se declara en GLSL dentro del shader. A diferencia de los atributos (in), un uniform no cambia de un vértice a otro: su valor es el mismo para todos los vértices y fragmentos durante un draw call.
Ejemplo en un vertex shader:
#version 430 core
layout(location = 0) in vec3 position;
uniform mat4 mv_matrix; // Modelo-Vista
uniform mat4 proj_matrix; // Proyección
void main(void) {
gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
}
Aquí mv_matrix combina las transformaciones de modelo y vista, mientras que proj_matrix define la proyección (perspectiva u ortográfica).
Ambas matrices se aplican igual a todos los vértices en el mismo dibujo.
Una vez compilado y enlazado el programa de shaders, podemos localizar y actualizar los uniforms desde C++. Esto ocurre en dos pasos:
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
Nota: el programa (
renderingProgram) debe estar enlazado antes de llamar aglGetUniformLocation.
glUniform*:glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv: actualiza un uniform de tipo mat4.
El 1 indica que se pasa una sola matriz.
GL_FALSE señala que no deben transponerse los datos.
glm::value_ptr(...) obtiene un puntero compatible con OpenGL.
En la función display(), justo antes de dibujar, se realiza este proceso.
Algunos de los usos más comunes de uniforms son:
Matrices de transformación: modelo, vista, proyección.
Parámetros de cámara: posición, dirección, matrices de vista.
Propiedades de materiales: color base, reflectividad, coeficientes de iluminación.
Luces: posición, color, intensidad.
Texturas: identificadores de samplers (aunque las texturas en sí se gestionan con otra API, se referencian mediante uniforms).
Atributos (in) → Se actualizan por vértice y pueden ser interpolados por el rasterizador.
Uniforms → Se mantienen constantes para todos los vértices/fragmentos durante un draw call.
Ejemplo práctico:
Un atributo puede ser el color distinto en cada vértice del cubo.
Un uniform puede ser la matriz de proyección que se aplica por igual a todos los vértices del cubo.
En pocas palabras: los uniforms son la forma de pasar información global al shader. Permiten aplicar transformaciones, parámetros de cámara o iluminación de manera coherente a toda la geometría, sin necesidad de duplicar datos en cada vértice.
Ya que hemos repasado los conceptos de buffers, shaders y uniforms, veamos cómo se usan en un programa real. El siguiente ejemplo dibuja un cubo 3D coloreado paso a paso, poniendo en práctica todo lo explicado sobre el pipeline de OpenGL.
Lo primero es definir los vértices que forman las caras del cubo y asignarles un color. Cada cara está compuesta por dos triángulos (6 vértices), y en total el cubo necesita 36 vértices.
// Posiciones de los vértices (36 vértices = 12 triángulos)
float cubeVertexPositions[108] = {
// cara frontal
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
// ... resto de las caras (izquierda, derecha, trasera, etc.)
};
// Colores asociados a cada vértice
float cubeVertexColors[108] = {
// frontal (rojo)
1.0f, 0.0f, 0.0f,
1.0f, 0.5f, 0.5f,
1.0f, 0.0f, 0.0f,
// ... resto de colores
};
Un VAO almacena la configuración de atributos, y dos VBOs guardan posiciones y colores.
GLuint vao[1];
GLuint vbo[2];
// Generar y enlazar VAO
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
// VBO de posiciones
glGenBuffers(2, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertexPositions), cubeVertexPositions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
// VBO de colores
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertexColors), cubeVertexColors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
El vertex shader transforma posiciones con las matrices de modelo-vista y proyección, y pasa el color al fragment shader.
#version 430 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
out vec4 varyingColor;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main(void) {
gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
varyingColor = vec4(color, 1.0);
}
El fragment shader recibe el color interpolado y lo envía al framebuffer:
#version 430 core
in vec4 varyingColor;
out vec4 fragColor;
void main(void) {
fragColor = varyingColor;
}
Desde C++ cargamos las matrices de transformación en los uniforms:
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
En la función de renderizado, limpiamos la pantalla y dibujamos los vértices:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(renderingProgram);
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glBindVertexArray(vao[0]);
glDrawArrays(GL_TRIANGLES, 0, 36);
El programa muestra un cubo 3D con colores en cada cara, renderizado con profundidad para que las caras ocultas no se dibujen.

VBOs y VAO para manejar los datos de vértices.
Shaders para transformar y colorear la geometría.
Uniforms para aplicar matrices de cámara y proyección.
El pipeline de OpenGL trabajando de principio a fin.
A lo largo del artículo vimos cómo funciona el recorrido de los datos en OpenGL: desde que definimos un conjunto de vértices en la aplicación, hasta que finalmente esos datos se transforman en píxeles en pantalla.
Los buffers nos permiten organizar la geometría en memoria para que la GPU la procese eficientemente.
Los uniforms actúan como parámetros globales que guían al pipeline sin repetirse en cada vértice.
Las etapas programables (vertex y fragment shaders) son los lugares donde podemos intervenir directamente para dar forma y color a lo que se dibuja.
Finalmente, todo desemboca en el framebuffer, que es el lienzo final donde queda guardada la imagen.
Este recorrido muestra que dibujar en 3D no es magia: es un proceso ordenado de transformaciones y cálculos que, etapa tras etapa, convierten datos numéricos en una escena visible.
Con estos conceptos en mente, ya cuentas con la base para dar el siguiente paso: experimentar con texturas, iluminación y efectos visuales que harán tus programas mucho más expresivos y atractivos.