OpenGL Vertex Rendering with VAO, VBO, and EBO

Vertex Buffer Object (VBO): Data Storage

A VBO is a memory buffer allocated in GPU memory to store vertex attributes such as positions, normals, colors, or texture coordinates. By keeping data on the GPU, it eliminates redundant transfers from the CPU, significantly improving performance.

  • Binary Storage: Data is stored as a contiguous byte stream, e.g., [x1,y1,z1,r1,g1,b1,x2,y2,z2,...].
  • Usage Hints: Memory access patterns can be optimized using flags like GL_STATIC_DRAW (data unchanged), GL_DYNAMIC_DRAW (frequent updates), or GL_STREAM_DRAW (one-time use).

Only one VBO can be bound to the GL_ARRAY_BUFFER target at a time. Data upload must occur while the target is active.

GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);

float positionData[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

glBufferData(GL_ARRAY_BUFFER, sizeof(positionData), positionData, GL_STATIC_DRAW);

Vertex Array Object (VAO): Attribute Configuration Blueprint

A VAO encapsulates the state needed to interpret data in VBOs. It stores how vertex attributes are laid out, including stride, offset, data type, and enabled channels — effectively acting as a "rendering profile".

  • Attribute Pointers: glVertexAttribPointer defines how to read a specific attribute (e.g., position, color) from the bound VBO.
  • Enable Channels: glEnableVertexAttribArray activates a logical attribute slot for shader access.
  • State Encapsulation: Once configured, binding a VAO restores all associated VBO bindings and attribute settings automatically.

Multiple VBOs can be linked to a single VAO via different attribute indices. Each attribute index corresponds to a layout(location = N) declaration in the vertex shader.

GLuint vertexArray;
glGenVertexArrays(1, &vertexArray);
glBindVertexArray(vertexArray);

// Bind VBO and configure attribute 0 as position
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

In the vertex shader:

layout(location = 0) in vec3 aPosition;

When rendering, only glBindVertexArray(vertexArray) is required — all VBOs and attribute configurations are restored automatically.

Element Buffer Objecct (EBO): Index-Based Geometry

When multiple triangles share vertices (e.g., meshes, grids), duplicating vertex data wastes memory. An EBO solves this by storing integer indices that reference vertices in a VBO.

Instead of repeating vertices for adjacent triangles:

// Without EBO: 6 vertices for 2 triangles sharing 2 points
// With EBO: 4 unique vertices + 6 indices

Example vertex and index data:

float vertices[] = {
    -0.5f, -0.5f, 0.0f,  // 0
     0.5f, -0.5f, 0.0f,  // 1
     0.0f,  0.5f, 0.0f,  // 2
    -0.5f,  0.7f, 0.0f   // 3
};

unsigned int indices[] = {
    0, 1, 3,  // First triangle
    1, 2, 3   // Second triangle
};

Setup steps:

GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// Render using indices
glBindVertexArray(vertexArray);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

Key constraint: Each VAO holds only one EBO. The last bound EBO during VAO configuration is the one used during rendering. EBOs are not shared across VAOs.

Rendering Pipeline Summary

  1. Initialize GPU context and shaders.
  2. Create and populate VBO(s) with vertex data.
  3. Create and bind VAO; configure attribute pointers to link VBOs to shader inputs.
  4. If using indexed rendering, create and bind EBO with index list.
  5. In the render loop: bind VAO → use shader program → call glDrawArrays or glDrawElements.

Face Culling for Performance

OpenGL can automatically discard triangles whose normals face away from the camera, reducing fill rate. This is controlled by the winding order of vertices (counter-clockwise by default).

glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // Discard back-facing triangles

For a triangle with vertices A→B→C, if the cross product (B−A) × (C−A) points toward the viewer, it's front-facing. Otherwise, it's culled.

Complete Working Example

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>

// Vertex shader source
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main() {
    gl_Position = vec4(aPos, 1.0);
}
)";

// Fragment shader source
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
)";

void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

int main() {
    if (!glfwInit()) return -1;

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Indexed Triangle", nullptr, nullptr);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);

    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) return -1;

    glViewport(0, 0, 800, 600);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    // Vertex data
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f,
        -0.5f,  0.7f, 0.0f
    };

    // Index data
    unsigned int indices[] = { 0, 1, 3, 1, 2, 3 };

    // VAO
    GLuint vao;
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // VBO
    GLuint vbo;
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // EBO
    GLuint ebo;
    glGenBuffers(1, &ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // Shader compilation
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
    glCompileShader(vertexShader);

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
    glCompileShader(fragmentShader);

    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // Render loop
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);
    }

    glfwTerminate();
    return 0;
}

Tags: OpenGL VBO VAO EBO gpu

Posted on Fri, 31 Jul 2026 16:46:39 +0000 by shelbytll