Qt Modern OpenGL 入门:从零开始绘制彩色图形

欢迎来到 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 -->

cpp 复制代码
// 核心片段: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 个顶点。

cpp 复制代码
// 核心片段: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 个索引来定义两个三角形。

cpp 复制代码
// 顶点数据 (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 文件)。

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);
}
相关推荐
会飞的胖达喵2 小时前
Qt CMake 项目构建配置详解
开发语言·qt
ceclar1232 小时前
C++范围操作(2)
开发语言·c++
一个尚在学习的计算机小白2 小时前
java集合
java·开发语言
IUGEI2 小时前
synchronized的工作机制是怎样的?深入解析synchronized底层原理
java·开发语言·后端·c#
z***I3942 小时前
Java桌面应用案例
java·开发语言
来来走走2 小时前
Android开发(Kotlin) LiveData的基本了解
android·开发语言·kotlin
明洞日记2 小时前
【数据结构手册002】动态数组vector - 连续内存的艺术与科学
开发语言·数据结构·c++
福尔摩斯张2 小时前
《C 语言指针从入门到精通:全面笔记 + 实战习题深度解析》(超详细)
linux·运维·服务器·c语言·开发语言·c++·算法
6***37943 小时前
Java安全
java·开发语言·安全