QOpenGLWidget的使用:绘制三角形

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、VBO
(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为例

自定义一个类继承QOpenGLWidget和QOpenGLFunctions,重载initializeGL()、paintGL()和resizeGL()三个函数,并定义所需要的OpenGL资源。大概框架如下:

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>

class MyOpenglWgt : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    explicit MyOpenglWgt(QWidget* parent = nullptr);
    ~MyOpenglWgt() override;
    
protected:
    void initializeGL() override;
    void paintGL() override;
    void resizeGL(int w, int h) override;

private:
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer vbo;
    QOpenGLVertexArrayObject vao;
};

(1)初始化时
这个方法会在第一次显示 QOpenGLWidget 时被调用,通常用于初始化 OpenGL 环境,如设置 OpenGL 状态、加载纹理、初始化着色器等。

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. 创建 VAO
    vao.create();
    vao.bind();

    // 3. 创建 VBO
    vbo.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() 的必要性 (重要)

在修改 OpenGL 顶点数据时,通常需要调用makeCurrent()和doneCurrent(),因为QOpenGLWidget内部已经在initializeGL() 、 paintGL() 、resizeGL() 之前调用了makeCurrent(),所以在这三个函数内部修改顶点数据时不需要手动调用。
但是,如果你想在其他函数修改OpenGL资源(如 VBO),就需要手动调用makeCurrent()和doneCurrent(),以确保OpenGL上下文是可用的。

附代码

实现一个简单的opengl图形,点击按钮切换三角形和正方形。如果实现更为复杂的图形, 可以把 Shape 定义成一个类。
头文件

#ifndef MYOPENGLWGT_H
#define MYOPENGLWGT_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>

enum ShapeType {
    Triangle,
    Square
};

class MyOpenglWgt : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT

public:
    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 <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. 创建 VAO
    vao.create();
    vao.bind();

    // 3. 创建 VBO
    vbo.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)
{
    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;
}

在这里插入图片描述
在这里插入图片描述

总结步骤

1️⃣ 创建QOpenGLWidget的子类,重写initializeGL、paintGL、resizeGL。

2️⃣ 在initializeGL中初始化OpenGL函数
(1)创建着色器程序并编译、链接。
(2) 定义顶点数据和索引数据。
(3) 创建并绑定VAO、VBO、EBO,上传数据。
(4) 设置顶点属性指针。

3️⃣ 在paintGL中清除缓冲区,使用着色器,绑定VAO,绘制元素。

4️⃣ 在析构函数中释放资源。

拓展

glDrawArrays函数

函数原型
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
参数类型说明
modeGLenum指定绘制的图元类型(点、线、三角形等)
firstGLint指定从 VBO 中的哪个顶点索引开始读取
countGLsizei指定要绘制的顶点个数
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) 来减少顶点数据的重复存储。

VBO的数据组织方式 + 配置顶点属性指针

顶点数据通常包含位置、颜色、纹理坐标、法线等属性。
这些属性可以以不同的方式排列,比如交错排列(interleaved)或者分块排列(blocked)。
只有了解VBO的数据组织方式,才能清楚如何配置顶点属性指针,以及采用不同的数据排列方法会如何影响这些参数的设置。

glVertexAttribPointer();

void QOpenGLFunctions::glVertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* ptr)

1️⃣ index :指定要配置的顶点属性的编号。
2️⃣ size :指定每个顶点属性的分量数(1、2、3 或 4,就像向量的维度一样)。
3️⃣ type :指定每个分量的数据类型,常用GL_SHORT、GL_INT、GL_FLOAT 或 GL_DOUBLE等。
4️⃣ normalized :指定是否将数据归一化到 [0,1] 或 [-1,1] 范围内。
5️⃣ stride :(步长)指定连续两个顶点属性间的字节数。如果为0,则表示顶点属性是紧密排列的。
6️⃣ ptr:指向缓冲对象中第一个顶点属性的第一个分量的地址。(offset的作用)

在使用 glVertexAttribPointer 时,您需要指定当前属性的编号,并将其与一个 VBO 关联起来。如果需要配置多个顶点属性,可以使用多个 glVertexAttribPointer 函数,并将每个属性的编号分别传递给它们。# glEnableVertexAttribArray();

void QOpenGLFunctions::glEnableVertexAttribArray(GLuint index)

默认情况下出于性能考虑,所有顶点着色器的属性变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU。必须由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,实现CPU数据上传至GPU。但是,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能:允许顶点着色器读取GPU数据。

VBO数据可能的排列方法

(1)交错排列:每个顶点的所有属性依次存放,例如位置、颜色、法线依次排列,然后下一个顶点同样排列。这种方式可以利用局部性原理,提高缓存效率。

// 顶点数据
float vertices[] = {
    // 位置      	   | 颜色       	 | 纹理坐标
     0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,  // 顶点1
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,  // 顶点2
     0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 1.0f   // 顶点3
};
// 位置属性(索引0)
glVertexAttribPointer(
    0,                  // 属性索引(layout location=0)
    3,                  // 分量数量(x,y,z)
    GL_FLOAT,           // 数据类型
    GL_FALSE,           // 是否归一化
    8 * sizeof(float),  // 步长(每个顶点占8个float)
    (void*)0            // 偏移量(位置从0开始)
);
glEnableVertexAttribArray(0);// 启用第一个顶点的数据

// 颜色属性(索引1)
glVertexAttribPointer(
    1,
    3,
    GL_FLOAT,
    GL_FALSE,
    8 * sizeof(float),
    (void*)(3 * sizeof(float))  // 颜色在顶点内偏移3个float的位置
);
glEnableVertexAttribArray(1);

// 纹理坐标属性(索引2)
glVertexAttribPointer(
    2,
    2,
    GL_FLOAT,
    GL_FALSE,
    8 * sizeof(float),
    (void*)(6 * sizeof(float))  // 纹理坐标偏移6个float的位置
);
glEnableVertexAttribArray(2);

(2)分块排列:所有顶点的同一属性连续存放,比如所有位置数据放在一起,然后是颜色数据,再是法线数据。这种方式可能需要更大的步长,但便于批量更新特定属性。

// 位置数据块
float positions[] = {
    0.5f, -0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.0f, 0.5f, 0.0f
};

// 颜色数据块
float colors[] = {
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f
};

// 纹理坐标数据块
float texCoords[] = {
    1.0f, 0.0f,
    0.0f, 0.0f,
    0.5f, 1.0f
};
// 创建3个VBO
GLuint vbos[3];
glGenBuffers(3, vbos);

// 绑定位置数据到VBO0
glBindBuffer(GL_ARRAY_BUFFER, vbos[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);// 偏移量为0
glEnableVertexAttribArray(0);

// 绑定颜色数据到VBO1
glBindBuffer(GL_ARRAY_BUFFER, vbos[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);

// 绑定纹理坐标到VBO2
glBindBuffer(GL_ARRAY_BUFFER, vbos[2]);
glBufferData(GL_ARRAY_BUFFER, sizeof(texCoords), texCoords, GL_STATIC_DRAW);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glEnableVertexAttribArray(2);

👉 交错排列可能更适合GPU一次性读取所有属性,减少缓存未命中,而分块排列可能在某些情况下更容易批量处理数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值