强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 3 章:OpenGL 基础

第 3 章:OpenGL 基础

本章进入实战阶段。你将学会 OpenGL 的核心数据对象(VAO、VBO、EBO)、编写第一个顶点和片段着色器,并在屏幕上绘制三角形。


3.1 OpenGL 的状态机模型

OpenGL 本质上是一个状态机(State Machine)。你通过函数调用改变状态,后续的绘制命令会使用当前状态。

┌──────────────────────────────┐
│         OpenGL 状态机         │
│                              │
│  当前着色器程序: program_id   │
│  当前绑定 VAO:   vao_id      │
│  当前绑定纹理:   texture_id  │
│  视口大小:       800x600     │
│  清除颜色:       (0.1,0.1,0.2)│
│  深度测试:       开启         │
│  ...                         │
└──────────────────────────────┘
         ▲
         │  glUseProgram(program)
         │  glBindVertexArray(vao)
         │  glBindTexture(GL_TEXTURE_2D, tex)
         │
    你的代码

💡 理解状态机模型是写 OpenGL 代码的基础。glBind*() 系列函数就是在切换"当前活动对象"。


3.2 核心数据对象:VAO、VBO、EBO

3.2.1 三者关系

对象全称存储位置作用
VBOVertex Buffer ObjectGPU 显存存储顶点数据(位置、颜色、UV 等)
EBOElement Buffer ObjectGPU 显存存储索引数据,避免重复顶点
VAOVertex Array ObjectGPU 显存记录 VBO 和 EBO 的绑定状态及顶点属性配置

3.2.2 数据流图

你的数据 (CPU 内存)
    │
    │  glBufferData()
    ▼
┌───────┐     ┌───────┐
│  VBO  │     │  EBO  │     ← GPU 显存
│ 顶点数据│    │ 索引数据│
└───┬───┘     └───┬───┘
    │              │
    │  glVertexAttribPointer()
    │  glVertexAttribIPointer()
    ▼              ▼
┌──────────────────────┐
│         VAO          │    ← 记录配置状态
│  属性 0: 位置(vec3)   │
│  属性 1: 颜色(vec3)   │
│  属性 2: UV(vec2)     │
│  EBO 绑定             │
└──────────────────────┘
    │
    │  glDrawElements()
    ▼
  GPU 执行渲染

3.2.3 为什么不直接把数据发给 GPU?

因为 GPU 显存(VRAM)的读写带宽远高于 CPU→GPU 的传输带宽。VBO 将数据一次性上传到 GPU,后续渲染直接从 VRAM 读取,避免了每帧传输的瓶颈。

操作耗时量级
GPU 显存读取~100 ns
CPU → GPU 传输(PCIe)~10 μs
每帧重建数据❌ 避免

3.3 定义顶点数据

3.3.1 三角形的三个顶点

// 三角形的顶点数据
// 每个顶点包含:位置 (x, y, z) + 颜色 (r, g, b)
float vertices[] = {
    // ---- 位置 ----    ---- 颜色 ----
     0.0f,  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,  // 右下:蓝色
};

3.3.2 顶点属性布局(Vertex Layout)

内存布局 (交错排列 Interleaved):
┌──────────┬──────────┐┌──────────┬──────────┐┌──────────┬──────────┐
│ position │  color   ││ position │  color   ││ position │  color   │
│  3 floats│ 3 floats ││  3 floats│ 3 floats ││  3 floats│ 3 floats │
│ (12字节)  │ (12字节) ││ (12字节)  │ (12字节) ││ (12字节)  │ (12字节) │
├──────────┴──────────┤├──────────┴──────────┤├──────────┴──────────┤
│     stride = 24字节  ││     stride = 24字节  ││     stride = 24字节  │
│    offset = 0       ││    offset = 12      ││                      │
│    offset = 0       ││    offset = 12      ││                      │
概念说明
stride相邻两个顶点之间的字节间隔
offset该属性在单个顶点数据中的起始偏移
layout (location = N)属性在着色器中的位置编号

3.4 创建 VAO / VBO / EBO

3.4.1 完整代码:创建与配置

