欢迎来到 Qt 与现代 OpenGL (Core Profile) 的世界!本教程将带领您从零开始搭建一个 Qt 项目,并逐步实现从简单的彩色三角形到更复杂的彩色四边形的绘制。

我们将重点关注 VAOVBOEBO 以及 着色器 的核心概念。

第一步:项目工程建立与配置

我们使用 Qt Creator 创建一个基于 Qt Widgets Application 的项目。

1. CMake 配置 (CMakeLists.txt)

要使用 OpenGL 绘制,我们需要在 CMakeLists.txt 中添加必要的模块。请确保您的配置文件中包含 OpenGLWidgets

# 设置 CMake 的最低要求版本
cmake_minimum_required(VERSION 3.16)

# 定义项目名称和版本
project(opengl3D VERSION 0.1 LANGUAGES CXX)

# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 启用 Qt 自动工具(MOC, UIC, RCC)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

# 查找所需的 Qt 6 模块:Widgets, OpenGL, OpenGLWidgets
find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets)

# 定义可执行文件及其源文件
qt_add_executable(opengl3D
    main.cpp
    mainwindow.cpp
    mainwindow.h
    mainwindow.ui
    openglwidget.cpp
    openglwidget.h
)

# --- 关键修正:添加当前源目录到头文件搜索路径 (用于解决 ui_mainwindow.h 错误) ---
target_include_directories(opengl3D PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

# 链接所需的 Qt 库
target_link_libraries(opengl3D PRIVATE
    Qt::Widgets
    Qt::OpenGL
    Qt::OpenGLWidgets
)

2. 创建 OpenGL 绘制组件

新建一个 C++ 类,命名为 OpenGLWidget,它需要继承自 QOpenGLWidget 并实现 QOpenGLFunctions_3_3_Core 来使用现代 OpenGL 的核心功能。

openglwidget.h

#ifndef OPENGLWIDGET_H
#define OPENGLWIDGET_H

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

class OpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
    Q_OBJECT

public:
    explicit OpenGLWidget(QWidget *parent = nullptr);
    ~OpenGLWidget();

protected:
    void initializeGL() override;
    void resizeGL(int w, int h) override;
    void paintGL() override;

private:
    QOpenGLShaderProgram *program = nullptr;
    QOpenGLBuffer vbo;
    QOpenGLVertexArrayObject vao;
    unsigned int ebo = 0; // EBO 仅用于四边形示例
};

#endif // OPENGLWIDGET_H

【重要提示:更新工程文件】

在 Qt Creator 中手动创建 openglwidget.cppopenglwidget.h 文件,并确保它们已在 CMakeLists.txt 中被引用后,您需要强制 Qt Creator 重新运行 CMake。

如果新文件未显示在项目树中,或编译时出现 No such file or directory 错误,请执行以下步骤:

  1. 菜单栏选择 构建 (Build) -> 清理项目 (Clean Project)

  2. 菜单栏选择 构建 (Build) -> 运行 CMake (Run CMake)(或者直接重新构建项目)。

此操作将强制 CMake 重新扫描文件并生成必要的构建文件,从而将新类添加到您的工程中,并解决头文件路径问题。

第二步:绘制彩色三角形(入门示例)

彩色三角形是 OpenGL 的 Hello World。我们使用交错数组存储位置和颜色数据。

1. 顶点数据与着色器

我们将 3 个位置(XYZ)和 3 个颜色(RGB)交错存储,总共 6 个浮点数/顶点。

// 顶点数据 (Triangle) - 3个顶点,包含位置和颜色
// 采用 glDrawArrays 方式时,不需要索引数组 (EBO)
float vertices[] = {
    // Position (Location 0)     // Color (Location 1)
    0.0f,  0.5f, 0.0f,           1.0f, 0.0f, 0.0f,  // 顶点 0: 顶部, 红色
    -0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,  // 顶点 1: 左下, 绿色
     0.5f, -0.5f, 0.0f,          0.0f, 0.0f, 1.0f   // 顶点 2: 右下, 蓝色
};

