Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

Qt 与可视化 / Qt & Visualization

1. Qt 6 与科学可视化路线 / Qt 6 and Scientific Visualization Roadmap

2. 信号槽机制深度解析 / Qt Signals and Slots in Depth

3. 对象树与内存管理 / QObject Trees and Memory Management

4. CAE开发常用核心API实战 / Essential Qt APIs for CAE Development

5. Model/View架构与CAE数据处理 / Model-View Architecture and CAE Data Processing

6. 多线程与求解器集成 / Multithreading and Solver Integration

7. CAE软件架构实战模式 / Practical Architecture Patterns for CAE Software

8. 3D可视化与交互 / Interactive 3D Visualization

9. 实战案例:从零拆解一个FEA前后处理器 / Building an FEA Pre- and Post-Processor from Scratch

本页目录

3D可视化与交互 / Interactive 3D Visualization ​

📅 创建时间:2026-07-13 🏷️ 标签:#Qt #OpenGL #VTK #3D可视化 #CAE #渲染 #视口交互 📚 前置知识:Qt Cpp Overview、Core Api Essentials


📋 本章目标 ​

  • 理解QOpenGLWidget在CAE视口中的基础角色
  • 掌握Qt + VTK的集成方案——QVTKOpenGLNativeWidget
  • 掌握轨迹球旋转/平移/缩放的交互实现
  • 掌握3D视口拾取(节点/单元选择)的完整流程
  • 理解LOD和VBO的性能优化策略
  • 掌握色标映射和图例渲染
  • 能够为CAE应用设计交互式3D可视化模块

专题扩展 ​

  • OpenGL/VTK 资源生命周期与性能

第1部分:QOpenGLWidget基础 ​

1.1 最小OpenGL视口 ​

