如果你从 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_Core 和 QOpenGLFunctions 不能同时继承——它们内部会有函数名冲突。
实际建议:选择 你能接受的最低 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.cpp,cmake -B build && cmake --build build,一个深蓝黑色空白窗口就出来了。OpenGL 版本信息会在终端打印。
系列文章:体素引擎从零构建 | 作者:Logic