目录
- 一、为什么要学 3D 图形开发
- 二、3D 图形基础概念(零基础也能懂)
- 三、坐标系统:3D 世界的"地图规则"
- 四、变换矩阵:让物体动起来的数学魔法
- 五、渲染管线:从数据到像素的旅程
- 六、Qt3D 的 ECS 架构:与传统 OOP 的思维转变
- 七、搭建第一个 Qt3D 项目
- 八、第一个 3D 场景:旋转的立方体
- 九、深入理解 Mesh 与 Material
- 十、相机与光照:让场景"活"起来
- 十一、键盘鼠标交互:3D 场景的导航
- 十二、加载外部 3D 模型文件
- 十三、动画系统:让物体自己动起来
- 十四、逐帧逻辑:FrameAction 与定时器
- 十五、综合实战:多物体场景管理
- 十六、调试技巧与常见问题
- 十七、总结与学习路线
一、为什么要学 3D 图形开发
如果你已经熟悉 C++ 和 Qt,可能会问:我一直在做桌面应用,为什么要学 3D?
实际上,3D 图形在工业领域应用极为广泛:
- 工业 HMI / SCADA:工厂产线 3D 可视化、设备数字孪生
- 机器人:机械臂仿真、路径规划可视化、示教器 3D 预览
- 医疗设备:CT/MRI 三维重建、手术导航
- 智慧城市 / 园区:建筑 BIM 可视化、园区 3D 地图
- 教育培训:物理仿真演示、虚拟实验
- 消费电子:产品 3D 预览、AR/VR 交互
· Qt3D 是 Qt 原生模块,与你的 Qt 应用无缝集成,C++ 开箱即用
· OpenGL 太底层,画一个三角形要写几百行代码
· Unity / Unreal 是游戏引擎,体积大、授权复杂,不适合嵌入式 HMI
· Three.js 是 JavaScript 库,不适合 C++ 桌面/嵌入式应用
· Qt3D 在 "足够好用"和"足够轻量"之间找到了平衡点
二、3D 图形基础概念(零基础也能懂)
2.1 从 2D 到 3D:多了什么?
你在 Qt 里画 2D 图形(用 QPainter)时,世界是这样的:
关键区别:
| 概念 | 2D (QPainter) | 3D (Qt3D) |
|---|---|---|
| 坐标维度 | (x, y) 两个值 | (x, y, z) 三个值 |
| 原点位置 | 左上角 (0,0) | 场景中心 (0,0,0) |
| Y 轴方向 | 向下为正 | 向上为正(OpenGL 惯例) |
| 绘制方式 | 直接命令式绘制 | 声明场景数据,引擎渲染 |
| 深度概念 | 无(后画的覆盖先画的) | 有(通过 Z-buffer 决定可见性) |
| 变换 | QTransform(2x3 矩阵) | 4x4 变换矩阵 |
2.2 3D 物体是由什么组成的?
一个 3D 物体看起来很复杂,但本质上只有三个组成部分:
想象你搭一个纸板房子:顶点就是纸板的四个角;三角面就是把纸板沿对角线折一下的三角形;材质就是你在纸板上涂的颜色或贴的壁纸。
2.3 立方体有多少个三角形?
一个立方体有 6 个面,每个面是一个正方形,每个正方形可以分成 2 个三角形,所以立方体 = 6 × 2 = 12 个三角形,需要 8 个顶点(但每个顶点会被多个面共享)。
三、坐标系统:3D 世界的"地图规则"
3.1 右手坐标系
Qt3D 使用的是 右手坐标系(与 OpenGL 一致)。判断方法:伸出右手,手指从 X 轴弯向 Y 轴,拇指方向就是 Z 轴。
X 向右走,Y 往上爬,Z 朝你脸上来。这是 OpenGL / Qt3D 的惯例,也是大多数 3D 引擎的标准。
3.2 多层坐标系:模型坐标系 → 世界坐标系 → 相机坐标系
3D 渲染中存在多个坐标系层级,理解它们是掌握 3D 的关键:
· 模型坐标系:你在建模软件里画一个立方体,原点在立方体中心——这是"物体自己的坐标"
· 世界坐标系:把立方体放到场景里,它移动到了 (5, 0, 3)——这是"在世界中的位置"
· 相机坐标系:你的眼睛(相机)站在某处看这个立方体,从相机视角看到的坐标
· 屏幕坐标系:最终投影到 2D 屏幕上的像素位置
四、变换矩阵:让物体动起来的数学魔法
4.1 三种基本变换
3D 中物体有三种基本变换:平移(Translation)、旋转(Rotation)、缩放(Scale)。在 Qt3D 中,这些变换通过 QTransform 组件实现。
4.2 4x4 矩阵:为什么不是 3x3?
你可能会想:3D 坐标是 (x, y, z) 三个值,为什么变换矩阵是 4x4 而不是 3x3?
因为 3x3 矩阵可以表示旋转和缩放,但无法表示平移。为了让一个矩阵能同时包含平移、旋转和缩放,我们引入了"齐次坐标"——在 (x, y, z) 后面加上一个 w=1,变成 (x, y, z, 1)。
┌ R R R T ┐ R = 旋转缩放部分 (3x3)
│ R R R T │ T = 平移部分 (Tx, Ty, Tz)
│ R R R T │
└ 0 0 0 1 ┘ 最后一行永远是 (0, 0, 0, 1)
好消息是:在 Qt3D 中你几乎不需要手动操作矩阵。Qt3D 提供了 QTransform 类,你只需要设置平移、旋转、缩放三个属性,Qt3D 自动帮你计算矩阵。
// Qt3D 中的变换 —— 你不需要手动算矩阵 Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); // 平移:移到 (5, 0, 3) transform->setTranslation(QVector3D(5, 0, 3)); // 旋转:绕 Y 轴旋转 45 度 transform->setRotation(QQuaternion::fromEulerAngles(0, 45, 0)); // 缩放:放大 2 倍 transform->setScale(2.0f); // Qt3D 内部自动将这三个属性组合成一个 4x4 矩阵
4.3 变换顺序很重要
"先平移再旋转"和"先旋转再平移"结果完全不同!
· 先平移(5,0,0) 再旋转45°:物体先移到右边,然后原地转 45°
· 先旋转45° 再平移(5,0,0):物体先转 45°,然后沿着旋转后的 X 轴方向移动
记忆方法:矩阵乘法不满足交换律,A × B ≠ B × A
五、渲染管线:从数据到像素的旅程
3D 渲染管线(Rendering Pipeline)是 3D 数据变成屏幕像素的完整流程。你不需要理解每个细节,但需要知道大致流程:
着色器是运行在 GPU 上的小程序,用 GLSL(OpenGL Shading Language)编写。
· 顶点着色器:对每个顶点执行一次,计算它的最终屏幕位置
· 片元着色器:对每个像素执行一次,计算它的最终颜色
在 Qt3D 中,你可以使用内置材质(不需要写着色器),也可以自定义着色器实现特殊效果。
如果你是初学者,完全不需要写 GLSL 着色器!Qt3D 内置了多种现成的材质(QPhongMaterial、QDiffuseMapMaterial 等),就像用 QPainter 的 pen 和 brush 一样简单。
六、Qt3D 的 ECS 架构:与传统 OOP 的思维转变
6.1 传统 OOP vs ECS
作为 C++/Qt 开发者,你已经习惯了 OOP(面向对象编程):一个类包含数据和行为。比如 QPushButton 既有属性(text、color),又有方法(click()、setText())。
Qt3D 采用了一种不同的架构:ECS(Entity-Component-System,实体-组件-系统)。
| 维度 | 传统 OOP | ECS(Qt3D) |
|---|---|---|
| 基本单位 | 类对象(数据+行为合一) | Entity(空壳)+ Component(数据) |
| 行为在哪里 | 在类的方法里 | 在 System(系统/后端)里 |
| 组合方式 | 继承(is-a) | 组件挂载(has-a) |
| 类比 | 一个人有名字、会走路 | 一个空壳人 + "名字组件" + "走路组件" |
6.2 Entity 和 Component 的关系
· Entity = 一个空底座,本身什么都不是
· QMesh = 积木的形状(方形、圆形、自定义)
· QTransform = 位置和角度说明书
· QMaterial = 涂什么颜色/贴什么壁纸
· 把这些组件"挂"到 Entity 上,就得到了一个完整的 3D 物体
6.3 代码对比:OOP 写法 vs ECS 写法
// ========== 传统 OOP 写法(假设)========== class Cube { QVector3D position; QColor color; MeshData mesh; public: void render() { // 绘制逻辑 } void setPosition(QVector3D pos) { position = pos; } void setColor(QColor c) { color = c; } }; // ========== Qt3D 的 ECS 写法 ========== // 1. 创建一个空 Entity Qt3DCore::QEntity *entity = new Qt3DCore::QEntity; // 2. 创建 Mesh 组件(形状) auto *mesh = new Qt3DExtras::QCuboidMesh; mesh->setXExtent(2.0f); // 宽 mesh->setYExtent(2.0f); // 高 mesh->setZExtent(2.0f); // 深 // 3. 创建 Transform 组件(位置/旋转/缩放) auto *transform = new Qt3DCore::QTransform; transform->setTranslation(QVector3D(0, 0, -10)); transform->setRotation(QQuaternion::fromEulerAngles(30, 45, 0)); // 4. 创建 Material 组件(材质) auto *material = new Qt3DExtras::QPhongMaterial; material->setDiffuse(QColor("#3b82f6")); // 蓝色 // 5. 把组件挂载到 Entity 上 entity->addComponent(mesh); entity->addComponent(transform); entity->addComponent(material); // 完成!一个蓝色的立方体就出现在 (0,0,-10) 位置
· 组件可以自由组合:同一个 Mesh 换个 Material 就变成另一个外观
· 组件可以复用:同一个 Material 可以挂到多个 Entity 上
· 数据与逻辑分离:System 负责处理逻辑,Component 只存数据
七、搭建第一个 Qt3D 项目
在使用 Qt 在线安装器时,勾选 Qt 3D 模块。如果已安装 Qt,可以在 Qt Maintenance Tool 中追加安装。
# .pro 文件 QT += 3dcore 3drender 3dinput 3dlogic 3dextras 3danimation # 各模块含义: # 3dcore — 核心类:QEntity, QComponent, QTransform # 3drender — 渲染:QViewport, QCamera, QRenderSettings # 3dinput — 输入:键盘鼠标的 3D 导航 # 3dlogic — 逻辑:帧更新(QFrameAction) # 3dextras — 预定义几何体和材质(立方体、球体、Phong 材质等) # 3danimation — 动画系统
# CMakeLists.txt find_package(Qt6 REQUIRED COMPONENTS 3DCore 3DRender 3DInput 3DLogic 3DExtras 3DAnimation) target_link_libraries(myapp PRIVATE Qt6::3DCore Qt6::3DRender Qt6::3DInput Qt6::3DLogic Qt6::3DExtras Qt6::3DAnimation )
// main.cpp — 最小 Qt3D 程序骨架 #include <QGuiApplication> #include <Qt3DWindow> #include <QEntity> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // 1. 创建 3D 窗口 Qt3DExtras::Qt3DWindow *view = new Qt3DExtras::Qt3DWindow; view->setTitle("我的第一个 Qt3D 程序"); view->resize(800, 600); view->show(); // 2. 创建根 Entity(场景根节点) Qt3DCore::QEntity *rootEntity = new Qt3DCore::QEntity; // 3. 设置相机 view->setRootEntity(rootEntity); view->camera()->setViewCenter(QVector3D(0, 0, 0)); view->camera()->setPosition(QVector3D(0, 0, -15)); // 4. TODO: 在这里添加你的 3D 物体…… return app.exec(); }
运行后你会看到一个黑色的 3D 窗口——这只是一个空场景。接下来我们往里添加物体。
八、第一个 3D 场景:旋转的立方体
现在我们创建一个完整的、可见的 3D 场景:一个在屏幕中央旋转的彩色立方体。
#include <QGuiApplication> #include <Qt3DWindow> #include <QEntity> #include <QCamera> #include <QCuboidMesh> #include <QPhongMaterial> #include <QTransform> #include <QPropertyAnimation> #include <QOrbitCameraController> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // ===== 1. 创建 3D 窗口和根实体 ===== auto *view = new Qt3DExtras::Qt3DWindow; view->setTitle("旋转立方体 — 第一个 Qt3D 程序"); view->resize(800, 600); auto *root = new Qt3DCore::QEntity; // ===== 2. 设置相机(你的眼睛) ===== view->camera()->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); view->camera()->setPosition(QVector3D(5, 5, -5)); view->camera()->setViewCenter(QVector3D(0, 0, 0)); // ===== 3. 设置相机控制器(鼠标拖拽旋转视角) ===== auto *camController = new Qt3DExtras::QOrbitCameraController(root); camController->setCamera(view->camera()); // ===== 4. 创建立方体 Entity ===== auto *cubeEntity = new Qt3DCore::QEntity(root); // 4a. Mesh — 形状 auto *cubeMesh = new Qt3DExtras::QCuboidMesh; cubeMesh->setXExtent(2.0f); cubeMesh->setYExtent(2.0f); cubeMesh->setZExtent(2.0f); // 4b. Material — 材质(颜色) auto *material = new Qt3DExtras::QPhongMaterial; material->setDiffuse(QColor("#3b82f6")); // 主体色:蓝 material->setSpecular(QColor("#ffffff")); // 高光色:白 material->setShininess(0.5f); // 光泽度 // 4c. Transform — 变换(位置/旋转) auto *transform = new Qt3DCore::QTransform; transform->setTranslation(QVector3D(0, 0, 0)); // 4d. 挂载组件 cubeEntity->addComponent(cubeMesh); cubeEntity->addComponent(material); cubeEntity->addComponent(transform); // ===== 5. 用 QPropertyAnimation 让立方体自动旋转 ===== auto *animation = new QPropertyAnimation(transform, "rotation"); animation->setDuration(10000); // 10 秒一圈 animation->setStartValue(QQuaternion::fromEulerAngles(0, 0, 0)); animation->setEndValue(QQuaternion::fromEulerAngles(0, 360, 0)); animation->setLoopCount(-1); // 无限循环 animation->start(); // ===== 6. 设置根实体并显示 ===== view->setRootEntity(root); view->show(); return app.exec(); }
运行效果:一个蓝色立方体在屏幕中央,绕 Y 轴匀速旋转。鼠标拖拽可以旋转视角,滚轮可以缩放。
· 步骤 1-2:创建窗口和相机,相机放在 (5,5,-5) 位置看向原点
· 步骤 3:QOrbitCameraController 让鼠标能拖拽旋转视角(轨道相机)
· 步骤 4:创建立方体 = Entity + Mesh(形状) + Material(蓝色) + Transform(位置)
· 步骤 5:QPropertyAnimation 驱动 Transform 的 rotation 属性,实现自动旋转
· 步骤 6:将场景树根节点设置给窗口,开始渲染
九、深入理解 Mesh 与 Material
9.1 Qt3D 内置几何体
Qt3D 在 Qt3DExtras 模块中提供了多种现成几何体:
| 类名 | 形状 | 常用参数 |
|---|---|---|
| QCuboidMesh | 立方体/长方体 | xExtent, yExtent, zExtent(半边长) |
| QSphereMesh | 球体 | radius, rings(纬线数), slices(经线数) |
| QCylinderMesh | 圆柱体 | radius, length, rings |
| QConeMesh | 圆锥体 | topRadius, bottomRadius, length |
| QTorusMesh | 圆环(甜甜圈) | radius, minorRadius, rings, sides |
| QPlaneMesh | 平面 | width, height |
| QExtrudedTextMesh | 3D 文字 | text, font |
// 球体示例 auto *sphere = new Qt3DExtras::QSphereMesh; sphere->setRadius(3.0f); // 半径 3 sphere->setRings(32); // 32 条纬线(越大越光滑) sphere->setSlices(32); // 32 条经线 // 圆环示例 auto *torus = new Qt3DExtras::QTorusMesh; torus->setRadius(4.0f); // 主半径 torus->setMinorRadius(1.0f); // 管半径 torus->setRings(32); torus->setSides(32);
9.2 材质类型
Qt3D 内置了多种材质,从简单到复杂:
| 材质类 | 效果 | 适用场景 |
|---|---|---|
| QPerVertexColorMaterial | 每个顶点有自己的颜色 | 数据可视化、彩色几何体 |
| QPhongMaterial | 经典 Phong 光照模型 | 最常用,适合大多数场景 |
| QDiffuseMapMaterial | 带纹理贴图的 Phong | 需要贴纹理的物体 |
| QNormalMapMaterial | 法线贴图(增加表面细节) | 游戏级别的表面细节 |
| QTextureMaterial | 纯纹理(无光照) | 简单的纹理展示 |
| Gooch(自定义着色器) | Gooch 非真实感渲染 | 技术示意图、卡通效果 |
Phong 材质参数详解
auto *mat = new Qt3DExtras::QPhongMaterial; // 环境光颜色(物体在阴影中也能看到的颜色) mat->setAmbient(QColor("#1a1a2e")); // 漫反射颜色(物体的"本色",受光照影响最大) mat->setDiffuse(QColor("#3b82f6")); // 镜面反射颜色(高光点的颜色) mat->setSpecular(QColor("#ffffff")); // 光泽度(0=粗糙如木头, 1=光滑如金属) mat->setShininess(0.8f); // ┌── 漫反射 (Diffuse) ──┐ ┌── 镜面反射 (Specular) ──┐ // │ 光线均匀散射 │ │ 集中反射形成高光 │ // │ 决定物体"本色" │ │ 决定物体"光泽" │ // └──────────────────────┘ └──────────────────────────┘ // ↓ ↓ // 哑光塑料 金属表面
十、相机与光照:让场景"活"起来
10.1 相机:你的眼睛
3D 场景中如果没有光源,你什么也看不到(全部是黑的)。相机决定了你从哪个角度观察场景。
// 相机设置详解 Qt3DRender::QCamera *camera = view->camera(); // 1. 透视投影(近大远小,人眼效果) // 参数:垂直角度(45°), 宽高比, 近裁面, 远裁面 camera->lens()->setPerspectiveProjection( 45.0f, // FOV:视角宽度,45° 是标准值 16.0f / 9.0f, // 宽高比 0.1f, // 近平面:比这更近的物体不渲染 1000.0f // 远平面:比这更远的物体不渲染 ); // 2. 相机位置 camera->setPosition(QVector3D(5, 5, -5)); // 3. 看向哪里(目标点) camera->setViewCenter(QVector3D(0, 0, 0)); // 4. 向上方向(通常 Y 轴朝上) camera->setUpVector(QVector3D(0, 1, 0));
· FOV = 45°:标准视角,类似于人眼
· FOV = 90°:广角镜头,看到更多但物体显得远
· FOV = 20°:长焦镜头,物体显得大但视野窄
10.2 光照:让物体可见
没有光源,3D 场景就是一片漆黑。Qt3D 提供了三种光源:
// 方向光(模拟太阳光)—— 最常用 auto *sunLight = new Qt3DRender::QDirectionalLight(root); sunLight->setWorldDirection(QVector3D(-1, -1, -1).normalized()); sunLight->setColor(QColor("#ffffff")); sunLight->setIntensity(0.8f); // 点光源(如灯泡) auto *lamp = new Qt3DRender::QPointLight(root); lamp->setColor(QColor("#ffddaa")); // 暖色光 lamp->setIntensity(1.0f); // 给点光源设置位置(需要 Transform) auto *lampTransform = new Qt3DCore::QTransform; lampTransform->setTranslation(QVector3D(5, 8, 0)); lamp->addComponent(lampTransform);
初学者最常见的问题:创建了几何体和材质,运行后屏幕全黑。
原因:场景中没有光源!QPhongMaterial 需要光照才能显示颜色。
解决:至少添加一个 QDirectionalLight 或使用不依赖光照的 QTextureMaterial。
十一、键盘鼠标交互:3D 场景的导航
11.1 内置相机控制器
Qt3D 提供了两种现成的相机控制器:
| 控制器 | 操作方式 | 适用场景 |
|---|---|---|
| QOrbitCameraController | 左键拖拽=旋转视角,右键拖拽=平移,滚轮=缩放 | 物体居中展示(产品展示、机械臂预览) |
| QFirstPersonCameraController | WASD=前后左右移动,鼠标=看向哪 | 第一人称视角(场景漫游) |
// 轨道相机控制器(最常用) auto *orbitController = new Qt3DExtras::QOrbitCameraController(root); orbitController->setCamera(view->camera()); orbitController->setLinearSpeed(100); // 平移速度 orbitController->setLookSpeed(100); // 旋转速度 orbitController->setZoomInLimit(2.0f); // 最近缩放距离
11.2 自定义键盘控制
有时你需要用键盘控制 3D 物体(比如控制机械臂关节),这需要用到 QKeyboard 组件:
// 1. 创建键盘输入设备 auto *keyboard = new Qt3DInput::QKeyboard(root); auto *inputSettings = new Qt3DInput::QInputSettings(root); inputSettings->setEventSource(view); // 2. 创建逻辑控制器(每帧执行的 QComponent) class KeyboardController : public Qt3DLogic::QFrameAction { Q_OBJECT Qt3DInput::QKeyboard *m_keyboard; Qt3DCore::QTransform *m_transform; public: KeyboardController(Qt3DCore::QNode *parent) : QFrameAction(parent), m_keyboard(nullptr), m_transform(nullptr) {} void setKeyboard(Qt3DInput::QKeyboard *kb) { m_keyboard = kb; } void setTransform(Qt3DCore::QTransform *t) { m_transform = t; } protected: void onTriggered(float dt) override { if (!m_keyboard || !m_transform) return; float speed = 5.0f * dt; // 5 单位/秒 QVector3D pos = m_transform->translation(); if (m_keyboard->isKeyPressed(Qt::Key_W)) pos.setZ(pos.z() + speed); // 向前 if (m_keyboard->isKeyPressed(Qt::Key_S)) pos.setZ(pos.z() - speed); // 向后 if (m_keyboard->isKeyPressed(Qt::Key_A)) pos.setX(pos.x() - speed); // 向左 if (m_keyboard->isKeyPressed(Qt::Key_D)) pos.setX(pos.x() + speed); // 向右 m_transform->setTranslation(pos); } }; // 3. 使用 auto *controller = new KeyboardController(root); controller->setKeyboard(keyboard); controller->setTransform(cubeTransform);
dt 是距离上一帧的时间间隔(秒)。用 speed × dt 而不是直接用 speed,是为了保证不同帧率下移动速度一致。
60fps:dt ≈ 0.0167 秒,移动 5×0.0167 ≈ 0.083
30fps:dt ≈ 0.033 秒,移动 5×0.033 ≈ 0.167
一秒钟内的总位移相同!
十二、加载外部 3D 模型文件
在实际项目中,你通常不会用代码画立方体,而是从建模软件(Blender、SolidWorks、Maya 等)导出 3D 模型文件,然后在 Qt3D 中加载。
12.1 支持的模型格式
| 格式 | 来源 | 特点 |
|---|---|---|
| .obj | Wavefront | 最简单通用,不含动画,文本格式 |
| .fbx | Autodesk | 含动画、材质、骨骼,业界标准 |
| .glTF / .gltf | Khronos | 现代标准,体积小、效率高 |
| .dae | COLLADA | XML 格式,含动画 |
| .stl | 3D 打印 | 仅几何体,无颜色/材质 |
| .ply | Stanford | 含顶点颜色,学术常用 |
12.2 加载模型代码
// 加载外部 3D 模型 auto *modelEntity = new Qt3DCore::QEntity(root); // 1. 创建 Mesh,指定模型文件路径 auto *mesh = new Qt3DRender::QMesh; mesh->setSource(QUrl::fromLocalFile("models/robot_arm.obj")); // 2. 创建材质(如果模型自带材质,可以用 QPhongMaterial 作为 fallback) auto *material = new Qt3DExtras::QPhongMaterial; material->setDiffuse(QColor("#cccccc")); // 3. 变换 auto *transform = new Qt3DCore::QTransform; transform->setScale(0.1f); // 模型可能太大,需要缩放 transform->setTranslation(QVector3D(0, 0, 0)); // 4. 挂载组件 modelEntity->addComponent(mesh); modelEntity->addComponent(material); modelEntity->addComponent(transform);
· 模型太大或太小:用 QTransform::setScale() 调整
· 模型方向不对:用 QTransform::setRotation() 旋转
· 模型全白/全黑:模型可能没有自带材质,手动加 QPhongMaterial
· 模型不显示:检查路径是否正确、相机是否能看到
十三、动画系统:让物体自己动起来
13.1 QPropertyAnimation:属性动画
最简单的动画方式是用 Qt 的 QPropertyAnimation 驱动 Entity 的 Transform 属性:
// 让物体从 (0,0,-5) 移动到 (0,0,5),耗时 3 秒 auto *anim = new QPropertyAnimation(transform, "translation"); anim->setDuration(3000); anim->setStartValue(QVector3D(0, 0, -5)); anim->setEndValue(QVector3D(0, 0, 5)); anim->setLoopCount(-1); // 无限循环 anim->start(); // 缓动曲线:让运动更自然(加速→减速) auto *ease = new QEasingCurve(QEasingCurve::InOutQuad); anim->setEasingCurve(*ease);
13.2 动画类型一览
13.3 多属性并行动画
// 同时旋转和缩放(使用 QParallelAnimationsGroup) auto *rotAnim = new QPropertyAnimation(transform, "rotation"); rotAnim->setDuration(5000); rotAnim->setStartValue(QQuaternion::fromEulerAngles(0, 0, 0)); rotAnim->setEndValue(QQuaternion::fromEulerAngles(360, 0, 0)); auto *scaleAnim = new QPropertyAnimation(transform, "scale"); scaleAnim->setDuration(2500); scaleAnim->setStartValue(1.0f); scaleAnim->setEndValue(2.0f); scaleAnim->setLoopCount(2); // 放大再缩小 = 2 次 auto *group = new QParallelAnimationGroup; group->addAnimation(rotAnim); group->addAnimation(scaleAnim); group->setLoopCount(-1); group->start();
十四、逐帧逻辑:FrameAction 与定时器
有些动画逻辑不是简单的"从 A 到 B",而是每帧根据复杂条件计算。这时用 QFrameAction:
// QFrameAction:每帧调用一次 onTriggered(float dt) class Simulator : public Qt3DLogic::QFrameAction { Q_OBJECT public: QVector<Qt3DCore::QTransform*> transforms; // 所有关节的 Transform float time = 0.0f; protected: void onTriggered(float dt) override { time += dt; // 累计时间 // 例:让每个关节做正弦摆动 for (int i = 0; i < transforms.size(); ++i) { float angle = 30.0f * std::sin(time * 2.0f + i * 0.5f); transforms[i]->setRotation( QQuaternion::fromEulerAngles(angle, 0, 0) ); } } }; // 使用 auto *sim = new Simulator(root); sim->transforms.append(joint1Transform); sim->transforms.append(joint2Transform); sim->transforms.append(joint3Transform);
· QFrameAction:与渲染同步,每帧调用一次,dt 是真实帧间隔。适合 3D 物体的逐帧更新
· QTimer:固定间隔触发,不与渲染同步。适合 UI 更新、逻辑调度等
推荐在 3D 场景的逐帧动画中优先使用 QFrameAction
十五、综合实战:多物体场景管理
让我们把前面学到的所有知识组合起来,创建一个包含多个物体、光照、动画的完整场景:
#include <QGuiApplication> #include <Qt3DWindow> #include <QEntity> #include <QCamera> #include <QCameraLens> #include <QOrbitCameraController> #include <QCuboidMesh> #include <QSphereMesh> #include <QCylinderMesh> #include <QPlaneMesh> #include <QPhongMaterial> #include <QTransform> #include <QPropertyAnimation> #include <QDirectionalLight> #include <QParallelAnimationGroup> // 辅助函数:创建一个带 Mesh + Material + Transform 的 Entity Qt3DCore::QEntity* createObject( Qt3DCore::QEntity *parent, Qt3DRender::QGeometryRenderer *mesh, QColor color, QVector3D pos, float scale = 1.0f) { auto *entity = new Qt3DCore::QEntity(parent); auto *mat = new Qt3DExtras::QPhongMaterial; mat->setDiffuse(color); auto *tr = new Qt3DCore::QTransform; tr->setTranslation(pos); tr->setScale(scale); entity->addComponent(mesh); entity->addComponent(mat); entity->addComponent(tr); return entity; } int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); auto *view = new Qt3DExtras::Qt3DWindow; view->setTitle("综合场景:太阳系简化模型"); view->resize(900, 600); auto *root = new Qt3DCore::QEntity; // === 相机 === view->camera()->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); view->camera()->setPosition(QVector3D(0, 30, -50)); view->camera()->setViewCenter(QVector3D(0, 0, 0)); auto *camCtrl = new Qt3DExtras::QOrbitCameraController(root); camCtrl->setCamera(view->camera()); // === 光照 === auto *sun = new Qt3DRender::QDirectionalLight(root); sun->setWorldDirection(QVector3D(-1, -1, -1).normalized()); sun->setIntensity(0.9f); // === 太阳(中心球体)=== auto *sunMesh = new Qt3DExtras::QSphereMesh; sunMesh->setRadius(5.0f); auto *sunEntity = createObject(root, sunMesh, QColor("#f59e0b"), QVector3D(0, 0, 0)); // === 地球(绕太阳旋转的球体)=== auto *earthMesh = new Qt3DExtras::QSphereMesh; earthMesh->setRadius(1.5f); auto *earthEntity = createObject(root, earthMesh, QColor("#3b82f6"), QVector3D(15, 0, 0)); // 地球公转:让一个父 Entity 绕 Y 轴旋转,地球挂在其下 auto *earthOrbit = new Qt3DCore::QEntity(root); auto *orbitTransform = new Qt3DCore::QTransform; earthOrbit->addComponent(orbitTransform); earthEntity->setParent(earthOrbit); // 地球变成公转节点的子节点 auto *orbitAnim = new QPropertyAnimation(orbitTransform, "rotation"); orbitAnim->setDuration(8000); orbitAnim->setStartValue(QQuaternion::fromEulerAngles(0, 0, 0)); orbitAnim->setEndValue(QQuaternion::fromEulerAngles(0, 360, 0)); orbitAnim->setLoopCount(-1); orbitAnim->start(); // === 地面 === auto *planeMesh = new Qt3DExtras::QPlaneMesh; planeMesh->setWidth(50); planeMesh->setHeight(50); createObject(root, planeMesh, QColor("#1a1a2e"), QVector3D(0, -5, 0)); view->setRootEntity(root); view->show(); return app.exec(); }
· 场景树层级:earthOrbit (父) → earthEntity (子),父节点旋转 = 子节点公转
· 辅助函数:createObject() 封装 Entity + Mesh + Material + Transform
· 多光源:方向光 + 环境光
· 多动画:太阳自转 + 地球公转(通过父子层级实现)
· 轨道相机:鼠标拖拽从任意角度观察场景
十六、调试技巧与常见问题
16.1 黑屏排查清单
1. 没有光源:QPhongMaterial 需要光照,添加 QDirectionalLight
2. 相机位置不对:相机可能太远或看向错误方向,检查 position 和 viewCenter
3. 物体太大/太小:检查 Mesh 尺寸和 Transform 的 Scale
4. 模型路径错误:QMesh 的 setSource 路径是否正确
5. 组件未挂载:确认 Entity 有 Mesh + Material + Transform 三个组件
16.2 场景树可视化
Qt3D 的场景树是隐式的(通过 QObject 父子关系构建),没有直接的可视化工具。但你可以用代码打印场景树:
void dumpScene(QObject *node, int depth = 0) { QString indent(depth * 2, ' '); qDebug() << indent << node->metaObject()->className(); for (auto *child : node->children()) dumpScene(child, depth + 1); } // 调用 dumpScene(rootEntity); // 输出示例: // QEntity // QEntity // QCuboidMesh // QPhongMaterial // QTransform // QDirectionalLight // QOrbitCameraController
16.3 性能优化建议
| 优化项 | 方法 | 效果 |
|---|---|---|
| 减少 Draw Call | 合并相同材质的 Mesh | 每帧绘制次数减少 |
| 降低面数 | 球体的 rings/slices 不要太高 | 顶点数减少 |
| 视锥裁剪 | 远裁面不要设太大 | 远处物体不渲染 |
| 纹理压缩 | 用 ASTC/ETC 格式代替 PNG | 显存占用减少 |
| 实例化渲染 | 相同 Mesh 用 QInstancedRendering | 大量相同物体高效渲染 |
十七、总结与学习路线
本文知识点回顾
- 3D 图形基础:坐标系统、顶点/三角面/材质、变换矩阵
- 渲染管线:顶点数据 → 顶点着色 → 图元组装 → 片元着色 → 屏幕
- ECS 架构:Entity(空壳)+ Component(数据)+ System(逻辑)
- Qt3D 项目搭建:CMake/qmake 配置、Qt3DWindow 骨架
- 第一个场景:立方体 + 材质 + 变换 + 动画 = 旋转立方体
- Mesh 与 Material:内置几何体、Phong 材质参数
- 相机与光照:透视投影、三种光源、轨道相机
- 交互:QOrbitCameraController、QKeyboard
- 模型加载:OBJ/FBX/glTF 加载方式
- 动画:QPropertyAnimation、QFrameAction
- 综合实战:多物体场景、父子层级动画
进阶学习路线
· Qt 官方 Qt3D 示例:Qt Creator → 示例 → 搜索 "3D"
· 本博客 《Qt3D + URDF 六轴机械臂仿真建模与控制实战》 —— 项目级实战
· LearnOpenGL.com(虽然用 C++/OpenGL,但 3D 概念通用)
· Qt3D 源码:
qt/qt3d 模块
3D 图形开发的学习曲线确实比 2D 陡,但核心概念并不多:坐标系、变换、ECS、Mesh + Material + Transform。
掌握本文的内容后,你已经具备了用 Qt3D 开发 3D 可视化应用的能力。剩下的就是通过项目实践来巩固和深化。