跳到主要内容

顶点缓冲对象

在现代OpenGL中,顶点数据通过缓冲对象高效地传输到GPU。VBO、VAO和EBO是管理顶点数据的三个核心概念,理解它们对于编写高效的OpenGL程序至关重要。

为什么需要缓冲对象?

在早期的立即模式中,每次绘制都需要将顶点数据从CPU传输到GPU:

// 立即模式(已废弃)- 每帧都传输数据
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glEnd();

这种方式效率极低,因为CPU-GPU数据传输是主要瓶颈。

现代OpenGL使用缓冲对象解决这个问题:将数据一次性上传到GPU显存,之后可以多次使用。

VBO(顶点缓冲对象)

VBO(Vertex Buffer Object)用于在GPU显存中存储顶点数据。

创建VBO

GLuint VBO;
glGenBuffers(1, &VBO); // 生成缓冲对象

绑定VBO

glBindBuffer(GL_ARRAY_BUFFER, VBO);

GL_ARRAY_BUFFER 表示这是一个顶点缓冲。绑定后,后续的缓冲操作都会针对这个VBO。

上传数据

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

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

glBufferData 的参数说明:

参数说明
target缓冲类型(GL_ARRAY_BUFFER)
size数据大小(字节)
data数据指针
usage数据使用模式

usage参数选项:

说明
GL_STATIC_DRAW数据设置一次,使用多次
GL_DYNAMIC_DRAW数据多次修改,使用多次
GL_STREAM_DRAW数据设置一次,使用很少

完整示例

// 1. 定义顶点数据
float vertices[] = {
// 位置 // 颜色
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f // 左上
};

// 2. 创建并绑定VBO
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

// 3. 上传数据到GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

VAO(顶点数组对象)

VAO(Vertex Array Object)是一个状态容器,它存储顶点属性配置和对应的VBO引用。

为什么需要VAO?

想象一下绘制多个物体的情况:

// 没有VAO的情况:每次绘制都需要重新配置
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glVertexAttribPointer(...);
glEnableVertexAttribArray(...);
glDrawArrays(...);

glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glVertexAttribPointer(...);
glEnableVertexAttribArray(...);
glDrawArrays(...);

使用VAO可以简化这个过程:

// 有VAO的情况:只需绑定VAO
glBindVertexArray(VAO1);
glDrawArrays(...);

glBindVertexArray(VAO2);
glDrawArrays(...);

VAO存储的内容

VAO存储以下状态:

  • 顶点属性的启用/禁用状态
  • 顶点属性的配置(glVertexAttribPointer
  • VBO的绑定(通过 glVertexAttribPointer 时的绑定)
  • EBO的绑定

创建和使用VAO

// 创建VAO
GLuint VAO;
glGenVertexArrays(1, &VAO);

// 绑定VAO(开始记录状态)
glBindVertexArray(VAO);

// 配置顶点属性(这些状态会被VAO记录)
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 解绑VAO(结束记录)
glBindVertexArray(0);

绘制时使用VAO

// 渲染循环中
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

EBO(元素缓冲对象)

EBO(Element Buffer Object),也称为IBO(Index Buffer Object),用于指定顶点的绘制顺序,实现顶点复用。

为什么需要EBO?

绘制一个矩形需要两个三角形,共6个顶点:

// 不使用EBO:6个顶点(有重复)
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, // 左下
// 第二个三角形
-0.5f, -0.5f, 0.0f, // 左下(重复)
-0.5f, 0.5f, 0.0f, // 左上
0.5f, 0.5f, 0.0f // 右上(重复)
};

使用EBO只需要4个唯一顶点:

// 使用EBO:4个唯一顶点
float vertices[] = {
0.5f, 0.5f, 0.0f, // 0: 右上
0.5f, -0.5f, 0.0f, // 1: 右下
-0.5f, -0.5f, 0.0f, // 2: 左下
-0.5f, 0.5f, 0.0f // 3: 左上
};

unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

创建和使用EBO

// 创建EBO
GLuint EBO;
glGenBuffers(1, &EBO);

// 绑定VAO
glBindVertexArray(VAO);

// 绑定VBO并上传数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 绑定EBO并上传索引数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 配置顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 解绑VAO
glBindVertexArray(0);

使用EBO绘制

glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements 参数说明:

参数说明
mode图元类型
count索引数量
type索引数据类型
indices索引数据偏移(使用EBO时为0)

顶点属性配置

glVertexAttribPointer 是配置顶点属性的核心函数:

void glVertexAttribPointer(
GLuint index, // 属性位置
GLint size, // 分量数量(1,2,3,4)
GLenum type, // 数据类型
GLboolean normalized, // 是否归一化
GLsizei stride, // 步长
const void* pointer // 偏移量
);

步长和偏移

当顶点数据包含多种属性时,需要正确设置步长和偏移:

// 顶点数据:位置(3) + 颜色(3) + 纹理坐标(2)
float vertices[] = {
// 位置 // 颜色 // 纹理坐标
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};

// 每个顶点8个float,步长 = 8 * sizeof(float)
// 位置属性:偏移0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 颜色属性:偏移3个float
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 纹理坐标:偏移6个float
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

数据布局图解

顶点数据布局:
┌─────────────────────────────────────────────────────────────┐
│ 顶点0 │
├──────────┬──────────┬──────────┬────────────────────────────┤
│ 位置(x,y,z) │ 颜色(r,g,b) │ 纹理(s,t) │ │
│ 3 floats │ 3 floats │ 2 floats │ │
├──────────┴──────────┴──────────┴────────────────────────────┤
│ 顶点1 │
├──────────┬──────────┬──────────┬────────────────────────────┤
│ 位置(x,y,z) │ 颜色(r,g,b) │ 纹理(s,t) │ │
└──────────┴──────────┴──────────┴────────────────────────────┘

步长(stride) = 8 * sizeof(float) = 32字节
位置偏移 = 0
颜色偏移 = 3 * sizeof(float) = 12字节
纹理偏移 = 6 * sizeof(float) = 24字节

完整示例:绘制带颜色的矩形

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

int main() {
// 初始化GLFW
glfwInit();
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, "VBO VAO EBO Example", NULL, NULL);
if (window == NULL) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);

if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cerr << "Failed to initialize GLAD" << std::endl;
return -1;
}

// 顶点数据
float vertices[] = {
// 位置 // 颜色
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f // 左上
};

unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

// 创建VAO、VBO、EBO
GLuint VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

// 绑定VAO
glBindVertexArray(VAO);

// 绑定VBO并上传数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 绑定EBO并上传数据
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 配置颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 解绑VAO
glBindVertexArray(0);

// 着色器代码
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
void main() {
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
)";

const char* fragmentShaderSource = R"(
#version 330 core
in vec3 ourColor;
out vec4 FragColor;
void main() {
FragColor = vec4(ourColor, 1.0);
}
)";

// 编译着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

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

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

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

// 渲染循环
while (!glfwWindowShouldClose(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);
glfwPollEvents();
}

// 清理资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);

glfwTerminate();
return 0;
}

资源管理

使用完缓冲对象后应该释放资源:

// 删除缓冲对象
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);

// 删除顶点数组对象
glDeleteVertexArrays(1, &VAO);

// 删除着色器程序
glDeleteProgram(shaderProgram);

最佳实践

1. 初始化时创建VAO/VBO

void initBuffers() {
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);
// 配置属性...
glBindVertexArray(0);
}

2. 渲染时只绑定VAO

void render() {
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
}

3. 使用RAII封装

class Mesh {
public:
GLuint VAO, VBO, EBO;
GLsizei indexCount;

Mesh(const std::vector<float>& vertices, const std::vector<unsigned int>& indices) {
indexCount = indices.size();

glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);

// 配置属性...

glBindVertexArray(0);
}

~Mesh() {
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
}

void draw() {
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
}
};

小结

VBO、VAO和EBO是现代OpenGL顶点数据管理的核心:

  • VBO:存储顶点数据在GPU显存中
  • VAO:存储顶点属性配置状态
  • EBO:存储顶点索引,实现顶点复用

正确使用这些对象可以显著提高渲染效率,减少CPU-GPU数据传输。

下一章我们将学习 纹理映射,为物体添加纹理。

调试建议

如果渲染结果不正确,检查以下几点:

  1. VAO是否正确绑定
  2. 顶点属性是否正确配置
  3. 步长和偏移是否正确
  4. 索引数据类型是否匹配