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_FLOAT
  • normalized:是否将非浮点数据归一化到 [-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