unsigned int VAO, VBO, EBO;

// ===== 1. 生成对象 =====
glGenVertexArrays(1, &VAO);   // 生成 1 个 VAO
glGenBuffers(1, &VBO);        // 生成 1 个 VBO
glGenBuffers(1, &EBO);        // 生成 1 个 EBO

// ===== 2. 绑定 VAO(后续所有 VBO/EBO 操作都会记录到此 VAO) =====
glBindVertexArray(VAO);

// ===== 3. 上传 VBO 数据 =====
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// ===== 4. 上传 EBO 数据 =====
unsigned int indices[] = { 0, 1, 2 };
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// ===== 5. 配置顶点属性 =====
// 属性 0:位置 (location = 0 in shader)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 属性 1:颜色 (location = 1 in shader)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// ===== 6. 解绑(可选,防止误操作) =====
glBindVertexArray(0);

3.4.2 glBufferData 的 usage 参数

参数含义典型场景
GL_STATIC_DRAW数据设置一次,使用多次静态模型、UI 元素
GL_DYNAMIC_DRAW数据会频繁修改粒子系统、动态网格
GL_STREAM_DRAW数据每帧都会更新实时视频纹理

💡 usage 参数只是提示(hint),驱动可以忽略它。但正确的设置有助于驱动优化内存分配策略。


3.5 着色器基础

3.5.1 顶点着色器(Vertex Shader)

// shaders/vertex.glsl
#version 460 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;              // 颜色插值传递
}

3.5.2 片段着色器(Fragment Shader)

// shaders/fragment.glsl
#version 460 core

in vec3 ourColor;    // 从顶点着色器插值得到
out vec4 FragColor;  // 最终输出颜色

void main() {
    FragColor = vec4(ourColor, 1.0);
}

3.5.3 着色器中的数据流

顶点着色器输入 (per vertex)
  aPos (location=0) ← VBO 属性 0
  aColor (location=1) ← VBO 属性 1
        │
        │  光栅化阶段:自动插值
        ▼
片段着色器输入 (per fragment)
  ourColor ← 三个顶点的颜色插值结果
        │
        ▼
输出
  FragColor → 帧缓冲的颜色附件

3.6 绘制调用

3.6.1 渲染循环中的绘制