// 顶点着色器
const char *vertexShaderSource =
    "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "layout (location = 1) in vec3 aColor;\n"
    "out vec3 ourColor;\n"
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPos, 1.0);\n"
    "    ourColor = aColor;\n"
    "}\0";

// 片段着色器
const char *fragmentShaderSource =
    "#version 330 core\n"
    "in vec3 ourColor;\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "    FragColor = vec4(ourColor, 1.0f);\n"
    "}\n\0";

2. 初始化核心逻辑 (initializeGL)

这里只创建和配置 VAOVBO

  • 步长 (Stride): 1 个顶点数据占用的总字节数,即 6 * sizeof(float)

  • 颜色偏移量 (Offset): 颜色数据在每个顶点数据块中开始的位置,即跳过 3 个位置 float,为 3 * sizeof(float)

<!-- end list -->

// 核心片段:initializeGL()
void OpenGLWidget::initializeGL()
{
    qDebug() << "Initialization started.";
    initializeOpenGLFunctions();
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

    // ... (着色器编译和链接代码) ...
    
    // 1. 设置 VAO
    vao.create();
    QOpenGLVertexArrayObject::Binder vaoBinder(&vao);

    // 2. 设置 VBO (仅 VBO,不使用 EBO)
    vbo.create();
    vbo.bind();
    vbo.allocate(vertices, sizeof(vertices)); 
    
    // 3. 设置顶点属性
    // 步长 (Stride): 1个顶点数据占用的总字节数: 6 * sizeof(float)
    GLsizei stride = 6 * sizeof(float); 

    program->bind();

    // 位置属性 (location = 0)
    program->enableAttributeArray(0);
    program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);

    // 颜色属性 (location = 1)
    program->enableAttributeArray(1);
    program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);

    // 释放资源
    program->release();
    vbo.release();
}

3. 绘制 (paintGL)

使用 glDrawArrays 直接从 VBO 绘制。从第 0 个顶点开始,绘制 3 个顶点。

// 核心片段:paintGL()
void OpenGLWidget::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT);
    program->bind();
    QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
    
    // 使用 glDrawArrays 绘制:从顶点 0 开始,绘制 3 个顶点 (1个三角形)
    glDrawArrays(GL_TRIANGLES, 0, 3); 
    
    program->release();
}

