如果你从 Qt5 迁移到 Qt6,会发现 OpenGL 不再是默认开箱即用的——它被拆成了独立的 Qt6::OpenGLWidgets 模块。这篇文章不涉及具体的渲染技巧,只把 Qt6 + OpenGL 的基础设施讲清楚。后续体素引擎的每一章都建立在这个基座上。

1. 模块拆分

Qt5 时代,你只需要 find_package(Qt5 REQUIRED COMPONENTS Widgets),然后 QOpenGLWidget 就在里面了。Qt6 不一样:

功能 Qt5 模块 Qt6 模块
QOpenGLWidget Qt5::Widgets(内置) Qt6::OpenGLWidgets(独立)
QOpenGLFunctions Qt5::Gui Qt6::OpenGL
QOpenGLShaderProgram Qt5::Gui Qt6::OpenGL
QOpenGLBuffer Qt5::Gui Qt6::OpenGL

CMakeLists.txt 需要至少这三个模块:

find_package(Qt6 REQUIRED COMPONENTS
    Core
    Gui
    Widgets
    OpenGLWidgets    # QOpenGLWidget
    OpenGL           # QOpenGLFunctions 等
)

核心记忆点:Widgets 不再包含 OpenGL,OpenGLWidgets 和 OpenGL 是两个独立的 find 目标。

2. QOpenGLWidget 的初始化顺序

Qt6 中的 QOpenGLWidget 初始化流程和 Qt5 基本一致,但有一个容易掉进去的坑。

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
    Q_OBJECT
public:
    explicit GLWidget(QWidget *parent = nullptr)
        : QOpenGLWidget(parent) {}

protected:
    void initializeGL() override {
        // 第一步:必须调用这个,否则所有 gl* 函数都是空指针
        if (!initializeOpenGLFunctions()) {
            qFatal("OpenGL functions not available");
        }

        // 第二步:检查 OpenGL 版本
        qDebug() << "OpenGL" << (const char*)glGetString(GL_VERSION);
        qDebug() << "GLSL"   << (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);

        // 第三步:设置全局状态
        glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);      // 背面剔除
    }

    void resizeGL(int w, int h) override {
        glViewport(0, 0, w, h);
    }

    void paintGL() override {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // 在这里画东西
    }
};

坑: QOpenGLFunctions 必须用 protected 继承(或者用组合也行,但要手动调 initializeOpenGLFunctions)。如果用 public 继承也没问题,protected 只是表示「这些 GL 函数不应该被外部直接调用」。

3. 主循环:requestUpdate() vs QTimer

QOpenGLWidget 提供了两种驱动渲染的方式:

方式 A:requestUpdate()(推荐)

void GLWidget::paintGL() {
    // 渲染当前帧
    // ...

    // 请求下一帧——Qt 会在事件循环空闲时调用 paintGL()
    requestUpdate();
}

requestUpdate() 不是同步调用 paintGL()。它向 Qt 的事件队列投递一个更新请求,Qt 在下一个事件循环周期处理它。多帧请求会被合并为一个。

优点:

  • 不会产生多余帧(vsync 对齐)
  • 窗口最小化时自动停止渲染
  • 多个 requestUpdate() 调用会被合并

缺点:

  • FPS 取决于显示器的刷新率(被 vsync 限住)
  • 不适合需要固定更新频率的逻辑(如物理模拟)

方式 B:QTimer

auto *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [this] {
    update(); // 直接触发 paintGL
});
timer->start(16); // ~60 FPS

或者用 QElapsedTimer 做帧率统计:

QElapsedTimer fpsTimer;
int frameCount = 0;

void GLWidget::paintGL() {
    // 渲染
    // ...

    frameCount++;
    if (fpsTimer.elapsed() >= 1000) {
        qDebug() << "FPS:" << frameCount;
        frameCount = 0;
        fpsTimer.restart();
    }
}

我的选择: 体素引擎里用 requestUpdate()。原因很简单——体素世界的更新频率不需要比显示器更高,而且窗口最小化时自动休眠这个特性太舒服了,不会白白烧 CPU。

4. QOpenGLFunctions 的版本选择

QOpenGLFunctions 默认提供 OpenGL ES 2.0 / Desktop 2.0 级别的函数。如果你需要 OpenGL 3.3+ 的函数(比如 glGenVertexArrays),需要继承带版本号的类:

