← 返回博客列表

一、为什么要学 3D 图形开发

如果你已经熟悉 C++ 和 Qt,可能会问:我一直在做桌面应用,为什么要学 3D?

实际上,3D 图形在工业领域应用极为广泛:

为什么选 Qt3D 而不是 OpenGL / Unity / Three.js?
· Qt3D 是 Qt 原生模块,与你的 Qt 应用无缝集成,C++ 开箱即用
· OpenGL 太底层,画一个三角形要写几百行代码
· Unity / Unreal 是游戏引擎,体积大、授权复杂,不适合嵌入式 HMI
· Three.js 是 JavaScript 库,不适合 C++ 桌面/嵌入式应用
· Qt3D 在 "足够好用"和"足够轻量"之间找到了平衡点

二、3D 图形基础概念(零基础也能懂)

2.1 从 2D 到 3D:多了什么?

你在 Qt 里画 2D 图形(用 QPainter)时,世界是这样的:

2D 世界的坐标系 屏幕区域 Y↓ X→ (0,0) 原点在左上角 → 3D 世界的坐标系 X→ Y↑ Z↗(深度) (0,0,0)
2D 坐标系(左)vs 3D 坐标系(右):3D 多了一个 Z 轴表示深度

关键区别:

概念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 物体看起来很复杂,但本质上只有三个组成部分:

3D 物体的三要素 ① 顶点 (Vertex) 空间中的点 (x, y, z) 坐标 如: (1,0,0) (0,1,0)... + ② 三角面 (Triangle) 三个顶点连接 组成一个面 三角形是 3D 的最小面 = ③ 材质 (Material) 决定外观 颜色/纹理/光照 如: 红色塑料 / 金属
所有 3D 物体无论多复杂,本质上都是"顶点 + 三角面 + 材质"
通俗比喻
想象你搭一个纸板房子:顶点就是纸板的四个角;三角面就是把纸板沿对角线折一下的三角形;材质就是你在纸板上涂的颜色或贴的壁纸。

2.3 立方体有多少个三角形?

一个立方体有 6 个面,每个面是一个正方形,每个正方形可以分成 2 个三角形,所以立方体 = 6 × 2 = 12 个三角形,需要 8 个顶点(但每个顶点会被多个面共享)。

立方体的三角面分解 每个正方形面 → 2 个三角形(红色虚线为分割线) 8 个顶点 12 个三角形 36 个顶点索引 (每个三角形 3 个顶点)
正方形沿对角线切成 2 个三角形,立方体 6 面共 12 个三角形

三、坐标系统:3D 世界的"地图规则"

3.1 右手坐标系

Qt3D 使用的是 右手坐标系(与 OpenGL 一致)。判断方法:伸出右手,手指从 X 轴弯向 Y 轴,拇指方向就是 Z 轴。

右手坐标系 Y(上) X(右) Z(前/朝向观察者) 原点 (0,0,0)
右手坐标系:X 向右、Y 向上、Z 朝向观察者(屏幕外)
记忆口诀
X 向右走,Y 往上爬,Z 朝你脸上来。这是 OpenGL / Qt3D 的惯例,也是大多数 3D 引擎的标准。

3.2 多层坐标系:模型坐标系 → 世界坐标系 → 相机坐标系

3D 渲染中存在多个坐标系层级,理解它们是掌握 3D 的关键:

坐标变换流水线 模型坐标系 Local Space 物体的"自带" 坐标系 原点在物体 几何中心 如: 立方体 顶点 ±1 → 模型变换 世界坐标系 World Space 整个场景的 统一坐标系 所有物体放到 同一空间 如: 物体在 (5, 0, 3) → 视图变换 相机坐标系 View/Camera Space 以相机为原点 的坐标系 相机看向 -Z 方向 只渲染相机 看到的 → 投影变换 屏幕坐标系 Screen Space 2D 像素坐标 3D → 2D 显示到屏幕 如: (800, 600)
3D 渲染的坐标变换流水线:每个阶段都是一次矩阵乘法
通俗理解多层坐标系
· 模型坐标系:你在建模软件里画一个立方体,原点在立方体中心——这是"物体自己的坐标"
· 世界坐标系:把立方体放到场景里,它移动到了 (5, 0, 3)——这是"在世界中的位置"
· 相机坐标系:你的眼睛(相机)站在某处看这个立方体,从相机视角看到的坐标
· 屏幕坐标系:最终投影到 2D 屏幕上的像素位置

四、变换矩阵:让物体动起来的数学魔法

4.1 三种基本变换

3D 中物体有三种基本变换:平移(Translation)、旋转(Rotation)、缩放(Scale)。在 Qt3D 中,这些变换通过 QTransform 组件实现。

