【OpenGL】Triangle、VBO、VAO、EBO
GPU有成千上万的小核心,每个核心都可以跑专门的小程序,这种程序称为shader。
GPU有固定的shader流水线,我们也可以通过自定义重写shader,精细的控制流水线的每一部分。
蓝色区域是我们可以重写的shader

VBO:顶点数据仓库
- 功能:
VBO是显存中的二进制数据块,负责高效存储顶点数据(如坐标、颜色、法线等),大幅减少CPU-GPU通信开销。 - 特点:
- 纯数据容器:以连续二进制流存储,例如
[x1,y1,z1,r1,g1,b1,x2,y2,z2,...]。 - 灵活使用模式:支持静态(
GL_STATIC_DRAW)、动态(GL_DYNAMIC_DRAW)等存储策略。
- 纯数据容器:以连续二进制流存储,例如

配置时,行为是状态机,只支持载入各种类型的一种对象。shader运行时,GPU会并发从VAO索引取出数据执行
1. 生成缓冲对象
使用 glGenBuffer 函数创建一个新的 VBO,并返回其 ID。
GLuint vbo;
glGenBuffers(1, &vbo); // 1 表示一次性创建几个,后一个参数提供数组指针,将ID回存入数组
2. 绑定缓冲对象
使用 glBindBuffer 函数将创建的 VBO 绑定为当前的顶点缓冲对象。
OPenGL状态机配置阶段只支持激活一个VBO,所以后续数据传入也必须指定这个缓冲区类型。
glBindBuffer(GL_ARRAY_BUFFER, vbo);
3. 上传数据
使用 glBufferData 函数将顶点数据上传到显卡内存。
float vertices[] = {
// 顶点数据,例如三角形的三个顶点
0.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f
};
//GL_STATIC_DRAW 表示数据不会频繁更改。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
传入到之前绑定好类型和对象的缓冲区
数据传到的缓冲区类型要和绑定的一致
// 绑定 VBO_A 并传输数据
glBindBuffer(GL_ARRAY_BUFFER, vboA);
glBufferData(GL_ARRAY_BUFFER, sizeof(dataA), dataA, GL_STATIC_DRAW); // 数据传至 vboA
// 绑定 VBO_B 并传输数据
glBindBuffer(GL_ARRAY_BUFFER, vboB);
glBufferData(GL_ARRAY_BUFFER, sizeof(dataB), dataB, GL_STATIC_DRAW); // 数据传至 vboB
VAO:数据解析蓝图
- 功能:
VAO定义VBO中数据的解析规则,通过顶点属性指定数据结构、类型和用途,实现数据与着色器的对接。 - 核心配置:
- 属性指针(
glVertexAttribPointer):定义数据起始偏移、类型(如GL_FLOAT)、向量维度(如3D坐标)等。 - 属性启用(
glEnableVertexAttribArray):激活对应属性通道,与着色器layout(location = N)绑定。
- 属性指针(
- 优势:
- 状态封装:绘制时只需绑定VAO,自动恢复所有属性配置。
- 多VBO支持:可关联多个VBO(如坐标VBO、颜色VBO),通过不同属性指针区分。
其同一存储在显存中
1. 生成 VAO
使用 glGenVertexArrays 函数创建一个新的 VAO,并返回其 ID。
GLuint VAO;
glGenVertexArrays(1, &VAO);
2. 绑定 VAO
使用 glBindVertexArray 函数将创建的 VAO 绑定为当前的顶点数组对象。绑定 VAO 后,所有后续的顶点属性配置(如 VBO 绑定、顶点属性指针设置)都会存储在这个 VAO 中。
glBindVertexArray(VAO);
后续
glVertexAttribPointer、glEnableVertexAttribArray、VBO的绑定状态,会自动存入 VAO
3. 生成并绑定VBO
GLfloat vertices[] = {
// 顶点坐标
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
4. 设置顶点属性解析方式

// 设置顶点属性解析方式
glVertexAttribPointer(
0, // 逻辑索引,shader中使用location = n 获取,独立于VBO,不能重复
3, // 每顶点分量数 3(vec3)
GL_FLOAT, //数据类型 GL_FLOAT
GL_FALSE, // 无归一化
3 * sizeof(float), // 步长(字节跨度)到下一个相同顶点的字节长度
(void*)0 // 偏移量,相对于VBO起始位置
);
glEnableVertexAttribArray(0); // 激活0索引的数据传输通道,供shader使用location从VBO读取数据给shader
- 逻辑索引:shader中使用
location = n获取。最多15个- 每个VAO相当于一个shader程序环境,逻辑索引对于shader来说不能重复冲突,所以逻辑索引每个VAO内不能重复。
- 分量数:分量的值精度所占字节是一开始就确定的,再依靠分量数可以获取到顶点的所有分量。
- 步长:顶点数据之间可以有间隔,或者插入其他数据。所以需要用步长来确定下一个相同顶点
glVertexAttribPointer 时,VAO会将当前绑定的VBO的引用和配置记录下,可以记录无限个绑定的。渲染时批量取出
逻辑索引用于在 shader 中取用数据,glVertexAttribPointer 的配置会自动生效,返回解析好的数据。
layout (location = 0) in vec3 aPos;
如果glVertexAttribPointer配置是3向量接受是2向量,会截断的第3向量

完整流程示例
#include <iostream>
#include <cstdlib>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
// shader硬编码可以定义一个宏 #define GET_STR(x) #x 会自动加双引号和换行
#define GET_STR(x) #x
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f, // 右下角
0.0f, 0.5f, 0.0f // 顶部
};
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
)";
const char *fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
}
)";
void processInput(GLFWwindow *window) {
// 检查ESC键状态
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true); // 退出程序
}
}
int main(int, char**) {
// 初始化GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return EXIT_FAILURE;
}
// 设置OpenGL版本和配置文件
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
if (window == NULL) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate(); // 退出
return -1;
}
// 设置当前上下文
glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文
// 初始化GLEW
glewExperimental = GL_TRUE; // 需要在glewInit()之前设置
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
glfwTerminate();
return EXIT_FAILURE;
}
// 设置视口,左下角起始坐标点,可以绘制的宽高
glViewport(0, 0, 800, 600);
// VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// set vertex attribute pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// vertex shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
glCompileShader(vertexShader);
// fragment shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// shader program
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 主渲染循环
// 当前输入作用于当前帧
while (!glfwWindowShouldClose(window)) {
glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
processInput(window); // 判断上下文中的事件
// 设置背景色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 交换缓冲区
glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
}
glfwTerminate();
return 0;
}
面剔除
法向量反过来的不渲染,法向量方向根据点的顺序左手法则确定。
// 开启面剔除
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // 剔除背面
glCullFace(GL_FRONT); // 剔除正面
1.VBO存储 VAO配置点的解析规则 开启逻辑通道
2.shader从逻辑通道读取数据
3. 规定shader绘制的点数和形状
#include <iostream>
#include <cstdlib>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
// shader硬编码可以定义一个宏 #define GET_STR(x) #x 会自动加双引号和换行
#define GET_STR(x) #x
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f, // 右下角
0.0f, 0.5f, 0.0f // 顶部
,0.0f, 0.5f, 0.0f
,-0.5f, -0.5f, 0.0f
,-0.5f, 0.7f, 0.0f
};
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
)";
const char *fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
}
)";
void processInput(GLFWwindow *window) {
// 检查ESC键状态
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true); // 退出程序
}
}
int main(int, char**) {
// 初始化GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return EXIT_FAILURE;
}
// 设置OpenGL版本和配置文件
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
if (window == NULL) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate(); // 退出
return -1;
}
// 设置当前上下文
glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文
// 初始化GLEW
glewExperimental = GL_TRUE; // 需要在glewInit()之前设置
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
glfwTerminate();
return EXIT_FAILURE;
}
// 设置视口,左下角起始坐标点,可以绘制的宽高
glViewport(0, 0, 800, 600);
// 开启面剔除
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // 剔除背面
// VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// set vertex attribute pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// vertex shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
glCompileShader(vertexShader);
// fragment shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// shader program
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 主渲染循环
// 当前输入作用于当前帧
while (!glfwWindowShouldClose(window)) {
glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
processInput(window); // 判断上下文中的事件
// 设置背景色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量
// 交换缓冲区
glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
}
glfwTerminate();
return 0;
}
EBO
索引缓冲对象(Element Buffer Object)
float vertices[] = {
// 第一个三角形
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f, // 右下角
0.0f, 0.5f, 0.0f // 顶部
// 第二个三角形
,0.0f, 0.5f, 0.0f
,-0.5f, -0.5f, 0.0f
,-0.5f, 0.7f, 0.0f
};
显然,有顶点叠加,矩形的4个点变成6个顶点,有50%的额外开销。或者对于相接的三角形也是如此。
那么我们不直接使用点,而是用点的索引来构造三角形,点的数据只用有一份。
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0
0.5f, -0.5f, 0.0f, // 1
0.0f, 0.5f, 0.0f // 2
// ,0.0f, 0.5f, 0.0f
// ,-0.5f, -0.5f, 0.0f
,-0.5f, 0.7f, 0.0f // 3
};
// 0,1,2 2,1,3 这样来构造三角形
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0
0.5f, -0.5f, 0.0f, // 1
0.0f, 0.5f, 0.0f // 2
// ,0.0f, 0.5f, 0.0f
// ,-0.5f, -0.5f, 0.0f
,-0.5f, 0.7f, 0.0f // 3
};
1、创建EBO
// EBO
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
2、传索引数据
unsigned indices[] = { // 注意索引从0开始
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
}
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
3、状态机绑定EBO
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
// glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量, 直接通过VBO顶点绘制
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 通过EBO索引绘制
状态机
OPenGL是状态机
配置时,同一时间只能有一个VAO和VBO进入状态机,然后使用glVertexAttribPointer关联VBO到VAO的栏位,保存VBO解析配置。


