拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Qt+OpenGL实战:用QOpenGLWidget绘制可旋转彩色立方体

简介用Qt与OpenGL绘制三维四棱锥并实现旋转是面向初学者的入门级实践案例演示了三维图形的基础绘制流程与顶点坐标计算方法帮助理解立体图形由多个面构成的建模思路。包体共16个文件以cpp源文件、h头文件、pro工程文件为主同时包含ui界面文件、exe可执行程序以及编译配置文件压缩包总大小约451KB结构紧凑便于直接打开工程查看。目前已有2418人学习与下载。读者可以从源码中观察四棱锥5个面的绘制顺序学习三维顶点坐标的设定方式并结合旋转效果掌握OpenGL的基本矩阵变换逻辑配套的可执行程序可先行运行观察效果再对照代码逐行理解适合入门阶段动手实验。 做着Qt界面开发久了总有那么一刻想往窗口里塞点立体东西。不是那种2D饼图而是实实在在能转起来的3D图形。我最近用Qt重写了以前的一个OpenGL演示项目在QOpenGLWidget里画了一个彩色立方体支持自动旋转和鼠标拖拽旋转。这个过程踩了不少坑也把OpenGL在现代Qt里的标准用法重新梳理了一遍。这篇博文就把关键步骤和数据细节记录下来给同样想在Qt里接3D画面、又不清楚从哪下手的同学一个完整参考。1. 项目背景与核心需求解析1.1 这个项目到底在解决什么问题传统Qt做界面很顺手但要做3D渲染很多人第一反应是找现成的三维图形库或者干脆换引擎。实际上Qt和OpenGL结合得很紧密通过QOpenGLWidget就能把OpenGL渲染画面嵌进普通窗口。这个项目用了一个“可旋转的彩色立方体”作为载体把整个链路走通OpenGL上下文创建、着色器编译链接、顶点缓冲设置、MVP矩阵变换、鼠标交互和定时器动画。麻雀虽小五脏俱全。如果你能把立方体正确画出来并转起来再去扩展Mesh加载、纹理映射、光照模型就有清晰的地图了。所以这个项目特别适合刚接触OpenGL的Qt开发者也适合想快速验证三维渲染效果、不想折腾原生窗口的人。1.2 为什么选Qt OpenGL而不是其他组合选择QtOpenGL的理由很大程度是“省事”。如果是裸OpenGL开发要自己处理窗口系统、事件循环和跨平台函数指针获取通常还要引入GLFW、GLAD/GLEW这类外部库。而Qt的QOpenGLWidget可以直接当普通控件嵌入界面鼠标事件、定时器、布局管理全部复用Qt现有机制节省了非常多工程代码。另一个关键点是QOpenGLFunctions封装了OpenGL函数的动态获取不用再手动处理glGenBuffers在不同平台上找不到符号的问题。虽然纯OpenGL更底层、更自由但做工具软件时Qt的成熟框架能省去大量重复劳动。这也是我最终在项目里选它而不是GLUT的原因。2. 搭建OpenGL绘制环境2.1 选择Qt版本和OpenGL模块我用的Qt 5.15但Qt 6.x也完全适用核心逻辑没有太大差异。在.pro文件里需要这样配置QT core gui widgets opengl openglwidgetsQt6里openglwidgets是必不可少的否则找不到QOpenGLWidget头文件。另外编译器用MinGW或MSVC都行开发机只要装好显卡驱动OpenGL库就能正常使用。我建议至少使用OpenGL 3.3以上的版本这样能走现代管线后续写着色器也更灵活。2.2 初始化上下文与基础设置主窗口里定义一个继承自QOpenGLWidget的自绘类名字就叫GLWidgetclass GLWidget : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT public: using QOpenGLWidget::QOpenGLWidget; protected: void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; };initializeGL里先调用initializeOpenGLFunctions()然后设置背景色、深度缓存resizeGL里设置视口paintGL里做所有绘制。只要把这三个函数填好OpenGL渲染就跑起来了。有一个容易被忽略的地方必须在创建窗口前设置SurfaceFormat否则OpenGL上下文版本可能不对后面会遇到GLSL语法错误或者干脆黑屏。我通常在main函数最开始写QSurfaceFormat format; format.setVersion(3, 3); format.setProfile(QSurfaceFormat::CoreProfile); format.setDepthBufferSize(24); QSurfaceFormat::setDefaultFormat(format);这段代码要放在QApplication实例化之前确保所有窗口默认使用这个格式。如果不设置Qt会用系统默认的兼容上下文在某些环境下着色器版本会变得很尴尬。2.3 准备顶点数据和缓冲区立方体是最简单的3D图元我用36个索引表示6个面每个面两个三角形。顶点数据包括位置和颜色结构体定义如下struct Vertex { QVector3D position; QVector3D color; };然后准备8个顶点的坐标和颜色再定义索引数组。比如前四个顶点是前面那扇面颜色给成红、绿、蓝、黄后方四个顶点用另外的颜色。这样每面颜色会自然过渡效果比较炫。缓冲区创建用Qt封装类m_vao.create(); m_vbo.create(); m_ebo.create(); m_vao.bind(); m_vbo.bind(); m_vbo.allocate(vertices.data(), vertices.size() * sizeof(Vertex)); m_ebo.bind(); m_ebo.allocate(indices.data(), indices.size() * sizeof(GLuint));记得要把顶点属性指针设置好告诉OpenGL每个顶点数据段怎么解析m_program-enableAttributeArray(0); m_program-setAttributeBuffer(0, GL_FLOAT, offsetof(Vertex, position), 3, sizeof(Vertex)); m_program-enableAttributeArray(1); m_program-setAttributeBuffer(1, GL_FLOAT, offsetof(Vertex, color), 3, sizeof(Vertex));很多新手在这里漏了stride参数导致图形乱掉。我给的数据结构紧凑stride就是sizeof(Vertex)。这步处理完再把VAO解绑后续绘制时重新绑定即可。3. 3D图形的绘制实现3.1 着色器编写与编译OpenGL 3.3核心管线必须用着色器不能再走固定管线了。我建了顶点着色器和片段着色器顶点着色器接收位置和颜色输出颜色给下一阶段。// vertex shader #version 330 core layout(location 0) in vec3 aPos; layout(location 1) in vec3 aColor; out vec3 vColor; uniform mat4 mvp; void main() { gl_Position mvp * vec4(aPos, 1.0); vColor aColor; }片段着色器更简单直接把颜色写出去// fragment shader #version 330 core in vec3 vColor; out vec4 FragColor; void main() { FragColor vec4(vColor, 1.0); }在Qt里用QOpenGLShaderProgram加载和链接着色器非常舒服m_program new QOpenGLShaderProgram(this); m_program-addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSrc); m_program-addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSrc); m_program-link();但这里有个大坑如果着色器里有语法错误link()很可能静默失败画面什么都不显示。一定要检查log()if (!m_program-link()) { qWarning() m_program-log(); return; }我第一次写时漏了一个分号画面直接空白最后就是靠这个日志定位的。所以调试阶段日志检查无论如何都不能省。3.2 MVP矩阵与透视投影要让立方体有立体感必须经过模型矩阵Model、视图矩阵View、投影矩阵Projection三步变换。我用QMatrix4x4逐帧构造QMatrix4x4 projection; projection.perspective(45.0f, float(width()) / float(height()), 0.1f, 100.0f); QMatrix4x4 view; view.lookAt(QVector3D(3.0f, 3.0f, 3.0f), QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0f, 1.0f, 0.0f)); QMatrix4x4 model; model.rotate(m_rotation.x(), 1.0f, 0.0f, 0.0f); model.rotate(m_rotation.y(), 0.0f, 1.0f, 0.0f); QMatrix4x4 mvp projection * view * model; m_program-setUniformValue(mvp, mvp);这里要注意QMatrix4x4默认是列主序存储跟OpenGL的约定一致所以直接传给uniform即可。最常出错的点是矩阵相乘顺序一定要先投影乘以视图再乘以模型。顺序反了图形会飞到完全无法预测的位置。还要注意透视函数里的宽高比必须用浮点数除法如果写成width()/height()因为整数除法会变成整数立方体就会被拉长或压扁。我在代码里刻意强转了float避免这个坑。3.3 颜色、深度测试与背面剔除如果没有深度测试后绘制的三角形会直接覆盖先绘制的三角形立方体看起来会像半透明的一团乱麻。所以initializeGL里必须打开深度缓冲glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS);同时为了减少不必要的片元计算可以开启背面剔除glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glFrontFace(GL_CCW);背面剔除要格外注意顶点绕序。如果顶点按顺时针定义就要把GL_CCW改成GL_CW否则立方体看起来时隐时现像坏掉一样。我建议先不启用剔除把颜色和旋转调通之后再加这样问题比较容易定位。颜色方面我用的是顶点颜色插值所以每个面内部会有渐变。如果你希望每个面是纯色需要为每个面重新定义顶点数据而不是共享顶点。两种方式都能实现纯色版本对顶点的组织要求更高但视觉效果更接近“魔方”。我在项目里先用渐变方案代码量最小调试也直观。4. 旋转动画与交互控制4.1 QTimer驱动自动旋转静态3D图形没意思转起来才有视觉冲击。最简单的自动旋转是用QTimer每16毫秒更新一次角度QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, [this]() { m_rotationY 1.0f; if (m_rotationY 360.0f) m_rotationY - 360.0f; update(); }); timer-start(16);注意这里必须用update()不要用repaint()。update()会合并同一帧内多次重绘请求在事件循环里统一刷新不会把CPU和GPU占用打到爆。实测普通集成显卡上16ms一帧已经能获得非常平滑的旋转体验。如果追求稳定帧率更专业的做法是用QElapsedTimer记录实际时间差再按真实时间增量累加角度而不是固定每次加1。否则在高刷新率屏幕上同一段代码的旋转速度会变得不同。这个细节平时不明显但一旦把窗口从60Hz显示器挪到144Hz显示器立刻就能看出差别。4.2 鼠标拖拽旋转视角只有自动旋转还不够让用户用鼠标拖拽旋转才真正算“可交互”。我在GLWidget里重写了鼠标事件void GLWidget::mousePressEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { m_lastPos event-pos(); } } void GLWidget::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { float dx event-pos().x() - m_lastPos.x(); float dy event-pos().y() - m_lastPos.y(); m_rotationY dx * 0.3f; m_rotationX dy * 0.3f; m_lastPos event-pos(); update(); } }这里用了X和Y两个旋转变量鼠标左右拖动绕Y轴旋转上下拖动绕X轴旋转。0.3是灵敏度系数可以根据屏幕DPI和个人手感调整。对于立方体这种简单对象两个欧拉角完全够用如果想做复杂工程模型建议引入四元数累积避免欧拉角的万向锁问题但那就是另一个话题了。4.3 旋转轴与速度的细节旋转轴不是随便写的。想让立方体像陀螺一样绕竖直轴转就只用Y轴增量想体现立体感最好同时叠加X和Y轴旋转配合透视投影立方体的空间关系才会有说服力。项目里我让自动旋转走Y轴鼠标拖拽同时操作X/Y轴这样视觉上既有“自动展示”的感觉又能让用户自己切到想看的角度。还有一个容易忽略的地方OpenGL里的旋转默认是逆时针为正遵循右手定则。所以拖拽方向是否“顺手”需要实际跑一遍确认。方向反了的话把增量前面的系数改成负号就好。这种小细节往往比写代码更磨人。我建议把灵敏度和初始角度都定义成具名常量比如MOUSE_SENSITIVITY和AUTO_ROTATE_SPEED调试时可以快速改参数不用到处找魔法数字。5. 常见问题与坑位排查5.1 黑屏或只显示背景色这是最让人抓狂的情况几乎人人都遇到过。我的排查顺序是先看着色器编译日志确认link成功。再确认VAO、VBO、EBO绑定正确绘制前有没有重新绑定VAO。检查MVP矩阵是否为单位矩阵。如果忘了给uniform传值OpenGL会使用默认的0矩阵画面必然消失。最后检查深度测试是否开启否则三角形先后顺序会乱。我自己有一个小技巧先画一个不带旋转的纯色三角形用最简着色器跑通后再加复杂逻辑。这样做能快速定位到底是缓冲区问题还是矩阵问题。别嫌步骤多图形调试本来就比业务逻辑调试更需要耐心。5.2 OpenGL版本与系统兼容问题在一台老电脑的虚拟机上我把SurfaceFormat设为3.3 Core程序启动后直接提示上下文创建失败。这时可以先用glGetString(GL_VERSION)看当前可用版本qDebug() reinterpret_castconst char*(glGetString(GL_VERSION));如果硬件确实太老就退回到OpenGL 2.1兼容模式同时把着色器降级为旧语法。例如#version 120 attribute vec3 aPos; uniform mat4 mvp; void main() { gl_Position mvp * vec4(aPos, 1.0); }Qt对OpenGL上下文的创建封装得已经比较完善但驱动兼容性最终只能靠降级解决。如果你要发布给不同公司、不同配置的用户最好在启动时自动检测版本再决定加载哪一套着色器程序。5.3 性能优化与QOpenGLWidget的注意事项在paintGL里千万不要创建QOpenGLShaderProgram、QOpenGLBuffer这类对象。把它们都放到initializeGL里一次性初始化否则每帧频繁分配GPU资源和驱动对象轻则掉帧重则引起上下文切换异常。动态数据更新时尽量复用已有的VBO用QOpenGLBuffer::write替换数据而不是反复allocate。另外Qt在Windows下有时会报webenginecontext used before qtwebengine::initialize() or opengl context creation这是QWebEngine和OpenGL共用上下文导致的。如果项目里同时用QWebEngineView和QOpenGLWidget一定要先初始化QApplication和默认SurfaceFormat再创建WebEngine控件顺序颠倒就会出问题。我在排查这个问题时花了半天最后就是调整创建顺序解决的。等基础立方体跑通你可以把它扩展成OBJ模型加载、贴纹理、加光照或者改成绘制三维曲线、接QCustomPlot的OpenGL加速显示。核心思路都一样先有稳定的Qt渲染框架再往里面填业务数据。这个流程我后来换过好几次渲染对象整体骨架都没变省了很多事。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门