Skip to main content

Command Palette

Search for a command to run...

Buffers y Uniforms en OpenGL

Updated
10 min read
Buffers y Uniforms en OpenGL

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.

2. Mecanismos de Envío de Datos: Buffers vs. Uniforms

Para que el pipeline de OpenGL funcione, necesitamos pasarle datos desde nuestra aplicación en C++. Estos datos suelen dividirse en dos tipos:

  1. Datos que varían por vértice o por fragmento (por ejemplo, la posición de un vértice o su color).

  2. 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.


2.1 Buffers y Vertex Attributes

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.


2.2 Variables Uniformes

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.

3. Buffers y Vertex Attributes (VBOs y VAOs)

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).


3.1 Vertex Buffer Object (VBO)

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.


3.2 Vertex Array Object (VAO)

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.


3.3 Atributos de Vértice

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.


3.4 Flujo de Inicialización vs. Renderizado

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.

4. Variables Uniformes

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.


4.1 Cómo se declaran los uniforms en 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.


4.2 Cómo se cargan los uniforms desde C++

Una vez compilado y enlazado el programa de shaders, podemos localizar y actualizar los uniforms desde C++. Esto ocurre en dos pasos:

  1. Obtener la ubicación del uniform en el programa:
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");

Nota: el programa (renderingProgram) debe estar enlazado antes de llamar a glGetUniformLocation.

  1. Enviar los datos al uniform con funciones 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.


4.3 Uso típico de Uniforms

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).


4.4 Diferencia con los Atributos

  • 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.

5. Ejemplo

Dibujar un cubo coloreado en OpenGL

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.


5.1 Preparar los datos del cubo

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
};

5.2 Crear VAO y VBOs

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);

5.3 Shaders: Vertex y Fragment

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;
}

5.4 Uso de uniforms

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));

5.5 Dibujar el cubo

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);

5.6 Resultado

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.

Conclusión

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.

Gráficos por computadora.

Part 2 of 6

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.

Up next

Espacios de Coordenadas en OpenGL

Marcos de Referencia para Crear Gráficos 3D

More from this blog