cpp
class MeshViewport : public QOpenGLWidget, protected QOpenGLFunctions {
    Q_OBJECT
public:
    explicit MeshViewport(QWidget* parent = nullptr)
        : QOpenGLWidget(parent) {}

protected:
    void initializeGL() override {
        initializeOpenGLFunctions();  // ★ Qt5/6 都需要这一行
        glClearColor(0.15f, 0.15f, 0.15f, 1.0f);  // 深灰背景
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_LIGHTING);
        glEnable(GL_LIGHT0);
    }

    void resizeGL(int w, int h) override {
        glViewport(0, 0, w, h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        // 透视投影
        gluPerspective(45.0, double(w)/h, 0.01, 1000.0);
        glMatrixMode(GL_MODELVIEW);
    }

    void paintGL() override {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glLoadIdentity();
        gluLookAt(0, 0, 5,  0, 0, 0,  0, 1, 0);

        // 绘制网格线框
        glColor3f(0.0, 0.8, 0.4);
        glBegin(GL_LINES);
        for (const auto& edge : m_edges) {
            glVertex3d(edge.p1.x, edge.p1.y, edge.p1.z);
            glVertex3d(edge.p2.x, edge.p2.y, edge.p2.z);
        }
        glEnd();
    }

private:
    std::vector<Edge> m_edges;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

1.2 Qt6迁移注意事项 ​

┌─────────────────────────────────────────────────────────────┐
│            QGLWidget → QOpenGLWidget 迁移                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Qt5 QGLWidget:                                             │
│  ┌──────────────────────────────────────────────────┐      │
│  │ class MeshViewport : public QGLWidget {          │      │
│  │     void initializeGL() override;               │      │
│  │    不需要 initializeOpenGLFunctions();           │      │
│  │ };                                                │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  Qt6 QOpenGLWidget:                                         │
│  ┌──────────────────────────────────────────────────┐      │
│  │ class MeshViewport : public QOpenGLWidget,       │      │
│  │     protected QOpenGLFunctions {                 │      │
│  │     void initializeGL() override {               │      │
│  │         initializeOpenGLFunctions();  // ★ 必须 │      │
│  │     }                                             │      │
│  │ };                                                │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  CMakeLists.txt:                                            │
│  Qt5: find_package(Qt5 REQUIRED COMPONENTS Widgets)         │
│       → QGLWidget 自动可用                                  │
│  Qt6: find_package(Qt6 REQUIRED COMPONENTS                  │
│                    Widgets OpenGLWidgets)                    │
│       → 必须显式链接 Qt6::OpenGLWidgets                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

第2部分:Qt + VTK集成 ​

2.1 为什么CAE用VTK ​

┌─────────────────────────────────────────────────────────────┐
│              原生OpenGL vs VTK                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原生OpenGL:                                                │
│  ┌──────────────────────────────────────────────────┐      │
│  │ ✅ 完全控制渲染管线                                │      │
│  │ ✅ 轻量级,无额外依赖                              │      │
│  │ ❌ 没有现成的等值面/切面/流线/标量条               │      │
│  │ ❌ 百万级数据每个渲染功能都要手写                   │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  VTK (Visualization Toolkit):                               │
│  ┌──────────────────────────────────────────────────┐      │
│  │ ✅ 等值面/切面/流线/体渲染 → 一行代码             │      │
│  │ ✅ 70+ 文件格式读取 (VTK/VTU/Exodus/XDMF...)      │      │
│  │ ✅ Pipeline 架构,可组合的过滤器链                 │      │
│  │ ✅ 并行渲染支持 (vtkPGenericAdaptor)              │      │
│  │ ❌ 依赖与部署体积较大(随构建模块和平台而变化)      │      │
│  │ ❌ 学习曲线(Pipeline概念与命令式OpenGL完全不同)  │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  结论:自研CAE工具 → VTK是标准选择                         │
│        只有最简原型或游戏引擎级渲染才用原生OpenGL           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

2.2 QVTKOpenGLNativeWidget —— 在Qt中嵌入VTK ​

cpp
// CMakeLists.txt
// find_package(VTK REQUIRED COMPONENTS GUISupportQt)
// target_link_libraries(fastcae PRIVATE VTK::GUISupportQt)

#include <QVTKOpenGLNativeWidget.h>
#include <vtkActor.h>
#include <vtkPolyDataMapper.h>
#include <vtkRenderer.h>
#include <vtkGenericOpenGLRenderWindow.h>

class VtkMeshView : public QVTKOpenGLNativeWidget {
public:
    VtkMeshView(QWidget* parent = nullptr)
        : QVTKOpenGLNativeWidget(parent)
    {
        // 创建VTK渲染管线
        m_renderer = vtkSmartPointer<vtkRenderer>::New();
        m_renderer->SetBackground(0.15, 0.15, 0.15);

        // setRenderWindow 是 QVTKOpenGLNativeWidget 的方法
        auto rw = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
        setRenderWindow(rw);
        rw->AddRenderer(m_renderer);
    }

    // 显示网格
    void setMesh(vtkSmartPointer<vtkPolyData> mesh) {
        m_renderer->RemoveAllViewProps();  // 清除旧actor

        auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
        mapper->SetInputData(mesh);

        auto actor = vtkSmartPointer<vtkActor>::New();
        actor->SetMapper(mapper);
        actor->GetProperty()->SetRepresentationToWireframe();
        actor->GetProperty()->SetColor(0.0, 0.8, 0.4);

        m_renderer->AddActor(actor);
        m_renderer->ResetCamera();
        update();  // 触发重绘
    }

    // 标量场云图
    void showScalarField(vtkSmartPointer<vtkPolyData> mesh,
                         const QString& fieldName)
    {
        mesh->GetPointData()->SetActiveScalars(
            fieldName.toUtf8().constData());

        auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
        mapper->SetInputData(mesh);
        mapper->SetScalarRange(
            mesh->GetPointData()->GetScalars()->GetRange());  // auto range

        auto actor = vtkSmartPointer<vtkActor>::New();
        actor->SetMapper(mapper);

        // 添加标量条
        auto scalarBar = vtkSmartPointer<vtkScalarBarActor>::New();
        scalarBar->SetLookupTable(mapper->GetLookupTable());
        scalarBar->SetTitle(fieldName.toUtf8().constData());

        m_renderer->RemoveAllViewProps();
        m_renderer->AddActor(actor);
        m_renderer->AddActor2D(scalarBar);

        update();
    }

private:
    vtkSmartPointer<vtkRenderer> m_renderer;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

第3部分:鼠标交互——轨迹球 ​

3.1 轨迹球旋转数学 ​

cpp
class TrackballCamera {
public:
    void mousePress(const QPointF& pos) {
        m_lastPos = pos;
        m_tracking = true;
    }

    void mouseMove(const QPointF& pos, int viewportWidth, int viewportHeight) {
        if (!m_tracking) return;

        QPointF delta = pos - m_lastPos;
        m_lastPos = pos;

        // 旋转(左键拖动)
        if (m_mode == Rotate) {
            // 将屏幕坐标映射到球面
            QVector3D lastOnSphere = toSphere(
                m_lastPos.x()/viewportWidth,
                m_lastPos.y()/viewportHeight);
            QVector3D currOnSphere = toSphere(
                pos.x()/viewportWidth,
                pos.y()/viewportHeight);

            // 两个球面向量的叉积 = 旋转轴
            QVector3D axis = QVector3D::crossProduct(
                lastOnSphere, currOnSphere).normalized();
            // 点积 = 旋转角
            float angle = acos(QVector3D::dotProduct(
                lastOnSphere, currOnSphere));

            m_rotation = QQuaternion::fromAxisAndAngle(axis,
                qRadiansToDegrees(angle)) * m_rotation;
        }

        // 平移(中键拖动)
        if (m_mode == Pan) {
            m_translation += QVector3D(
                delta.x() * m_panSpeed,
               -delta.y() * m_panSpeed, 0);
        }

        // 缩放(滚轮)
        m_zoom *= (1.0 + delta.y() * m_zoomSpeed);
    }

    QMatrix4x4 viewMatrix() const {
        QMatrix4x4 view;
        view.translate(m_translation);
        view.rotate(m_rotation);
        view.translate(0, 0, -m_zoom);
        return view;
    }

private:
    QVector3D toSphere(float x, float y) const {
        // 将屏幕坐标(x,y)映射到虚构的球面上
        float xm = 2.0f * x - 1.0f;  // [0,1] → [-1,1]
        float ym = 1.0f - 2.0f * y;  // [0,1] → [1,-1]
        float r2 = xm*xm + ym*ym;

        if (r2 <= 1.0f) {
            return {xm, ym, sqrt(1.0f - r2)};   // 在球面上
        }
        // 在球外
        float norm = 1.0f / sqrt(r2);
        return {xm*norm, ym*norm, 0.0f};  // 在赤道圈上
    }

    QPointF m_lastPos;
    bool m_tracking = false;
    int m_mode = Rotate;
    QQuaternion m_rotation;
    QVector3D m_translation;
    float m_zoom = 5.0f;
    float m_panSpeed = 0.01f;
    float m_zoomSpeed = 0.001f;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

3.2 Qt事件集成 ​

cpp
class TrackballViewport : public QOpenGLWidget {
protected:
    void mousePressEvent(QMouseEvent* e) override {
        m_camera.mousePress(e->position());
        if (e->button() == Qt::LeftButton)
            m_mode = TrackballCamera::Rotate;
        else if (e->button() == Qt::MiddleButton)
            m_mode = TrackballCamera::Pan;
    }

    void mouseMoveEvent(QMouseEvent* e) override {
        m_camera.mouseMove(e->position(), width(), height());
        update();  // 触发重绘
    }

    void wheelEvent(QWheelEvent* e) override {
        m_camera.mouseMove(
            QPointF(0, e->angleDelta().y() * 0.1),
            width(), height());
        update();
    }

    void paintGL() override {
        // 时间相机
        glLoadIdentity();
        QMatrix4x4 view = m_camera.viewMatrix();
        // 应用view矩阵...
        drawMesh();
    }

    TrackballCamera m_camera;
    int m_mode;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第4部分:视口拾取 ​

4.1 VTK拾取实现 ​

cpp
class PickableVtkView : public QVTKOpenGLNativeWidget {
public:
    void mousePressEvent(QMouseEvent* e) override {
        if (e->button() == Qt::LeftButton) {
            int x = e->position().x();
            int y = e->position().y();

            // ★ VTK拾取
            auto picker = vtkSmartPointer<vtkPointPicker>::New();
            picker->Pick(x, height() - y, 0, m_renderer);

            vtkIdType pointId = picker->GetPointId();
            if (pointId >= 0) {
                double* worldPos = picker->GetPickPosition();
                emit nodePicked(pointId,
                    QVector3D(worldPos[0], worldPos[1], worldPos[2]));
            }
        }
        QVTKOpenGLNativeWidget::mousePressEvent(e);
    }

signals:
    void nodePicked(int nodeId, QVector3D worldPosition);

private:
    vtkSmartPointer<vtkRenderer> m_renderer;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

4.2 拾取反馈链——选中节点→高亮→更新属性面板 ​

cpp
// 连接拾取信号
connect(viewport, &PickableVtkView::nodePicked, this,
    [this](int nodeId, QVector3D pos) {
        // 1. 高亮选中节点(改变颜色/尺寸)
        viewport->highlightNode(nodeId);

        // 2. 更新属性面板
        m_propertyPanel->showNodeInfo(nodeId, pos);

        // 3. 状态栏提示
        m_statusBar->showMessage(
            QString("Node %1 selected").arg(nodeId));
    });
1
2
3
4
5
6
7
8
9
10
11
12
13

第5部分:性能优化 ​

5.1 VBO(顶点缓冲对象) ​

cpp
// 使用VBO避免每帧传输百万顶点到GPU
class VBOMeshView : public QOpenGLWidget, protected QOpenGLFunctions {
protected:
    void initializeGL() override {
        initializeOpenGLFunctions();
        m_vbo.create();
        m_vbo.bind();
        m_vbo.allocate(meshVertexData, vertexCount * sizeof(float));
        m_vbo.release();
    }

    void paintGL() override {
        m_vbo.bind();
        glEnableClientState(GL_VERTEX_ARRAY);
        glVertexPointer(3, GL_FLOAT, 0, nullptr);
        glDrawArrays(GL_TRIANGLES, 0, vertexCount);
        m_vbo.release();
    }

    QOpenGLBuffer m_vbo;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

5.2 VTK LOD ​

cpp
// VTK自动LOD
auto lod = vtkSmartPointer<vtkLODActor>::New();
lod->SetNumberOfCloudPoints(10000);  // 远距离 → 点云

auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputData(fineMesh);

lod->SetLowResFilter(mapper->GetInputAlgorithm());
lod->AddLODMapper(mapper);
1
2
3
4
5
6
7
8
9

核心总结 ​

总结1:VTK 与原生 OpenGL 面向不同抽象层 ​

VTK 提供科学数据模型、过滤器和可视化管线,常能显著缩短等值面、切面、流线和拾取功能的开发周期;原生 OpenGL 更适合需要完全控制渲染、已有引擎或极致定制的场景。选择还要考虑部署体积、启动时间、GPU 特性、VTK 版本兼容和团队经验,依赖大小不是固定值。

总结2:轨迹球的本质 ​

屏幕坐标 → 球面投影 → 两个球面向量的差 = 旋转。核心是toSphere()将2D鼠标坐标映射到虚构3D球面上,两个frame之间的四元数乘积。

总结3:拾取流程 ​

鼠标点击 → vtkPointPicker::Pick(x, y) → 获取pointId + worldPos
  → emit nodePicked(id, pos)
  → 高亮渲染 + 属性面板更新 + 状态栏提示
1
2
3

章节测试 ​

测试1:OpenGL Widget ​

Qt6中QOpenGLWidget与QGLWidget的区别是什么? A. 没有区别,只是重命名 B. Qt6中QOpenGLWidget位于OpenGLWidgets模块;若通过QOpenGLFunctions调用函数,需要初始化对应函数封装 C. QOpenGLWidget只支持OpenGL ES D. QGLWidget性能更好

测试2:VTK集成 ​

QVTKOpenGLNativeWidget相比QOpenGLWidget+手动OpenGL的优点是什么? A. 体积更小 B. 自带等值面/切面/流线/标量条/70+格式读取 C. 渲染性能比手写OpenGL高很多 D. 不需要写着色器

测试3:视口拾取 ​

视口拾取选中节点后的标准响应流程是? A. 弹出对话框 B. 删除节点 C. 高亮渲染 + 更新属性面板 + 状态栏提示 D. 重新求解


参考答案 ​

测试1答案 ​

答案:B。Qt 6 的 QOpenGLWidget 位于 OpenGLWidgets 模块,通常链接 Qt6::OpenGLWidgets。继承 QOpenGLFunctions 是获取跨平台函数封装的一种方式,并非使用 QOpenGLWidget 的语言级强制要求;若采用该封装,应在当前 Context 有效时初始化。

测试2答案 ​

答案:B。VTK的核心价值就是科学可视化管线——等值面、切面、流线、标量条、70+格式全是开箱即用的。

测试3答案 ​

答案:C。标准拾取反馈:高亮选中的几何体(改变颜色/加粗/发光),属性面板显示该节点的坐标和场量值,状态栏显示Node ID等信息。


相关笔记 ​

  • Core Api Essentials - QTimer(动画帧驱动)
  • Qt Cpp Overview - Qt模块总览
  • Real Cae Case Study - 实战案例(视口与求解的集成)

下一步学习 ​

  • [ ] 阅读 08 - 实战案例:FEA前后处理器 —— 全部知识串联
  • [ ] 尝试用VTK Pipeline显示你自己的网格数据
  • [ ] 研究FreeCAD的视口实现(基于Coin3D/OpenInventor)

学习状态:🟡 开始学习

从 Document 到一帧的资源流 ​

text
Document revision
 -> extract render data / LOD
 -> upload/staging buffers
 -> scene graph/draw list
 -> command submission fence N
 -> present
 -> deferred deletion after fence completion
1
2
3
4
5
6
7

CPU scene revision、GPU resource generation 和窗口 surface generation 都应显式,旧异步上传不能写入重建后的 renderer。

坐标与精度 ​

CAE 模型可能尺度巨大,直接把世界坐标 float 上传会丢精度。常见 camera-relative/origin rebasing:

text
double world position - double local origin -> float GPU local position
1

拾取结果再结合 origin 转回领域坐标;单位、坐标系和矩阵乘法约定必须统一测试。

面试连续追问 ​

问:为什么资源删除需要 fence? 答:提交是异步的,CPU 返回时 GPU 可能仍引用 buffer/texture;fence 证明相关命令完成。

问:为何不能每帧重建所有 mesh buffer? 答:产生分配、上传带宽与同步,应用按 revision 缓存并增量更新。

问:Picking 为什么常异步? 答:GPU readback 同步会让 CPU 等 GPU,异步 staging 可保持流水线但结果需 generation 校验。

自测与答案 ​

  1. device lost 后权威数据在哪? 答:Document/domain,GPU cache 可重建。
  2. LOD 选择应影响 Document 吗? 答:通常是视图/渲染策略,不修改权威几何。
  3. 窗口关闭先销毁 context 可以吗? 答:不行,应先停止提交并清理依赖资源/在途工作。

最后更新于:

Pager
上一篇7. CAE软件架构实战模式 / Practical Architecture Patterns for CAE Software
下一篇9. 实战案例:从零拆解一个FEA前后处理器 / Building an FEA Pre- and Post-Processor from Scratch

持续记录,持续成长

Copyright © Tidenflow