OpenGL资源的生命周期
所有 OpenGL 资源(VBO、VAO、Shader)都是 先创建 → 再绑定 → 配置 → 解绑 → 使用时重新绑定 → 释放,大致流程如下:
首先要了解,VAO(顶点数组对象)和 VBO(顶点缓冲对象)是 OpenGL 里用于管理顶点数据的两种不同对象。
简单来说:
VBO(Vertex Buffer Object) : 是一个 GPU 缓冲区,存储顶点数据(如坐标、颜色、法线等)。如果只 bind() 了 VBO,OpenGL 还不知道数据怎么解析。
VAO(Vertex Array Object) : 记录 “绑定了哪些 VBO” 和 “如何解析数据”,让 OpenGL 知道如何读取数据。每次绘制时,只需要 绑定 VAO,就能自动恢复所有顶点属性配置,而不需要每次都重新 glVertexAttribPointer()。
VBO = 存数据,VAO = 记录 VBO 和解析方式
VAO 里存了 VBO 的状态,所以绘制时 只需要绑定 VAO,OpenGL 就知道怎么渲染!
VAO & VBO 的逻辑:
(1)创建 VAO
(2)绑定 VAO(让 VAO 记录后续状态)
(3)绑定 VBO
(4)填充 VBO 数据
(5)配置顶点属性(告诉 OpenGL 如何读取 VBO)
(6)解绑 VAO(防止后续操作影响 VAO)
(7)绘制时,只绑定 VAO,OpenGL 就能自动找到 VBO
初始化时:
1. 创建 VAO、VBO、ShaderProgram
2. 绑定 VAO
3. 绑定 VBO
4. 配置 VBO(传递数据 + 设置顶点属性)
5. 解绑 VAO 和 VBO(防止误操作)绘制时:
6. 绑定 ShaderProgram
7. 绑定 VAO
8. 绘制
9. 解绑 VAO 和 ShaderProgram(可选)销毁时:
10. 释放 VAO、VBO、ShaderProgram
VAO到底是如何记录VBO的呢?
在Qt封装的Opengl中,VAO记录 VBO的方式是通过记录OpenGL的状态。虽然代码中没有显式地将VBO存入VAO,但当VAO处于绑定状态时,它会自动记录所有VBO绑定和顶点属性设置。
代码关键点
(1) vao.bind():绑定 VAO (glBindVertexArray(VAO)):告诉 OpenGL 开始记录所有 VBO 和属性设置。
(2)vbo.bind():绑定 VBO (glBindBuffer(GL_ARRAY_BUFFER, VBO)):告诉 VAO 这个 VBO 以后会用于绘制。
(3)glVertexAttribPointer():告诉 VAO,VBO 中的数据是如何解析的。
(4)vao.release():解绑 VAO (glBindVertexArray(0)):保存所有的 VBO 和属性设置,以后只需绑定 VAO 就能恢复。
下面代码以QOpenGLWidget为例
变量声明
QOpenGLShaderProgram shaderProgram;QOpenGLBuffer vbo;QOpenGLVertexArrayObject vao;
(1)初始化时
void initializeGL() override {initializeOpenGLFunctions(); // 初始化 OpenGL 函数指针// 1. 创建 ShaderProgram:加载顶点着色器代码和片元着色器代码shaderProgram = new QOpenGLShaderProgram(this);shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, // 编译GLSL着色器代码"#version 330 core\n""layout(location = 0) in vec2 aPos;\n""void main() { gl_Position = vec4(aPos, 0.0, 1.0); }");shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment,"#version 330 core\n""out vec4 FragColor;\n""void main() { FragColor = vec4(1.0, 0.5, 0.2, 1.0); }");shaderProgram->link();// 生成可执行的GPU着色器程序 (把已经编译的着色器连接到一个可执行GPU着色器程序,仍然没有激活GPU着色器!)// 2. 创建 VAOvao.create();vao.bind();// 3. 创建 VBOvbo.create();vbo.bind();// 4. 传递顶点数据float vertices[] = { // 默认三角形0.0f, 0.5f,-0.5f, -0.5f,0.5f, -0.5f};vbo.allocate(vertices, sizeof(vertices));// 5. 配置顶点属性glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);glEnableVertexAttribArray(0);// 6. 解绑 VAO 和 VBO(防止误操作影响vao和vbo)vbo.release();vao.release();
}
关键点:
vao.bind() 之后,所有 glBindBuffer() 和 glVertexAttribPointer() 的操作都会 被 VAO 记录!解绑 VAO 后,VAO 就会保存这些状态,以后只要 vao.bind(),VBO 和顶点属性就会自动恢复!
(2)绘制时
void paintGL() override {glClear(GL_COLOR_BUFFER_BIT);shaderProgram->bind(); // 绑定Shader: 最终激活GPU着色器!这个函数把着色器程序加载到GPU并让OpenGL使用它!vao.bind(); // 只需绑定 VAO,VBO 和顶点属性都会自动恢复!glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形vao.release();shaderProgram->release();
}
关键点:
只绑定 VAO,就能恢复 VBO 并正确解析数据!
不需要再手动 glBindBuffer() 和 glVertexAttribPointer()!
(3)释放资源
~OpenGLRenderer() {makeCurrent();vao.destroy();vbo.destroy();delete shaderProgram;doneCurrent();
}
一些注意点:
为什么要使用VAO?
如果 没有 VAO,每次绘制前都要重新绑定 VBO 并配置 glVertexAttribPointer(),影响性能。
QOpenGLShaderProgram类
把着色器加载到 GPU 主要由以下函数完成:
函数 | 作用 | 是否上传到 GPU? |
---|---|---|
addShaderFromSourceCode() / addShaderFromSourceFile() | 编译 GLSL 着色器代码 | ❌ 仅仅编译 |
link() | 连接着色器,生成 GPU 可执行程序 | ❌ 还未启用 |
bind() | 上传到 GPU 并激活着色器 | ✅ 真正让 GPU 使用 |
👉 最终,真正把着色器加载到 GPU 并使用的函数是 bind() 🚀
makeCurrent() 和 doneCurrent() 的必要性 (重要)
在 initializeGL() 和 paintGL() 里 不需要 makeCurrent(),因为 Qt 已经自动调用 了。
在 setShape() 里修改 VBO 时 需要 makeCurrent(),因为 Qt 事件循环可能在非 OpenGL 线程调用它。
附代码
实现一个简单的opengl图形,点击按钮切换三角形和正方形。如果实现更为复杂的图形, 可以把 Shape 定义成一个类。
头文件
#ifndef MYOPENGLWGT_H
#define MYOPENGLWGT_H#include <QWidget>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>enum ShapeType {Triangle,Square
};class MyOpenglWgt : public QOpenGLWidget, protected QOpenGLFunctions
{Q_OBJECTpublic:explicit MyOpenglWgt(QWidget* parent = nullptr);~MyOpenglWgt() override;void setShape(int shape);protected:void initializeGL() override;void paintGL() override;void resizeGL(int w, int h) override;private:QOpenGLShaderProgram shaderProgram;QOpenGLBuffer vbo;QOpenGLVertexArrayObject vao;ShapeType m_shapeType;
};#endif // MYOPENGLWGT_H
源文件
#include "myopenglwgt.h"
#include <QDebug>
#include <QOpenGLShader>MyOpenglWgt::MyOpenglWgt(QWidget* parent): QOpenGLWidget(parent), vbo(QOpenGLBuffer::VertexBuffer),m_shapeType(ShapeType::Triangle)
{
}MyOpenglWgt::~MyOpenglWgt()
{makeCurrent(); // 需要确保 OpenGL 上下文可用vbo.destroy();vao.destroy();shaderProgram.removeAllShaders();doneCurrent();
}// 在 initializeGL() 里,创建 VAO、VBO、Shader,并配置它们
void MyOpenglWgt::initializeGL()
{initializeOpenGLFunctions(); // 初始化 OpenGL 函数指针// 1. 创建ShaderProgram着色器:加载顶点着色器代码和片元着色器代码shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, // 编译GLSL着色器代码"#version 330 core\n""layout(location = 0) in vec2 aPos;\n""void main() { gl_Position = vec4(aPos, 0.0, 1.0); }");shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment,"#version 330 core\n""out vec4 FragColor;\n""void main() { FragColor = vec4(1.0, 0.5, 0.2, 1.0); }");shaderProgram.link(); // 生成可执行的GPU着色器程序 (把已经编译的着色器连接到一个可执行GPU着色器程序,仍然没有激活GPU着色器!)// 2. 创建 VAOvao.create();vao.bind();// 3. 创建 VBOvbo.create();vbo.bind();vbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);// 4. 传递顶点数据float vertices[] = { // 默认三角形0.0f, 0.5f,-0.5f, -0.5f,0.5f, -0.5f};vbo.allocate(vertices, sizeof(vertices)); // 将数据复制到 GPU// 5. 配置顶点属性glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);glEnableVertexAttribArray(0);// 6. 解绑 VAO 和 VBO(防止误操作影响到vao和vbo)vbo.release();vao.release();
}//在paintGL()里,通过绑定 Shader+VAO,并调用glDrawArrays()进行绘制:
void MyOpenglWgt::paintGL()
{glClear(GL_COLOR_BUFFER_BIT);shaderProgram.bind(); // 绑定Shader: 最终激活GPU着色器!这个函数把着色器程序加载到GPU并让OpenGL使用它!vao.bind(); // 只需绑定VAO,VBO 和顶点属性都会自动恢复!if (m_shapeType == ShapeType::Triangle) {glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形}else if(ShapeType::Square){glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // 绘制正方形}vao.release();shaderProgram.release();
}void MyOpenglWgt::resizeGL(int w, int h)
{qDebug() << "resizeGL";glViewport(0, 0, w, h);
}// 如果想要切换成正方形,只需要修改VBO 数据:
void MyOpenglWgt::setShape(int shapeType) {m_shapeType = static_cast<ShapeType>(shapeType);makeCurrent(); // 必须调用,确保 OpenGL 上下文可用vao.bind();vbo.bind();if (shapeType == ShapeType::Triangle) { // 三角形float vertices[] = {0.0f, 0.5f,-0.5f, -0.5f,0.5f, -0.5f};vbo.allocate(vertices, sizeof(vertices));} else if(ShapeType::Square) { // 正方形float vertices[] = {-0.5f, 0.5f,0.5f, 0.5f,0.5f, -0.5f,-0.5f, -0.5f};vbo.allocate(vertices, sizeof(vertices)); // 将数据复制到 GPU}vbo.release();vao.release();doneCurrent(); // 释放 OpenGL 上下文update(); // 触发重绘
}
代码调用:
#include "widget.h"
#include "ui_widget.h"
#include "myopenglwgt.h"
#include <QPushButton>
int shape =0;
Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget),m_pOpenglWgt(new MyOpenglWgt(this))
{ui->setupUi(this);ui->layout->addWidget(m_pOpenglWgt);QPushButton* pBtn = new QPushButton("Triangle", this);ui->horizontalLayout->addWidget(pBtn);connect(pBtn, &QPushButton::released, [=]{shape = shape == 0 ? 1 : 0;pBtn->setText(shape==0 ? "Triangle" : "Square");m_pOpenglWgt->setShape(shape);});
}Widget::~Widget()
{delete ui;
}
拓展
glDrawArrays函数
函数原型
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
参数 | 类型 | 说明 |
---|---|---|
mode | GLenum | 指定绘制的图元类型(点、线、三角形等) |
first | GLint | 指定从 VBO 中的哪个顶点索引开始读取 |
count | GLsizei | 指定要绘制的顶点个数 |
GLenum mode 可选的绘制模式
mode 指定了 绘制方式,OpenGL 提供了多个模式:
GLenum 值 | 说明 | 需要的最少顶点数 |
---|---|---|
GL_POINTS | 绘制独立的点 | 1 |
GL_LINES | 每两个顶点形成一条独立的线段 | 2 |
GL_LINE_STRIP | 连续连接所有顶点形成折线 | 2 |
GL_LINE_LOOP | 闭合折线(最后一个点自动连接到第一个点) | 2 |
GL_TRIANGLES | 每 3 个顶点形成一个独立的三角形 | 3 |
GL_TRIANGLE_STRIP | 连续三角形,每新增 1 个点都会形成 1 个新三角形 | 3 |
GL_TRIANGLE_FAN | 扇形三角形(以第 1 个点为中心) | 3 |
GL_TRIANGLE_STRIP和GL_TRIANGLE_FAN区别
GL_TRIANGLE_STRIP 和 GL_TRIANGLE_FAN 都是减少冗余顶点的三角形绘制模式,但它们的拼接方式不同,适用于不同的图形结构。
(1) GL_TRIANGLES三角形
定义:以每三个顶点绘制一个独立的三角形。 顶点使用:第一个三角形使用顶点v0、v1、v2,第二个使用v3、v4、v5,以此类推。
顶点数量要求:如果顶点的个数n不是3的倍数,那么最后的1个或者2个顶点会被忽略。
适用场景:适用于需要绘制多个独立三角形且三角形之间不共享顶点的场景。
(2) GL_TRIANGLE_STRIP三角形带
定义:绘制一组相连的三角形,三角形的构建依赖于顶点的序号和奇偶性。 如果当前顶点是奇数,组成三角形的顶点排列顺序为[n-1, n-2,n]。 如果当前顶点是偶数,组成三角形的顶点排列顺序为[n-2, n-1, n]。
顶点使用:第一个三角形由顶点v0、v1、v2组成(假设v2为偶数顶点),第二个三角形由顶点v1、v2、v3组成(v3为奇数顶点),以此类推。
顶点数要求:顶点个数n至少要大于3,否则不能绘制任何三角形。
适用场景:适用于需要绘制一系列相连的三角形且希望三角形之间共享顶点的场景(条带状结构:长方形、地形、路面),可以减少顶点传递次数,提高性能。
(3) GL_TRIANGLE_FAN三角形扇
定义:绘制一组相连的三角形,所有三角形共用一个起始顶点。
顶点使用:以v0为起始点,第一个三角形由顶点v0、v1、v2组成,第二个三角形由顶点v0、v2、v3组成,以此类推。
顶点数要求:与GL_TRIANGLES类似,如果顶点数量不是足够形成完整的三角形序列,则最后的顶点可能会被忽略。
适用场景:适用于需要绘制以某个顶点为中心的扇形三角形序列的场景(围绕中心的扇形结构:圆盘、光环、多边形)。
总结
✅glDrawArrays() 适用于没有索引(EBO)的简单图形,绘制方式由 GLenum mode 决定。
✅ GL_TRIANGLE_STRIP 和 GL_TRIANGLE_FAN 可以减少冗余顶点,提高绘制效率。
✅ 对于更复杂的图形,通常使用 glDrawElements() + 索引缓冲对象(EBO) 来减少顶点数据的重复存储。