// 渲染循环
while (!glfwWindowShouldClose(window)) {
    processInput(window);

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

    // 使用着色器程序
    shader.use();

    // 绑定 VAO(自动恢复所有顶点属性配置)
    glBindVertexArray(VAO);

    // 方式 1:使用索引绘制(EBO)
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

    // 方式 2:不使用索引(直接绘制)
    // glDrawArrays(GL_TRIANGLES, 0, 3);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

3.6.2 绘制模式对比

函数用途数据来源
glDrawArrays顺序绘制只用 VBO
glDrawElements索引绘制VBO + EBO
glDrawArraysInstanced实例化绘制VBO(见第 9 章)
glMultiDrawArrays批量绘制多组顶点范围

3.6.3 图元类型

类型说明
GL_TRIANGLES每 3 个顶点组成一个三角形
GL_TRIANGLE_STRIP条带三角形(共享边)
GL_TRIANGLE_FAN扇形三角形(共享中心点)
GL_LINES每 2 个顶点组成一条线段
GL_LINE_STRIP连续线段
GL_POINTS每个顶点绘制为一个点

3.7 完整示例:彩色三角形

将以上所有内容组合成一个完整可运行的程序:

// src/triangle.cpp - 完整的彩色三角形示例
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "shader.h"
#include <iostream>

void framebuffer_size_callback(GLFWwindow* w, int width, int height) {
    glViewport(0, 0, width, height);
}

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

int main() {
    // ---- 初始化 ----
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Chapter 3: Triangle", NULL, NULL);
    if (!window) { glfwTerminate(); return -1; }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

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

    // ---- 着色器 ----
    Shader shader("shaders/vertex.glsl", "shaders/fragment.glsl");

    // ---- 顶点数据 ----
    float vertices[] = {
        // 位置              // 颜色
         0.0f,  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,
    };

    unsigned int indices[] = { 0, 1, 2 };

    // ---- VAO / VBO / EBO ----
    unsigned int VAO, VBO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

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

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

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

    glBindVertexArray(0);

    // ---- 渲染循环 ----
    while (!glfwWindowShouldClose(window)) {
        processInput(window);

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

        shader.use();
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // ---- 清理 ----
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glfwTerminate();
    return 0;
}

预期结果:窗口中央显示一个从红→绿→蓝渐变的三角形。


3.8 进阶:绘制矩形

矩形由 2 个三角形组成,使用 EBO 可以共享顶点:

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,  // 第二个三角形
};

// 绘制调用
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
3 ─── 0          三角形 1: 顶点 0, 1, 3
│ ╲  │           三角形 2: 顶点 1, 2, 3
│  ╲ │           共享 4 个顶点(而非 6 个)
2 ─── 1

3.9 OpenGL 对象管理速查

函数作用生成/删除
glGenVertexArrays生成 VAOglDeleteVertexArrays
glGenBuffers生成 VBO/EBOglDeleteBuffers
glCreateProgram创建着色器程序glDeleteProgram
glCreateShader创建着色器对象glDeleteShader
glGenTextures生成纹理glDeleteTextures
glGenFramebuffers生成帧缓冲glDeleteFramebuffers

⚠️ 资源泄漏:OpenGL 对象在程序退出时不会自动释放。必须手动调用 glDelete*(),或者使用 RAII 包装类。


3.10 错误处理

OpenGL 的错误不会抛出异常,你需要主动检查:

// 检查 OpenGL 错误
GLenum err;
while ((err = glGetError()) != GL_NO_ERROR) {
    std::cerr << "OpenGL Error: 0x" << std::hex << err << std::endl;
}

// 调试回调(OpenGL 4.3+,推荐)
void APIENTRY glDebugCallback(GLenum source, GLenum type, GLuint id,
                               GLenum severity, GLsizei length,
                               const GLchar* message, const void* userParam) {
    if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;  // 忽略通知
    std::cerr << "[GL Debug] " << message << std::endl;
}

// 在初始化时启用
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(glDebugCallback, nullptr);

3.11 注意事项

⚠️ 核心配置文件必须使用 VAO。即使你只有一个着色器程序,OpenGL Core Profile 也要求绑定一个 VAO 才能执行绘制调用。

⚠️ glVertexAttribPointer 必须在绑定 VAO 之后调用。VAO 记录的是调用时的当前状态。

⚠️ 交错布局 vs 分离布局:交错布局(位置+颜色交替)通常更缓存友好。分离布局(所有位置在一起,所有颜色在一起)在某些更新场景下更方便。

⚠️ 不要每帧调用 glGenBuffers。缓冲对象应该在初始化时创建一次,后续只更新数据(glBufferSubDataglMapBuffer)。


3.12 业务场景

场景 1:数据可视化中的散点图

每个散点是一个顶点,颜色映射数据值。使用 GL_POINTS + gl_PointSize 在着色器中控制点大小。

场景 2:游戏中的网格渲染

3D 模型(如 .obj 文件)本质就是一组 VBO(位置、法线、UV)+ EBO(三角形索引)。

场景 3:UI 框架

矩形按钮、文本背景等都是用 2 个三角形 + 纹理贴图实现的。


3.13 扩展阅读

资源说明
Learn OpenGL - Hello Triangle经典三角形教程
OpenGL Vertex Specification顶点规范详解
glVertexAttribPointer 参考官方 API 文档

本章小结

  • OpenGL 是状态机,glBind*() 切换当前活动对象
  • VBO 存顶点数据,EBO 存索引数据,VAO 记录配置状态
  • 顶点属性通过 glVertexAttribPointer 配置 stride 和 offset
  • 着色器是 GPU 上运行的小程序,顶点着色器必选,片段着色器必选
  • glDrawArrays 按顺序绘制,glDrawElements 按索引绘制
  • 务必启用调试回调(GL_DEBUG_OUTPUT)方便排查错误

上一章第 2 章:开发环境搭建 下一章第 4 章:GLSL 着色语言