EBO的配置与VBO类似,但是是独占式的,配置时只能绑定到VAO唯一的EBO索引,渲染时也只能载入唯一的EBO来索引作用于多个VBO。

VAO 仅保存最后一次绑定的 EBO 引用,而非多个 EBO 的集合
EBO单独对于各个VBO索引,而不是将所有VBO作为集合索引
取出VBO数据时只看所有配置过的索引,不看当前绑定的什么VBO。而EBO需要看当前绑定的EBO。
#include <iostream>
#include <cstdlib>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0
0.5f, -0.5f, 0.0f, // 1
0.0f, 0.5f, 0.0f, // 2
-0.5f, 0.7f, 0.0f // 3
};
unsigned indices[] = { // 注意索引从0开始
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
const char *vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
)";
const char *fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
}
)";
void processInput(GLFWwindow *window) {
// 检查ESC键状态
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true); // 退出程序
}
}
int main(int, char**) {
// 初始化GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return EXIT_FAILURE;
}
// 设置OpenGL版本和配置文件
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
if (window == NULL) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate(); // 退出
return -1;
}
// 设置当前上下文
glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文
// 初始化GLEW
glewExperimental = GL_TRUE; // 需要在glewInit()之前设置
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
glfwTerminate();
return EXIT_FAILURE;
}
// 设置视口,左下角起始坐标点,可以绘制的宽高
glViewport(0, 0, 800, 600);
// 开启面剔除
// glEnable(GL_CULL_FACE);
// glCullFace(GL_BACK); // 剔除背面
// VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// set VBO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// EBO
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// vertex shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
glCompileShader(vertexShader);
// fragment shader
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// shader program
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 主渲染循环
// 当前输入作用于当前帧
while (!glfwWindowShouldClose(window)) {
glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
processInput(window); // 判断上下文中的事件
// 设置背景色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
// glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量, 直接通过VBO顶点绘制
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 通过EBO索引绘制
// 交换缓冲区
glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
}
glfwTerminate();
return 0;
}

浙公网安备 33010602011771号