第 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 三者关系
| 对象 | 全称 | 存储位置 | 作用 |
|---|---|---|---|
| VBO | Vertex Buffer Object | GPU 显存 | 存储顶点数据(位置、颜色、UV 等) |
| EBO | Element Buffer Object | GPU 显存 | 存储索引数据,避免重复顶点 |
| VAO | Vertex Array Object | GPU 显存 | 记录 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 | 生成 VAO | glDeleteVertexArrays |
glGenBuffers | 生成 VBO/EBO | glDeleteBuffers |
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。缓冲对象应该在初始化时创建一次,后续只更新数据(glBufferSubData或glMapBuffer)。
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 着色语言