完全代码openglwidget.cpp:

    #include "openglwidget.h"
    #include <QDebug>
    #include <QOpenGLShaderProgram>
    #include <QOpenGLVertexArrayObject>
    
    // ==========================================================
    // 1. 顶点数据 (Triangle) - 3个顶点,包含位置和颜色
    // ==========================================================
    float vertices[] = {
        // Position (Location 0)     // Color (Location 1)
        0.0f,  0.5f, 0.0f,           1.0f, 0.0f, 0.0f,  // Vertex 0: Top, Red
        -0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,  // Vertex 1: Bottom-Left, Green
         0.5f, -0.5f, 0.0f,          0.0f, 0.0f, 1.0f   // Vertex 2: Bottom-Right, Blue
    };
    
    // 注意:使用 glDrawArrays 方式时,不再需要索引数组 (EBO)
    
    // === 2. 顶点着色器 ===
    const char *vertexShaderSource =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "layout (location = 1) in vec3 aColor;\n"
        "out vec3 ourColor;\n"
        "void main()\n"
        "{\n"
        "    gl_Position = vec4(aPos, 1.0);\n"
        "    ourColor = aColor;\n"
        "}\0";
    
    // === 3. 片段着色器 ===
    const char *fragmentShaderSource =
        "#version 330 core\n"
        "in vec3 ourColor;\n"
        "out vec4 FragColor;\n"
        "void main()\n"
        "{\n"
        "    FragColor = vec4(ourColor, 1.0f);\n"
        "}\n\0";
    
    // ==========================================================
    // 4. 核心类实现
    // ==========================================================
    OpenGLWidget::OpenGLWidget(QWidget *parent) : QOpenGLWidget(parent) {}
    
    OpenGLWidget::~OpenGLWidget() {
        makeCurrent();
        vao.destroy();
        vbo.destroy();
        // 析构函数中移除了 EBO 的清理
        delete program;
        doneCurrent();
    }
    
    void OpenGLWidget::initializeGL()
    {
        qDebug() << "Triangle initialization started.";
        initializeOpenGLFunctions();
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        program = new QOpenGLShaderProgram(this);
    
        if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) ||
            !program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) ||
            !program->link()) { 
            qDebug() << "Shader linking failed:" << program->log();
            return; 
        }
        qDebug() << "Shaders linked successfully.";
    
        // 1. 设置 VAO
        vao.create();
        QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
    
        // 2. 设置 VBO (仅 VBO,不使用 EBO)
        vbo.create();
        vbo.bind();
        vbo.allocate(vertices, sizeof(vertices)); 
    
        // 3. 移除了 EBO 的设置
    
        // 4. 设置顶点属性
        GLsizei stride = 6 * sizeof(float); // 步长: 3 Pos + 3 Color
        program->bind();
    
        // 位置属性 (location 0)
        program->enableAttributeArray(0);
        program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);
    
        // 颜色属性 (location 1)
        program->enableAttributeArray(1);
        program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);
    
        program->release();
        vbo.release();
        qDebug() << "Triangle initialization finished.";
    }
    
    void OpenGLWidget::paintGL()
    {
        qDebug() << "Drawing triangle...";
        glClear(GL_COLOR_BUFFER_BIT);
    
        program->bind();
        QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
    
        // 使用 glDrawArrays 绘制:从顶点 0 开始,绘制 3 个顶点 (1个三角形)
        glDrawArrays(GL_TRIANGLES, 0, 3); 
        
        program->release();
        qDebug() << "Triangle drawn successfully.";
    }
    
    void OpenGLWidget::resizeGL(int w, int h)
    {
        glViewport(0, 0, w, h);
    }

    第三步:进阶:绘制彩色四边形 (使用 EBO)

    四边形本质上是两个三角形。为了避免重复存储顶点数据(例如,右上角和左下角顶点在两个三角形中都用到),我们引入 索引 (Indices)EBO (Element Buffer Object)。这是 EBO 的典型使用场景。

    1. 修改顶点数据和索引

    在四边形中,我们只有 4 个顶点,但有 6 个索引来定义两个三角形。

    // 顶点数据 (Quad) - 4个顶点,包含位置 (XYZ) 和颜色 (RGB)
    float vertices[] = {
        // Position (Location 0)     // Color (Location 1)
        -0.5f,  0.5f, 0.0f,          1.0f, 0.0f, 0.0f,  // 顶点 0: 左上
        -0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,  // 顶点 1: 左下
         0.5f, -0.5f, 0.0f,          0.0f, 0.0f, 1.0f,  // 顶点 2: 右下
         0.5f,  0.5f, 0.0f,          1.0f, 1.0f, 0.0f   // 顶点 3: 右上
    };
    
    // 索引数据 (6个索引绘制2个三角形)
    unsigned int indices[] = {
        0, 1, 2,  // 第一个三角形: 0 (左上), 1 (左下), 2 (右下)
        2, 3, 0   // 第二个三角形: 2 (右下), 3 (右上), 0 (左上)
    };

    2. 核心 C++ 代码:彩色四边形

    此时,initializeGL()paintGL() 需要引入 EBO 的设置和 glDrawElements 调用(完整的四边形代码请参考 openglwidget.cpp 文件)。

    #include "openglwidget.h"
    #include <QDebug>
    #include <QOpenGLShaderProgram>
    #include <QOpenGLVertexArrayObject>
    
    // ==========================================================
    // 1. 顶点数据 (Quad) - 4个顶点,包含位置 (XYZ) 和颜色 (RGB)
    // ==========================================================
    float vertices[] = {
        // Position (Location 0)     // Color (Location 1)
        -0.5f,  0.5f, 0.0f,          1.0f, 0.0f, 0.0f,  // 顶点 0: 左上,红色
        -0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,  // 顶点 1: 左下,绿色
        0.5f, -0.5f, 0.0f,          0.0f, 0.0f, 1.0f,  // 顶点 2: 右下,蓝色
        0.5f,  0.5f, 0.0f,          1.0f, 1.0f, 0.0f   // 顶点 3: 右上,黄色
    };
    
    // ==========================================================
    // 2. 索引数据 (EBO) - 6个索引,通过两个三角形构成四边形
    // ==========================================================
    unsigned int indices[] = {
        0, 1, 2,  // Triangle 1
        2, 3, 0   // Triangle 2
    };
    
    // === 3. 顶点着色器 (传递位置和颜色) ===
    const char *vertexShaderSource =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "layout (location = 1) in vec3 aColor;\n"
        "out vec3 ourColor;\n"
        "void main()\n"
        "{\n"
        "    gl_Position = vec4(aPos, 1.0);\n"
        "    ourColor = aColor;\n"
        "}\0";
    
    // === 4. 片段着色器 (接收并使用内插后的颜色) ===
    const char *fragmentShaderSource =
        "#version 330 core\n"
        "in vec3 ourColor;\n"
        "out vec4 FragColor;\n"
        "void main()\n"
        "{\n"
        "    FragColor = vec4(ourColor, 1.0f);\n"
        "}\n\0";
    
    // ==========================================================
    // 5. 构造函数和析构函数
    // ==========================================================
    OpenGLWidget::OpenGLWidget(QWidget *parent)
        : QOpenGLWidget(parent)
    {
    }
    
    OpenGLWidget::~OpenGLWidget()
    {
        makeCurrent();
        vao.destroy();
        vbo.destroy();
        // 使用原生 OpenGL API 释放 EBO
        if (ebo != 0) {
            glDeleteBuffers(1, &ebo);
        }
        delete program;
        doneCurrent();
    }
    
    // ==========================================================
    // 6. 初始化函数
    // ==========================================================
    void OpenGLWidget::initializeGL()
    {
        qDebug() << "Initialization started.";
        initializeOpenGLFunctions();
    
        // 设置背景色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    
        program = new QOpenGLShaderProgram(this);
    
        // 编译和链接着色器
        if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) ||
            !program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) ||
            !program->link())
        {
            qDebug() << "Shader error:" << program->log();
            return;
        }
    
        qDebug() << "Shaders linked successfully.";
    
        // 1. 设置 VAO
        vao.create();
        QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
    
        // 2. 设置 VBO (顶点数据)
        vbo.create();
        vbo.bind();
        vbo.allocate(vertices, sizeof(vertices));
    
        // 3. 设置 EBO (索引数据)
        glGenBuffers(1, &ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    
        // 4. 设置顶点属性
        GLsizei stride = 6 * sizeof(float); // 步长: 3个位置float + 3个颜色float
    
        program->bind();
    
        // 位置属性 (location = 0)
        program->enableAttributeArray(0);
        program->setAttributeBuffer(0, GL_FLOAT, 0, 3, stride);
    
        // 颜色属性 (location = 1)
        program->enableAttributeArray(1);
        // 偏移量: 跳过前面的 3 * sizeof(float) 位置数据
        program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, stride);
    
        // 释放资源
        program->release();
        vbo.release();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 释放 EBO 绑定
        qDebug() << "Initialization finished.";
    }
    
    // ==========================================================
    // 7. 绘制函数 (PaintGL)
    // ==========================================================
    void OpenGLWidget::paintGL()
    {
        // 清除背景
        glClear(GL_COLOR_BUFFER_BIT);
    
        program->bind();
        QOpenGLVertexArrayObject::Binder vaoBinder(&vao);
    
        // 关键修复: 再次显式绑定 EBO
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    
        // 绘制 6 个索引 (2个三角形 = 1个四边形)
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑 EBO
    
        program->release();
    }
    
    void OpenGLWidget::resizeGL(int w, int h)
    {
        glViewport(0, 0, w, h);
    }
    

    Logo

    开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

    更多推荐