// OpenGL 3.3 核心模式
class GLWidget : public QOpenGLWidget,
                 protected QOpenGLFunctions_3_3_Core {
    // ...
};

// OpenGL 4.5 核心模式
class GLWidget : public QOpenGLWidget,
                 protected QOpenGLFunctions_4_5_Core {
    // ...
};

但是,QOpenGLFunctions_3_3_CoreQOpenGLFunctions 不能同时继承——它们内部会有函数名冲突。

实际建议:选择 你能接受的最低 OpenGL 版本对应的 Functions 类。体素引擎我选择 QOpenGLFunctions_3_3_Core,因为 3.3 是 VAO 成为强制的第一个版本,也是绝大多数 2010 年后显卡都支持的版本。不需要 4.x 特性。

5. 着色器程序的最小示例

Qt 封装了 QOpenGLShaderProgram,用法比裸 OpenGL 方便很多:

// 在 initializeGL() 中
auto *program = new QOpenGLShaderProgram(this);

// 编译顶点着色器
if (!program->addShaderFromSourceFile(QOpenGLShader::Vertex,
                                       ":/shaders/chunk.vert")) {
    qWarning() << "Vertex shader error:" << program->log();
}

// 编译片段着色器
if (!program->addShaderFromSourceFile(QOpenGLShader::Fragment,
                                       ":/shaders/chunk.frag")) {
    qWarning() << "Fragment shader error:" << program->log();
}

// 链接
if (!program->link()) {
    qWarning() << "Shader link error:" << program->log();
}

// 绑定 attribute 位置(在 link 之前调用 bindAttributeLocation 也可以)
program->bind();
program->setUniformValue("u_projection", projectionMatrix);
program->setUniformValue("u_view", viewMatrix);

// 在 paintGL() 中
program->bind();
// 绑定 VAO、画东西...
program->release();

Qt 的资源系统(.qrc 文件)可以方便地嵌入着色器文件,不用操心文件路径。但在体素引擎课程中我会把着色器放在 shaders/ 目录下用文件路径加载——这样读者可以直接用文本编辑器改着色器,不需要了解 .qrc

6. 多采样抗锯齿(MSAA)

一行代码开启:

// 在 QOpenGLWidget 构造函数中
QSurfaceFormat fmt;
fmt.setSamples(4);           // 4x MSAA
fmt.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(fmt);

必须在创建 QApplication 之前调用 setDefaultFormat,或者在 QOpenGLWidget 构造时传入。体素引擎默认不开启——方块世界的边缘本来就是方的,MSAA 意义不大,反而浪费显存。

7. 跨平台注意事项

平台 特殊处理
macOS Qt6 默认用 Metal 后端,QOpenGLWidget 仍然可用但底层是 Metal 封装的。OpenGL 4.1 是 macOS 支持的最高版本(且已被 Apple 标记为 deprecated)。如果强制用 Core Profile 4.5 会失败。
Windows 最省心。Qt6 + MinGW 或 MSVC 都直接工作。注意如果装了多个 Qt 版本,CMAKE_PREFIX_PATH 必须指向正确的 Qt 目录。
Linux 需要安装 libgl1-mesa-dev 和对应 Qt 的 OpenGL 包。Wayland 下 QOpenGLWidget 用 EGL 而不是 GLX,行为基本一致。

8. 一个可运行的完整示例

把所有内容拼成一个 main.cpp

#include <QApplication>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QDebug>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
    Q_OBJECT
public:
    using QOpenGLWidget::QOpenGLWidget;

protected:
    void initializeGL() override {
        initializeOpenGLFunctions();
        qDebug() << "OpenGL" << (const char*)glGetString(GL_VERSION);
        glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
    }
    void resizeGL(int w, int h) override { glViewport(0, 0, w, h); }
    void paintGL() override {
        glClear(GL_COLOR_BUFFER_BIT);
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    GLWidget w;
    w.resize(800, 600);
    w.show();
    return app.exec();
}

#include "main.moc"

配套 CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(qt6-opengl-demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGLWidgets)

add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE
    Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGLWidgets)

保存为 main.cppcmake -B build && cmake --build build,一个深蓝黑色空白窗口就出来了。OpenGL 版本信息会在终端打印。


系列文章:体素引擎从零构建 | 作者:Logic