缓冲对象与顶点
1. 缓冲对象
OpenGL 运行在 GPU 上,而 C++ 代码运行在 CPU 上。要让 GPU 处理图形数据,需要把数据从 CPU 的内存复制到 GPU 的显存中。缓冲对象(Buffer Object)就是 OpenGL 用来管理 GPU 端内存的机制——它本质上是在 GPU 上分配的一块存储区域,可以把数据复制进去,之后 GPU 就直接从中读取。
操作缓冲对象的一般流程是三步:生成(glGenBuffers)→ 绑定(glBindBuffer)→ 填充数据(glBufferData)。
1.1 生成缓冲对象(glGenBuffers)
glGenBuffers 函数用于生成一个或多个缓冲对象的名称(ID),并将其存储在指定的变量中。
函数原型:glGenBuffers(GLsizei n, GLuint *buffers);
n:要生成的缓冲对象的数量buffers:指向一个无符号整数数组的指针,用于存储生成的缓冲对象名称
GLuint VBO;
glGenBuffers(1, &VBO); // 生成一个顶点缓冲对象
GLuint VBOs[2];
glGenBuffers(2, VBOs); // 生成两个顶点缓冲对象
1.2 绑定缓冲对象(glBindBuffer)
glBindBuffer 函数用于绑定一个缓冲对象到一个特定的绑定点(bind point)上。
函数原型:glBindBuffer(GLenum target, GLuint buffer);
target:指定要绑定的缓冲对象的类型buffer:要绑定的缓冲对象的名称(ID)
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定顶点缓冲对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定索引缓冲对象
参数 target 决定了这块缓冲对象的用途,常见的取值有:
| target 常量 | 用途 |
|---|---|
GL_ARRAY_BUFFER |
顶点属性数据 |
GL_ELEMENT_ARRAY_BUFFER |
顶点索引数据 |
GL_UNIFORM_BUFFER |
uniform 变量块 |
GL_SHADER_STORAGE_BUFFER |
着色器可读写的通用存储 |
注:绑定点就像一个“插槽”,OpenGL 的状态机中每种 target 对应一个当前绑定的缓冲对象。当你调用
glBufferData等函数时,操作会作用于当前绑定到该 target 的缓冲对象。
1.3 缓冲数据(glBufferData)
glBufferData 函数用于创建并初始化缓冲对象的数据存储。
函数原型:glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage);
target:指定要初始化的缓冲对象的类型size:指定要分配的缓冲区大小(字节数)data:指向要复制到缓冲区的数据的指针,传入nullptr则只分配空间不填充usage:指定缓冲区的使用方式,帮助 OpenGL 优化存储位置
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 初始化顶点缓冲对象
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 初始化索引缓冲对象
usage 参数常见的取值:
| 常量 | 含义 |
|---|---|
GL_STATIC_DRAW |
数据设置一次,使用多次(如静态场景的顶点数据) |
GL_DYNAMIC_DRAW |
数据会被多次修改和使用(如动画中的顶点) |
GL_STREAM_DRAW |
数据每次绘制都会改变(如每帧更新的粒子位置) |
2. 顶点
顶点(vertex)是构建所有图形的基本单元。一个顶点可以携带多种属性:位置坐标、颜色、法线方向、纹理坐标等。
在代码中,顶点数据通常就是一个 float 数组。例如一个三角形的三个顶点,每个顶点包含位置(x, y, z)三个分量:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
对 GPU 来说,这只是一串连续的 float 数字,它本身并不知道哪些数字代表位置、哪些代表颜色。你需要通过顶点属性(vertex attribute)的配置来告诉 GPU 如何解读这些数据。
如果你在顶点数据中加入了颜色信息,光栅化阶段会自动在顶点之间对颜色进行插值(interpolation),使得三角形的颜色从顶点向内部平滑过渡。顶点颜色如何传递给片元、插值的具体机制,会在着色器中详细讲解。
2.1 顶点缓冲对象
顶点缓冲对象(Vertex Buffer Object,VBO)是一种专门用来存储顶点数据的缓冲对象。把顶点数据从 CPU 内存复制到 GPU 显存后,GPU 就能高效地直接访问这些数据,避免每次绘制都从 CPU 传输。
使用 VBO 的完整流程:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
GLuint VBO;
glGenBuffers(1, &VBO); // 生成 VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定到顶点缓冲绑定点
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 复制数据
2.2 链接顶点属性
有了 VBO 之后,GPU 知道数据在哪里,但还不知道怎么解读它们。链接顶点属性(linking vertex attributes)就是告诉 GPU 这块数据中,每个顶点有几个分量、每个分量的类型、以及数据之间的间隔。
这一步通过 glVertexAttribPointer 完成:
函数原型:glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
index:顶点属性的索引(location),对应着色器中的layout(location = index)size:每个顶点该属性有几个分量(1~4)type:数据类型,如GL_FLOATnormalized:是否将非浮点数据归一化到 [-1, 1] 或 [0, 1]stride:连续两个顶点之间的字节间隔pointer:该属性在顶点数据中的偏移量(字节)
以刚才的位置数据为例:
// 位置属性:每个顶点 3 个 float,没有间隔,从偏移 0 开始
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 启用该顶点属性
当顶点包含多个属性时(比如位置 + 颜色),数据通常是交错排列(interleaved)的:
float vertices[] = {
// 位置(x, y, z) 颜色(r, g, b)
-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.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};
// 位置属性:3 个 float,stride 为 6 个 float,偏移为 0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性:3 个 float,stride 为 6 个 float,偏移为 3 个 float
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
注:
stride是两个相邻顶点起始地址之间的字节数,而不是某个属性内部分量之间的间隔。在交错排列中,每个顶点占 6 个float(24 字节),所以 stride 是6 * sizeof(float)。
2.3 元素缓冲对象
绘制一个矩形需要 4 个顶点,但实际上矩形由两个三角形组成,共需要 6 个顶点——其中有 2 个顶点是重复的。当模型复杂时,这种重复会非常浪费。
元素缓冲对象(Element Buffer Object,EBO,也叫索引缓冲 Index Buffer Object,IBO)通过索引来解决重复顶点的问题:你只存储不重复的顶点数据,再用一个索引数组指定绘制顺序。
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 // 左上
};
GLuint indices[] = {
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
};
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
使用 EBO 绘制时,把 glDrawArrays 替换为 glDrawElements:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
参数依次是:图元类型、索引数量、索引类型、偏移量。
2.4 顶点数组对象
到目前为止,配置顶点属性需要多次函数调用(绑定 VBO、设置属性指针、启用属性、绑定 EBO)。顶点数组对象(Vertex Array Object,VAO)可以把所有这些状态打包保存起来,之后只需要绑定 VAO 就能恢复整套配置。
VAO 记录的内容包括:
- 调用
glEnableVertexAttribArray或glDisableVertexAttribArray的状态 - 通过
glVertexAttribPointer配置的属性设置 - 当前绑定的
GL_ELEMENT_ARRAY_BUFFER(EBO)
GLuint VAO;
glGenVertexArrays(1, &VAO);
// 绑定 VAO,之后的顶点属性配置都会记录到这个 VAO 中
glBindVertexArray(VAO);
// 绑定 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(VAO 会记住这个绑定)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 解绑 VAO
glBindVertexArray(0);
之后在绘制循环中,只需要:
glBindVertexArray(VAO); // 恢复所有顶点属性配置
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
注:通常在初始化阶段创建 VAO 并完成配置,绘制时绑定它即可。解绑 VAO(绑定 0)是良好的习惯,可以防止后续的缓冲操作意外修改当前 VAO 的状态。
3. 以上内容的 C++ 封装
3.1 VertexBuffer 的封装
// VertexBufferLayout.hpp
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <concepts>
namespace MY::OpenGL
{
/**
* 属性的数据类型
*/
enum class AttributeType : uint8_t
{
Float = 0,
Float2 = 1,
Float3 = 2,
Float4 = 3,
Int = 4,
Int2 = 5,
Int3 = 6,
Int4 = 7,
UByte4 = 8
};
/**
* 顶点缓冲区的属性
* - name: 属性名称
* - type: 属性类型
* - normalized: 是否归一化(即将数据映射到[0, 1]或[-1, 1]区间)
* - offset: 属性在 VertexBufferLayout 中的偏移量
*/
struct VertexBufferAttribute
{
std::string name;
AttributeType type;
bool normalized = false;
uint32_t offset = 0;
};
template<typename... VBAttrs>
concept AllVertexBufferAttributes = (std::same_as<VBAttrs, VertexBufferAttribute> && ...);
class VertexBufferLayout
{
public:
VertexBufferLayout() = default;
~VertexBufferLayout() = default;
template<AllVertexBufferAttributes... VBAttrs>
requires AllVertexBufferAttributes<VBAttrs...>
VertexBufferLayout(VBAttrs&&... attrs)
{
(m_attributes.emplace_back(std::forward<VBAttrs>(attrs)), ...);
calculate_stride();
}
void clear() { m_attributes.clear(); m_stride = 0; }
public:
// 供外界访问的接口
uint32_t get_stride() const { return m_stride; }
size_t get_attribute_count() const { return m_attributes.size(); }
const VertexBufferAttribute& operator[](size_t index) const { return m_attributes[index]; }
private:
std::vector<VertexBufferAttribute> m_attributes;
uint32_t m_stride = 0;
/**
* @brief 计算当前 m_attributes 里每个属性的 offset 并更新 stride
* @warning 每次修改 attributes 后都需要调用此函数
*/
void calculate_stride();
/**
* @brief 将一个 VertexAttribute 对象添加到 m_attributes 中
* @warning 此函数不会更新 m_stride,需要在调用后手动调用 calculate_stride()
*/
void emplace_back(VertexBufferAttribute&& attr);
public:
// 对迭代器的支持,仅提供 const 版本的迭代器,防止外部修改 m_attributes
using const_iterator = std::vector<VertexBufferAttribute>::const_iterator;
const_iterator begin() const { return m_attributes.begin(); }
const_iterator end() const { return m_attributes.end(); }
};
} // namespace MY::OpenGL
// VertexBufferLayout.cpp
#include "VertexBufferLayout.hpp"
namespace MY::OpenGL
{
void VertexBufferLayout::calculate_stride()
{
static constexpr int float_size = sizeof(float);
static constexpr int int_size = sizeof(int);
static constexpr int ubyte_size = sizeof(unsigned char);
m_stride = 0;
for(auto& attr : m_attributes)
{
attr.offset = m_stride;
switch(attr.type)
{
case AttributeType::Float: m_stride += float_size; break;
case AttributeType::Float2: m_stride += 2 * float_size; break;
case AttributeType::Float3: m_stride += 3 * float_size; break;
case AttributeType::Float4: m_stride += 4 * float_size; break;
case AttributeType::Int: m_stride += int_size; break;
case AttributeType::Int2: m_stride += 2 * int_size; break;
case AttributeType::Int3: m_stride += 3 * int_size; break;
case AttributeType::Int4: m_stride += 4 * int_size; break;
case AttributeType::UByte4: m_stride += 4 * ubyte_size; break;
}
}
}
void VertexBufferLayout::emplace_back(VertexBufferAttribute&& attr)
{
m_attributes.emplace_back(std::move(attr));
}
} // namespace MY::OpenGL
// VertexBuffer.hpp
#pragma once
#include "VertexBufferLayout.hpp"
#include <glad/glad.h>
namespace MY::OpenGL
{
class VertexBuffer
{
public:
VertexBuffer(const float* data, uint32_t count, const VertexBufferLayout& layout);
~VertexBuffer();
// 禁止拷贝
VertexBuffer(const VertexBuffer&) = delete;
VertexBuffer& operator=(const VertexBuffer&) = delete;
// 允许移动
VertexBuffer(VertexBuffer&&);
VertexBuffer& operator=(VertexBuffer&&);
public:
GLuint get_vbo() const { return m_VBO; }
uint32_t get_vertex_count() const { return m_vertex_count; }
private:
GLuint m_VBO = 0; // 顶点缓冲对象
uint32_t m_vertex_count = 0; // 顶点数量
};
} // namespace MY::CG
#include "VertexBuffer.hpp"
namespace MY::OpenGL
{
VertexBuffer::VertexBuffer(const float* data, uint32_t count, const VertexBufferLayout& layout)
: m_vertex_count(count)
{
glGenBuffers(1, &m_VBO);
glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
glBufferData(GL_ARRAY_BUFFER, layout.get_stride() * count, data, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
VertexBuffer::~VertexBuffer()
{
glDeleteBuffers(1, &m_VBO);
}
VertexBuffer::VertexBuffer(VertexBuffer&& other) noexcept
: m_VBO(other.m_VBO), m_vertex_count(other.m_vertex_count)
{
other.m_VBO = 0;
other.m_vertex_count = 0;
}
VertexBuffer& VertexBuffer::operator=(VertexBuffer&& other) noexcept
{
if(this != &other)
{
glDeleteBuffers(1, &m_VBO);
m_VBO = other.m_VBO;
m_vertex_count = other.m_vertex_count;
other.m_VBO = 0;
other.m_vertex_count = 0;
}
return *this;
}
} // namespace MY::OpenGL
3.2 IndexBuffer 的封装
// IndexBuffer.hpp
#pragma once
#include <glad/glad.h>
namespace MY::OpenGL
{
class IndexBuffer
{
public:
IndexBuffer(const uint32_t* indices, uint32_t count);
~IndexBuffer();
// 禁止拷贝
IndexBuffer(const IndexBuffer&) = delete;
IndexBuffer& operator=(const IndexBuffer&) = delete;
// 允许移动(Vector 扩容时需要)
IndexBuffer(IndexBuffer&&);
IndexBuffer& operator=(IndexBuffer&&);
public:
GLuint get_ibo() const { return m_IBO; }
uint32_t get_index_count() const { return m_index_count; }
private:
GLuint m_IBO; // 索引缓冲对象
uint32_t m_index_count; // 索引的数量
};
} // namespace MY::OpenGL
// IndexBuffer.cpp
#include "IndexBuffer.hpp"
namespace MY::OpenGL
{
IndexBuffer::IndexBuffer(const uint32_t* indices, uint32_t count)
: m_index_count(count)
{
glGenBuffers(1, &m_IBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(count * sizeof(uint32_t)), indices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
IndexBuffer::~IndexBuffer()
{
glDeleteBuffers(1, &m_IBO);
}
IndexBuffer::IndexBuffer(IndexBuffer&& other) noexcept
: m_IBO(other.m_IBO), m_index_count(other.m_index_count)
{
other.m_IBO = 0;
other.m_index_count = 0;
}
IndexBuffer& IndexBuffer::operator=(IndexBuffer&& other) noexcept
{
if (this != &other)
{
glDeleteBuffers(1, &m_IBO);
m_IBO = other.m_IBO;
m_index_count = other.m_index_count;
other.m_IBO = 0;
other.m_index_count = 0;
}
return *this;
}
} // namespace MY::OpenGL



