资讯动态

Qt + OpenGL 3D图形绘制与旋转:基于QOpenGLWidget的工程实践

发布时间:2026/10/19 10:43:44 来源:尧图企业网站定制
简介面向Qt/OpenGL初学者的源码包演示三维空间中四棱锥的绘制与旋转操作。四棱锥由4个共顶点的三角形侧面和1个四边形底面共5个面封闭构成绘制前需先计算各顶点三维坐标再按照OpenGL图元装配流程依次提交各面渲染适合已掌握平面图形着色、希望进阶立体渲染的读者。压缩包内共16个文件cpp和h为程序源码及接口声明ui为界面设计pro和makefile负责工程构建release与debug目录存放编译产物exe可直接运行观察效果txt记录了实现过程要点。整个资源包仅451KB轻量易下载。当前已有2421人学习浏览可见该主题关注度较高。通过源码可学习三维顶点坐标设定、面片绘制顺序、旋转动画的实现方法以及Qt与OpenGL结合的项目组织方式方便对照修改与二次练习也可作为三维图形入门课程的参考范例。1. Qt OpenGL 3D图形的绘制和旋转一个 16 毫秒循环里的几何真相之前接了一个模拟项目X对方要一个能自动旋转的 3D 图形展示页。我一开始想直接拿原生 OpenGL 写结果光在窗口创建、上下文初始化、平台差异这些地方就折腾了两天还没到画三角形那一步。后来换到 Qt 的 QOpenGLWidget才发现原来 Qt 源码早就把这些脏活全包了窗口系统、上下文管理、垂直同步、高 DPI 适配全都封装得干干净净开发者真正要关心的只剩两件事——顶点数据怎么组织旋转矩阵怎么更新。这个项目就是这样一条最短路径用 Qt 承接 OpenGL 3D图形的绘制和旋转从初始化到着色器编译再到鼠标拖拽旋转每一步都有完整源码可看、可改、可跑。它适合两种人写过 Qt 界面但没碰过 OpenGL 的开发者以及学过 OpenGL 但被窗口创建劝退、想快速看到几何体在屏幕上转起来的新手。2. 工程骨架解读从 .pro 文件到 Shader 的 Qt 源码路径2.1 为什么选 QOpenGLWidget 而不是 QGLWidget 或原生窗口老项目里经常看到 QGLWidget那是 Qt 4 时代的东西Qt 5 里虽然还能用但官方已经不再推荐。QOpenGLWidget 是替代品它对图形驱动做了更合理的封装最直接的好处是支持高 DPI 屏幕在 Windows 和 macOS 上不会出现界面模糊或者坐标错位。原生窗口这条路我也试过在 Windows 上要处理 WGL在 Linux 上要处理 GLX 或者 EGL在 macOS 上又是另一套 CGL同一个图形代码写三份平台适配纯属给自己找不痛快。QOpenGLWidget 配合 QOpenGLFunctions_3_3_Core能把 OpenGL 3.3 核心模式的函数直接拉进 Qt 环境。这意味着你不需要自己手动解析 glDrawArrays 这类函数指针不需要在 Windows 上写一大段 wglGetProcAddress 的逻辑。用的时候只要让 GLWidget 继承 QOpenGLWidget 和 QOpenGLFunctions_3_3_Core然后在 initializeGL 里调用 initializeOpenGLFunctions()后面所有 gl 开头的函数就能直接调了。这种继承方式在 Qt 源码里很常见是标准的扩展写法。2.2 工程结构与构建配置整个项目的文件结构非常精简一个窗口类加一个入口文件就够OpenGLDemo/ ├── OpenGLDemo.pro ├── main.cpp ├── glwidget.h └── glwidget.cpp.pro 文件只需要这几行QT widgets gui CONFIG c17 TARGET OpenGLDemo SOURCES main.cpp glwidget.cpp HEADERS glwidget.h很多初学者会以为要加 QT opengl其实在 Qt 5 以后不需要。QOpenGLWidget、QOpenGLBuffer、QOpenGLShaderProgram 这些类都归属在 Qt GUI 模块里pro 里写了 gui 就够用。编译命令也简单qmake 之后 make 或者 nmake没有额外的依赖配置。这里有一个建议使用 Qt 源码调试时尽量在 pro 里加上 CONFIG debug这样如果着色器编译失败或者 OpenGL 函数调用出错断点能停到 Qt 源码内部方便追踪。main.cpp 里做的第一件事不是我一开始以为的创建窗口而是设置 QSurfaceFormat#include QApplication #include QSurfaceFormat #include glwidget.h int main(int argc, char *argv[]) { QApplication app(argc, argv); QSurfaceFormat fmt; fmt.setVersion(3, 3); // 请求 OpenGL 3.3 fmt.setProfile(QSurfaceFormat::CoreProfile); // 核心模式 fmt.setSwapInterval(1); // 垂直同步避免画面撕裂 QSurfaceFormat::setDefaultFormat(fmt); GLWidget w; w.resize(800, 600); w.show(); return app.exec(); }setVersion 和 setProfile 决定 OpenGL 上下文版本和模式setSwapInterval(1) 是交换缓冲区时的垂直同步开关。这三个参数必须在窗口创建之前设置QSurfaceFormat::setDefaultFormat 会让后续所有 QOpenGLWidget 都使用这个格式。如果在创建窗口之后再改格式新版本下通常不生效这是我踩过一次的坑。2.3 三个矩阵模型、视图、投影在 Qt 里如何分工OpenGL 渲染 3D 图形时顶点要经过三次坐标变换才能到屏幕上模型矩阵把物体从自身坐标搬到世界坐标视图矩阵把世界坐标搬到相机坐标投影矩阵把相机坐标压到裁剪空间。很多教程把这三个矩阵写在一页公式里看得人头晕其实用 QMatrix4x4 做这件事非常简单。void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); float aspect float(w) / float(h); m_projection.setToIdentity(); m_projection.perspective(45.0f, aspect, 0.1f, 100.0f); m_view.setToIdentity(); m_view.translate(0.0f, 0.0f, -3.0f); }perspective 的第一个参数是视场角45 度比较接近人眼习惯第二个参数是宽高比窗口一变这个值也要重算否则图形会被压扁第三、四个参数是近裁剪面和远裁剪面只有在这个范围内的顶点才会被绘制。translate(0, 0, -3) 相当于把相机往后拉 3 个单位这样几何体才能出现在视野里。这里要特别说清楚投影矩阵和视图矩阵在 resizeGL 里设置一次就够了因为窗口尺寸和相机位置在运行过程中相对稳定。模型矩阵则要在每帧绘制时更新因为旋转是个持续变化的过程。这个分工想清楚之后paintGL 里的代码会清爽很多。2.4 顶点数据与缓冲的生命周期顶点数据不会直接发到显卡而是要经过 VBO 和 VAO 两步。VBO 存的是原始顶点字节流VAO 存的是“顶点数据怎么解释”的配置。一个形象的理解VBO 是一堆文件内容VAO 是文件格式说明OpenGL 读文件时必须先拿到格式说明才知道每个字节是位置还是颜色。m_vbo.create(); m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);setUsagePattern 告诉驱动这块缓冲的用法StaticDraw 表示数据几乎不变适合静态几何体如果每帧都在改顶点位置就要用 DynamicDraw。缓冲区销毁也要手动管理最好在析构函数里按先 VBO 后 VAO 的顺序 destroy。后面第 5 章会详细说你如果不这么做程序退出时大概率直接崩溃。3. 核心模块拆解一个顶点从数组到屏幕的完整旅程3.1 initializeGL 里发生了什么initializeGL 是 QOpenGLWidget 提供的第一道入口它在 OpenGL 上下文准备好之后被调用一次适合做所有初始化工作。这里先把完整代码贴出来后面两节再逐块拆开讲。void GLWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.1f, 0.12f, 0.16f, 1.0f); glEnable(GL_DEPTH_TEST); // 编译器顶点着色器和片元着色器 m_program new QOpenGLShaderProgram(this); if (!m_program-addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) || !m_program-addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) || !m_program-link()) { qCritical() Shader error: m_program-log(); return; } // 一个四面体4 个三角面每个面 3 个顶点每个顶点 6 个 float const GLfloat vertices[] { // 底面 -0.5f, -0.5f, -0.4f, 1.0f, 0.3f, 0.3f, 0.5f, -0.5f, -0.4f, 0.3f, 1.0f, 0.3f, 0.0f, -0.5f, 0.5f, 0.3f, 0.3f, 1.0f, // 侧面 1 0.0f, 0.5f, 0.0f, 1.0f, 0.8f, 0.2f, -0.5f, -0.5f, -0.4f, 1.0f, 0.3f, 0.3f, 0.0f, -0.5f, 0.5f, 0.3f, 0.3f, 1.0f, // 侧面 2 0.0f, 0.5f, 0.0f, 0.2f, 1.0f, 0.8f, 0.5f, -0.5f, -0.4f, 0.3f, 1.0f, 0.3f, -0.5f, -0.5f, -0.4f, 1.0f, 0.3f, 0.3f, // 侧面 3 0.0f, 0.5f, 0.0f, 0.8f, 0.2f, 1.0f, 0.0f, -0.5f, 0.5f, 0.3f, 0.3f, 1.0f, 0.5f, -0.5f, -0.4f, 0.3f, 1.0f, 0.3f, }; m_vao.create(); m_vao.bind(); m_vbo.create(); m_vbo.bind(); m_vbo.allocate(vertices, sizeof(vertices)); // 顶点属性位置在前 3 个 float颜色在后 3 个 float GLint posLoc m_program-attributeLocation(aPos); m_program-enableAttributeArray(posLoc); m_program-setAttributeBuffer(posLoc, GL_FLOAT, 0, 3, 6 * sizeof(GLfloat)); GLint colorLoc m_program-attributeLocation(aColor); m_program-enableAttributeArray(colorLoc); m_program-setAttributeBuffer(colorLoc, GL_FLOAT, 3 * sizeof(GLfloat), 3, 6 * sizeof(GLfloat)); m_vao.release(); }这段代码里有一个很容易忽略的设计四面体顶点故意没有复用。底面有 3 个顶点三个侧面又各自独立写了完整顶点总共 12 个顶点。原因是顶点颜色是逐顶点插值的如果底面和侧面共用同一个顶点位置那么两个面在这个顶点上的颜色必须一致整个模型会失去彩色面效果。为了直观展示 3D 面这里用独立顶点是最省心的做法。3.2 GLSL 着色器位置、颜色和 uniform 矩阵的接口约定着色器源码以字符串形式存在 C 代码里这是 Qt OpenGL 工程最常见的做法。顶点着色器的任务是把每个顶点的位置从模型空间变换到裁剪空间#version 330 core layout(location 0) in vec3 aPos; layout(location 1) in vec3 aColor; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProjection; out vec3 vColor; void main() { vColor aColor; gl_Position uProjection * uView * uModel * vec4(aPos, 1.0); }注意 gl_Position 这行矩阵乘法的顺序顶点先在模型空间然后被视图矩阵搬到相机空间最后经过投影矩阵裁剪。顺序反了会得到完全错误的结果这是后面避坑章节里要重点聊的话题。片元着色器更简单拿到顶点着色器传来的颜色直接输出#version 330 core in vec3 vColor; out vec4 fragColor; void main() { fragColor vec4(vColor, 1.0); }Qt 里编译着色器不需要手动调 glCompileShaderQOpenGLShaderProgram 已经把整个流程包好了。addShaderFromSourceCode 传入着色器类型和源码字符串link 做链接。任何一个环节失败都可以用 log() 拿到编译日志日志里会精确告诉你哪一行 GLSL 出错、变量名拼错还是版本号不匹配。3.3 顶点布局给 OpenGL 讲清每个顶点的字节结构顶点数据已经塞进 VBO 了但 OpenGL 还不知道这堆二进制里哪些字节是 x、y、z哪些字节是 r、g、b。setAttributeBuffer 就是干这个的m_program-setAttributeBuffer(posLoc, GL_FLOAT, 0, 3, 6 * sizeof(GLfloat)); m_program-setAttributeBuffer(colorLoc, GL_FLOAT, 3 * sizeof(GLfloat), 3, 6 * sizeof(GLfloat));第一个参数是顶点属性的 location对应着色器里的 layout(location)。第二个参数是数据类型这里是 float。第三个参数是偏移量位置从第 0 个字节开始颜色从第 3 个 float 也就是 12 字节处开始。第四个参数是分量个数位置有 3 个分量颜色也是 3 个。第五个参数是步长也就是一个完整顶点占多少字节6 个 float 共 24 字节。很多新手的翻车现场就是把步长写成 0OpenGL 会认为顶点数据是紧密排列的颜色和位置就会错位。这组调用必须在 VAO 绑定状态下执行顺序也很重要先 bind VAO再 bind VBO再设置 attribute。如果顺序反了VAO 可能没有正确记录 VBO 的关联关系结果就是画面黑屏或者只有背景色。3.4 paintGL绑定、绘制、解绑的顺序为什么不能乱初始化完成后每一帧的绘制都发生在 paintGL 里。这个函数在窗口需要重绘时被 Qt 自动调用我们不应该手动直接调它而是调 update() 触发。绘制代码看起来就那么几行void GLWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); m_program-bind(); QMatrix4x4 model; model.rotate(m_angleX, 1.0f, 0.0f, 0.0f); model.rotate(m_angleY, 0.0f, 1.0f, 0.0f); m_program-setUniformValue(uModel, model); m_program-setUniformValue(uView, m_view); m_program-setUniformValue(uProjection, m_projection); m_vao.bind(); glDrawArrays(GL_TRIANGLES, 0, 12); m_vao.release(); m_program-release(); }glClear 清屏时要同时清理颜色缓冲和深度缓冲只清颜色缓冲的话旋转时后面的三角面会叠到前面来。model 矩阵每帧重建用两个累计角度分别绕 X 轴和 Y 轴旋转。setUniformValue 把矩阵传给着色器里的 uniform 变量名称要和 GLSL 里完全一致。最后 glDrawArrays 的第一个参数是图元类型三角形就是 GL_TRIANGLES第二个参数是起始顶点索引第三个参数是顶点数量12 个顶点对应 4 个三角形面。这里的绘制顺序不能乱先绑定 shader program再设置 uniform再绑定 VAO最后解绑顺序反过来。如果你先绑 VAO 再绑 shader部分驱动上 uniform 赋值可能不会生效。4. 旋转与交互实现定时器自动旋转和鼠标拖拽的手感调校4.1 基于 QTimer 的 60 FPS 自动旋转自动旋转的本质是每隔固定时间让角度增加几度然后请求重绘。QTimer 是最直观的选择它天然跑在 Qt 事件循环里不需要自己开线程m_timer new QTimer(this); m_timer-setTimerType(Qt::PreciseTimer); m_timer-setInterval(16); connect(m_timer, QTimer::timeout, this, [this]() { m_angleY 0.8f; if (m_angleY 360.0f) { m_angleY - 360.0f; } update(); }); m_timer-start();setInterval(16) 对应约 60 FPS因为 1000 毫秒除以 16 约等于 62 帧。setTimerType(Qt::PreciseTimer) 会让定时器尽量贴近精确时间而不是省电模式下的粗粒度时间旋转动画会明显顺滑很多。每次 timeout 只加 0.8 度每秒约 48 度转完一整圈需要 7.5 秒这个速度在视觉上比较舒服。这里有初学者容易误解的一点update() 不是立即调用 paintGL而是把窗口标记为“需要重绘”等 Qt 回到事件循环后才会触发 paintGL。这样设计是为了避免重绘嵌套防止一帧绘制过程中又触发另一帧绘制。所以定时器只管改状态绘制的事全部交给 paintGL各干各的。4.2 累加角度与模型矩阵的两种写法实现旋转有两种风格一种是我们现在用的角度累加一种是在 paintGL 里直接对模型矩阵做累乘m_model.rotate(0.8f, 0.0f, 1.0f, 0.0f);这种写法乍一看更简洁因为不需要管 m_angleX、m_angleY 这些变量。但是它有很明显的短板模型矩阵会一直在矩阵里累积数值经过几百帧的浮点运算后矩阵里的数字会偏离真实旋转而且想重置回初始姿态的时候必须保留一个初始矩阵副本。角度累加则不同角度变量是可控的、可读的可以随时归零可以保存到配置文件里甚至可以做缓动动画。paintGL 里有两种旋转的建模方式区别还体现在交互上。鼠标拖拽改变角度timer 也改变角度两者都是朝同一个变量里加数字不会互相冲突。如果你想做“拖拽时暂停自动旋转松开后再继续”这类交互角度累加配合一个 bool 状态变量几行就能实现矩阵累乘方式就要处理矩阵的叠加和回滚麻烦得多。4.3 鼠标拖拽旋转屏幕坐标差到旋转角度的换算鼠标旋转的原理是把鼠标的横向位移映射成绕 Y 轴的角度纵向位移映射成绕 X 轴的角度。为了算位移量必须记录上一次的鼠标位置void GLWidget::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { m_lastPos event-pos(); m_autoRotate false; // 拖拽时暂停自动旋转 } } void GLWidget::mouseMoveEvent(QMouseEvent *event) { if (!(event-buttons() Qt::LeftButton)) { return; } int dx event-pos().x() - m_lastPos.x(); int dy event-pos().y() - m_lastPos.y(); m_angleY dx * 0.3f; m_angleX dy * 0.3f; m_lastPos event-pos(); update(); }0.3f 这个系数决定手感鼠标移动 10 个像素模型转 3 度。系数太大模型会像失控一样狂转太小则拖半天只挪一点点我一般先设 0.3跑起来再按实际体感微调。拖拽旋转直觉上是“模型跟着鼠标走”dx 为正说明鼠标向右模型应该绕 Y 轴往右转m_angleY 加的是正数dy 为正说明鼠标向下模型应该向下翻转但绕 X 轴的正方向是向上所以 m_angleX 这里的符号调整要看你用的坐标系规则跑起来不对就换符号这个不值得死记。拖拽时把自动旋转关掉是交互设计上的一个小细节。如果拖拽的同时 timer 还在加角度画面会像失控一样自己动个不停用户会感觉鼠标失灵。m_autoRotate 在 mouseReleaseEvent 里可以选择恢复也可以保持暂停看业务需求。4.4 暂停、重置和速度接口的设计为了让旋转功能可复用最好不要让外界直接访问 m_angleX 和 m_angleY 这些内部变量而是提供语义化接口void GLWidget::setAutoRotate(bool on); void GLWidget::setRotateSpeed(float degreePerFrame); void GLWidget::resetRotation();setAutoRotate 控制 timer 是否 start 或 stopsetRotateSpeed 设置每帧加多少度resetRotation 把角度归零。这样改造之后外部调用方不需要知道内部有 QTimer、有角度变量只需要理解这三个接口的含义。后续如果想把自动旋转换成 QPropertyAnimation 或者加缓动曲线内部可以改接口不用动。这也是从“自己跑的脚本”到“可以交给别人用的小组件”之间关键的一步。这个接口设计的思路在 Qt 源码里很常见QPropertyAnimation、QAbstractAnimation 这些类都是把“动画状态”和“动画计算”分离外部只关心 start、stop、setDuration。照着这个模式写出来的 GLWidget之后接进模拟项目X几乎不需要改动就能直接嵌进主界面。5. 避坑与排查黑屏、拉伸、闪动和转错方向的现场复盘5.1 黑屏大多数时候是着色器日志没看现象程序能启动窗口背景色也有但 3D 图形完全看不到没有报错弹窗。原因最常见的是 GLSL 代码编译失败。比如版本号写错了、变量名拼错了、或者 uniform 名字在 C 里和对局器里不一致。QOpenGLShaderProgram 的错误是静默的不主动打日志你根本不知道失败在哪一行。另一个常见原因是 VAO 绑定顺序错了顶点属性没有被正确记录。解决把错误日志打出来不要猜。在 initializeGL 里判断 addShaderFromSourceCode 和 link 的返回值if (!m_program-addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource) || !m_program-addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource) || !m_program-link()) { qCritical() Shader log: m_program-log(); return; }qCritical 会把日志输出到控制台。看到日志后再对照 GLSL 源码逐行排查比对着屏幕发呆快得多。如果日志为空再检查 glVertexAttribPointer 相关的布局设置。5.2 图形压扁或拉长宽高比和 devicePixelRatio 的锅现象窗口拉宽后三角形变扁拉高后图形变瘦在带高分屏的笔记本上图形明显偏小或偏移。原因投影矩阵里的 aspect 没有跟着窗口尺寸走。很多人只在 initializeGL 里设置过一次 perspective窗口 resize 之后 aspect 还是旧值。高分屏的问题更隐蔽QOpenGLWidget 内部的实际绘制尺寸是窗口逻辑尺寸乘以设备像素比如果你只用 width() 和 height()在 Retina 屏幕上 aspect 就是错的。解决在 resizeGL 里重新计算投影矩阵并且乘上 devicePixelRatiovoid GLWidget::resizeGL(int w, int h) { int realW int(w * devicePixelRatio()); int realH int(h * devicePixelRatio()); glViewport(0, 0, realW, realH); float aspect float(realW) / float(realH); m_projection.setToIdentity(); m_projection.perspective(45.0f, aspect, 0.1f, 100.0f); }glViewport 也要用真实像素尺寸否则画面只会占窗口一角。这个细节在 Qt 源码里几乎每个图形示例都会写新手最容易漏。5.3 旋转方向不对矩阵乘法顺序的左右乘问题现象鼠标向右拖模型向左转或者定时器明明是加角度模型却往反方向转。原因本质上是对“旋转叠加”的理解偏差。OpenGL 变换矩阵乘法不满足交换律uModel * vertex 是先应用模型矩阵再应用顶点数据而 model.rotate 这个调用是让新的旋转右乘到当前模型矩阵上。如果你在 paintGL 里先 rotate 了 Y 轴又 rotate 了 X 轴旋转是在模型局部空间发生的顺序和世界空间相反。解决一次只改一个变量别同时调符号和矩阵顺序。先把 X 轴旋转注释掉只保留绕 Y 轴旋转确认方向正确后再打开 X 轴旋转看效果。如果方向还是反的就在 rotate 前面加个负号。这个小技巧很朴素但非常管用组合旋转出问题时拆成单轴逐个验证能少走很多弯路。5.4 画面闪烁和撕裂垂直同步没有开现象模型旋转时画面横向撕裂运动越快越明显帧率忽高忽低。原因交换缓冲区时显卡和显示器的刷新节奏没对齐。OpenGL 默认的 surface format 在部分驱动上不开启垂直同步可能一帧显示到一半就被下一帧覆盖了。解决在创建窗口之前设置交换间隔。main.cpp 里的 QSurfaceFormat::setSwapInterval(1) 就是干这个的。注意它必须放在 QApplication 创建之后、GLWidget 创建之前这个顺序很关键。如果设了还是闪检查是不是有多个 QOpenGLWidget 叠加在同一区域多个上下文同时交换缓冲区也会导致闪烁。5.5 程序退出时崩溃析构函数没 makeCurrent现象程序正常运行但退出时崩溃或者控制台打印出 OpenGL context 相关的错误。原因GLWidget 销毁时OpenGL 上下文可能已经不存在或不再当前此时去销毁 VBO、VAO 就等于操作一个无效上下文。这是 Qt 的 QOpenGLWidget 生命周期里最容易踩的坑。解决在析构函数里先 makeCurrent再销毁 GPU 资源最后 doneCurrentGLWidget::~GLWidget() { makeCurrent(); m_vbo.destroy(); m_vao.destroy(); delete m_program; doneCurrent(); }makeCurrent 会把 OpenGL 上下文切换到当前线程保证后续的 destroy 调用是在合法上下文里执行的。delete shader program 也要放在这个区间里否则同样会崩。从那以后我每次写 GLWidget析构函数永远是第一个写的地方。6. 二次开发方向把单模型 Demo 改成多模型管理的小框架6.1 先重构数据结构把顶点数组收进 Mesh 对象拿到这份资源做二次开发第一步不是加光照也不是换模型而是把零散的 VBO、VAO 和顶点数量收拢成一个 Mesh 结构struct Mesh { QOpenGLBuffer vbo; QOpenGLVertexArrayObject vao; int vertexCount 0; float angleX 0.0f; float angleY 0.0f; float speed 1.0f; };一个窗口维护一个 QVectorMesh每个 Mesh 有自己独立的缓冲和旋转状态。paintGL 里遍历所有 Mesh逐个绑定、设置 uniform、glDrawArrays。这样一次性能显示多个旋转速度不同的几何体也是小型场景图系统的最初形态。场景图再往上扩展就是每个 Mesh 再挂一个坐标信息、一个父节点那已经是另一个层级的话题了但基本功就是这里打下的。6.2 用四元数替代两轴欧拉角消除万向锁当前实现用 m_angleX 和 m_angleY 两个角度保存旋转状态简单但有限制当绕 X 轴转到接近 90 度时Y 轴旋转会出现万向锁模型翻转方向突然变得很奇怪。解决方法是把角度变量换成一个 QQuaternion鼠标拖拽时用四元数相乘QQuaternion delta QQuaternion::fromAxisAndAngle( QVector3D(0.0f, 1.0f, 0.0f), dx * 0.3f); m_rotation delta * m_rotation;paintGL 里把四元数转成矩阵传给 uniformm_program-setUniformValue(uModel, m_rotation.toRotationMatrix());四元数旋转不会出现万向锁而且旋转轨迹更接近鼠标画出的弧线手感比两个角度变量的方式上一个档次。改动量不大但能明显提升交互体验。6.3 从手写顶点到加载模型文件手写顶点数组只适合 demo 和调试。再往后走大概率是要加载外部模型文件。最常见的格式是 OBJ结构简单Qt 的 QOpenGLBuffer 直接能接。加载时需要解析 v 开头的顶点坐标、vn 法线、vt 纹理坐标以及 f 面的顶点索引。注意 OBJ 的索引方式和 OpenGL 的索引缓冲不完全一样很多时候要做顶点去重和索引重排这里会踩不少坑。建议先用一个简单的三角形 OBJ 文件打通链路再换复杂模型。6.4 再加一点纹理和光照把顶点着色器里的 aColor 换成 aTexCoord片元着色器里增加一个 sampler2D uniform结合 QOpenGLTexture 就能贴上纹理。光照方面给每个顶点增加法线属性在片元着色器里做最简单的兰伯特光照albedo 乘以 max(dot(N, L), 0)。这两个效果加上之后模型从“彩色塑料”变成“有质感的物体”。改完记得回到第 5 章重新检查一遍避坑清单尤其是布局 stride 有没有包含法线和纹理坐标。这套改造路径走完你手里的工程跟顶多只能画四面体的 Demo 已经没有太大关系了它已经具备一个简易三维查看器的雏形。希望帮到你拆完这份 Qt 和 OpenGL 的源码再去改出自己的功能。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