三种基本变换 平移 Translation 物体位置改变 旋转 Rotation 绕轴旋转角度 缩放 Scale 物体大小改变
平移、旋转、缩放是 3D 变换的三板斧

4.2 4x4 矩阵:为什么不是 3x3?

你可能会想:3D 坐标是 (x, y, z) 三个值,为什么变换矩阵是 4x4 而不是 3x3?

因为 3x3 矩阵可以表示旋转和缩放,但无法表示平移。为了让一个矩阵能同时包含平移、旋转和缩放,我们引入了"齐次坐标"——在 (x, y, z) 后面加上一个 w=1,变成 (x, y, z, 1)。

4x4 变换矩阵的结构
┌ 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 数据变成屏幕像素的完整流程。你不需要理解每个细节,但需要知道大致流程:

简化版 3D 渲染管线 顶点数据 (x,y,z) 坐标 颜色/法线 → 顶点着色 模型→世界 →相机 变换 → 图元组装 顶点组成 三角形 → 片元着色 计算每个 像素颜色 → 输出到屏幕 深度测试 写入帧缓冲 着色器 (Shader) 介入的两个阶段 顶点着色器 决定顶点最终位置 片元着色器 决定像素最终颜色
渲染管线:3D 数据经过一系列步骤变成屏幕上的像素
着色器 (Shader) 是什么?
着色器是运行在 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,实体-组件-系统)。

维度传统 OOPECS(Qt3D)
基本单位类对象(数据+行为合一)Entity(空壳)+ Component(数据)
行为在哪里在类的方法里在 System(系统/后端)里
组合方式继承(is-a)组件挂载(has-a)
类比一个人有名字、会走路一个空壳人 + "名字组件" + "走路组件"

6.2 Entity 和 Component 的关系

Entity-Component 架构图 Entity(实体) 一个空壳,只有一个 ID 本身没有任何数据和行为 QMesh 几何形状数据 立方体/球体/Mesh文件 QTransform 位置/旋转/缩放 决定物体在场景中的变换 QMaterial 材质/外观 颜色/光照/纹理 QLight 光源 环境光/点光/聚光灯 ↑ 组件可以自由组合,像搭积木一样 Entity + Mesh + Transform + Material = 一个可见的 3D 物体
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) 位置
ECS 的核心优势
· 组件可以自由组合:同一个 Mesh 换个 Material 就变成另一个外观
· 组件可以复用:同一个 Material 可以挂到多个 Entity 上
· 数据与逻辑分离:System 负责处理逻辑,Component 只存数据

七、搭建第一个 Qt3D 项目

1安装 Qt3D 模块

在使用 Qt 在线安装器时,勾选 Qt 3D 模块。如果已安装 Qt,可以在 Qt Maintenance Tool 中追加安装。

2在 .pro 文件中添加模块
# .pro 文件
QT += 3dcore 3drender 3dinput 3dlogic 3dextras 3danimation

# 各模块含义:
# 3dcore    — 核心类:QEntity, QComponent, QTransform
# 3drender  — 渲染:QViewport, QCamera, QRenderSettings
# 3dinput   — 输入:键盘鼠标的 3D 导航
# 3dlogic   — 逻辑:帧更新(QFrameAction)
# 3dextras  — 预定义几何体和材质(立方体、球体、Phong 材质等)
# 3danimation — 动画系统
3CMake 写法(Qt6 推荐)
# 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
)
4最小可运行项目骨架
// 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 窗口——这只是一个空场景。接下来我们往里添加物体。

Qt3D 程序的基本结构 Qt3DWindow (3D 渲染窗口) └─ Camera (相机 — 从哪里看) └─ RootEntity (根实体 — 场景树根节点) ├─ Entity (立方体) ← Mesh + Transform + Material └─ Entity (光源) ← Light 组件
Qt3D 程序的基本层级结构

八、第一个 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:将场景树根节点设置给窗口,开始渲染
旋转立方体场景示意图 Y轴旋转 光源 地面参考线 相机在 (5,5,-5) 看向原点
蓝色立方体绕 Y 轴旋转,相机在斜上方观察

九、深入理解 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
QExtrudedTextMesh3D 文字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);
Qt3D 内置几何体一览 Cuboid Sphere Cylinder Cone Torus Plane
Qt3DExtras 提供的 6 种常用内置几何体

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) ──┐
// │   光线均匀散射       │  │   集中反射形成高光       │
// │   决定物体"本色"     │  │   决定物体"光泽"          │
// └──────────────────────┘  └──────────────────────────┘
//     ↓                            ↓
//   哑光塑料                     金属表面
不同材质参数对比 shininess=0 哑光 shininess=0.5 半光泽 shininess=1.0 高光泽
光泽度从 0 到 1:从哑光到金属质感

十、相机与光照:让场景"活"起来

10.1 相机:你的眼睛

3D 场景中如果没有光源,你什么也看不到(全部是黑的)。相机决定了你从哪个角度观察场景。

相机参数示意图 物体 相机 看向 ViewCenter Up 向量 Position 近平面 ────────── 远平面(视锥体 Frustum)
相机的关键参数:位置、看向哪里、向上方向、视锥体
// 相机设置详解
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(视场角)的理解
· FOV = 45°:标准视角,类似于人眼
· FOV = 90°:广角镜头,看到更多但物体显得远
· FOV = 20°:长焦镜头,物体显得大但视野窄

10.2 光照:让物体可见

没有光源,3D 场景就是一片漆黑。Qt3D 提供了三种光源:

三种光源类型 环境光 (Ambient) 均匀照亮所有面 没有方向性 QEnvironmentLight 点光源 (Point Light) QPointLight 方向光 (Directional) 平行光线 模拟太阳光 QDirectionalLight
环境光(均匀)、点光源(放射)、方向光(平行)
// 方向光(模拟太阳光)—— 最常用
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左键拖拽=旋转视角,右键拖拽=平移,滚轮=缩放物体居中展示(产品展示、机械臂预览)
QFirstPersonCameraControllerWASD=前后左右移动,鼠标=看向哪第一人称视角(场景漫游)
// 轨道相机控制器(最常用)
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(deltaTime)是什么?
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 支持的模型格式

格式来源特点
.objWavefront最简单通用,不含动画,文本格式
.fbxAutodesk含动画、材质、骨骼,业界标准
.glTF / .gltfKhronos现代标准,体积小、效率高
.daeCOLLADAXML 格式,含动画
.stl3D 打印仅几何体,无颜色/材质
.plyStanford含顶点颜色,学术常用

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 动画类型一览

三种动画驱动方式 QPropertyAnimation 起点→终点,匀速/缓动 简单变换动画 适合: 简单位移/旋转 QAnimationClip 关键帧动画 适合: 角色骨骼动画 QFrameAction 逐帧回调 自定义逻辑 适合: 复杂自定义动画
三种动画方式:属性动画、关键帧动画、逐帧回调

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 vs QTimer 的区别
· 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 黑屏排查清单

3D 场景黑屏的 5 个常见原因
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大量相同物体高效渲染

十七、总结与学习路线

本文知识点回顾

  1. 3D 图形基础:坐标系统、顶点/三角面/材质、变换矩阵
  2. 渲染管线:顶点数据 → 顶点着色 → 图元组装 → 片元着色 → 屏幕
  3. ECS 架构:Entity(空壳)+ Component(数据)+ System(逻辑)
  4. Qt3D 项目搭建:CMake/qmake 配置、Qt3DWindow 骨架
  5. 第一个场景:立方体 + 材质 + 变换 + 动画 = 旋转立方体
  6. Mesh 与 Material:内置几何体、Phong 材质参数
  7. 相机与光照:透视投影、三种光源、轨道相机
  8. 交互:QOrbitCameraController、QKeyboard
  9. 模型加载:OBJ/FBX/glTF 加载方式
  10. 动画:QPropertyAnimation、QFrameAction
  11. 综合实战:多物体场景、父子层级动画

进阶学习路线

Qt3D 学习路线图 入门 本文涵盖内容 ECS+Mesh+光照 → 进阶 自定义着色器 多 Pass 渲染 纹理与法线贴图 → 高级 FrameGraph 后处理特效 阴影/雾效 → 实战项目 机械臂仿真 数字孪生 3HMI 可视化 ↓ 掌握本文 ↓ 学习 GLSL ↓ 研究源码 ↓ 做项目 推荐下一步: 阅读本博客《Qt3D + URDF 六轴机械臂仿真》进行项目实战
从入门到实战的学习路线
推荐学习资源
· Qt 官方 Qt3D 示例:Qt Creator → 示例 → 搜索 "3D"
· 本博客 《Qt3D + URDF 六轴机械臂仿真建模与控制实战》 —— 项目级实战
· LearnOpenGL.com(虽然用 C++/OpenGL,但 3D 概念通用)
· Qt3D 源码:qt/qt3d 模块
最后的话
3D 图形开发的学习曲线确实比 2D 陡,但核心概念并不多:坐标系、变换、ECS、Mesh + Material + Transform。
掌握本文的内容后,你已经具备了用 Qt3D 开发 3D 可视化应用的能力。剩下的就是通过项目实践来巩固和深化。