1. 1. CMake 与 VTK:第三方库的管理与集成
    1. 1.1. 如何处理第三方库
      1. 1.1.1. 小型第三方库
        1. 1.1.1.1. Eigen (Header-only)
        2. 1.1.1.2. GoogleTest (需要编译的小型 CMake 项目)
        3. 1.1.1.3. Pybind11 (Header-only + CMake 支持)
          1. 1.1.1.3.1. 使用 add_subdirectory()
          2. 1.1.1.3.2. 使用 find_package()
        4. 1.1.1.4. 什么叫安装(install)一个库
      2. 1.1.2. 大型第三方库
      3. 1.1.3. 小结
    2. 1.2. VTK的安装和使用
      1. 1.2.1. VTK库的安装
        1. 1.2.1.1. vcpkg 安装
        2. 1.2.1.2. 源代码编译
        3. 1.2.1.3. 可执行文件与 VTK 动态库
      2. 1.2.2. VTK库的使用
        1. 1.2.2.1. VTK 模块
          1. 1.2.2.1.1. 数据处理相关模块(VTK Filters 系列,负责数据过滤和转换)
          2. 1.2.2.1.2. 渲染和可视化相关模块(VTK Rendering 系列,负责渲染)
          3. 1.2.2.1.3. 交互和 UI 相关模块(VTK Interaction 系列,用户交互)
          4. 1.2.2.1.4. 计算与数学相关模块(VTK Common 系列,数学和数据结构)
          5. 1.2.2.1.5. 体数据与医学影像(VTK Imaging 系列,图像处理)
          6. 1.2.2.1.6. I/O 数据读取与写入(VTK IO 系列,数据输入输出)
          7. 1.2.2.1.7. 物理仿真和计算(VTK Physics 和 Parallel 系列,物理仿真和并行计算)
          8. 1.2.2.1.8. 科学可视化(VTK Views 和 Charts 系列)
        2. 1.2.2.2. VTK 数据结构和智能指针
          1. 1.2.2.2.1. 1. 表面几何数据(vtkPolyData)
          2. 1.2.2.2.2. 2. 体数据(vtkImageData)
          3. 1.2.2.2.3. 3. 结构化网格(vtkStructuredGrid, vtkRectilinearGrid)
          4. 1.2.2.2.4. 4. 非结构化网格(vtkUnstructuredGrid)
        3. 1.2.2.3. VTK 可视化管线
          1. 1.2.2.3.1. 1. 数据源(Source)
          2. 1.2.2.3.2. 2. 数据过滤(Filter)
          3. 1.2.2.3.3. 3. 数据映射(Mapper)
          4. 1.2.2.3.4. 4. 渲染管理(Renderer)
          5. 1.2.2.3.5. 5. 交互控制(Interactor)
        4. 1.2.2.4. 绘制一个立方体

CMake 与 VTK:第三方库的管理与集成

CMake 与 VTK:第三方库的管理与集成

VTK是一个开源的C++库(https://github.com/Kitware/VTK),可以用于三维图形渲染和可视化。科研领域常用的开源可视化软件 ParaView 的底层就是 VTK,很多开源 CAE 软件的建模和后处理也是基于 VTK 库进行的开发。假如我们想做一个需要附带三维建模仿真可视化的应用,一个好的选择就是把 VTK 库集成到自己的项目中,利用 VTK 的功能去实现可视化的部分。这篇文章首先整理一下在 C++ 项目中处理第三方库的一些方式,之后整理一下 VTK 库的安装和简单使用。

如何处理第三方库

在自己的 C++ 项目中,总会遇到需要使用第三方库的情况。比如用 GoogleTest 做测试,用 Eigen 做数值计算,或者用 pybind11 把 C++ 类或函数封装出一个 Python 接口。假如我们用 CMake 构建整个项目,不同的第三方库可以有不同的组织和集成方式。比较简单常见的有以下三种处理方式:

  1. include_directories() 适用于纯头文件库(Header-only)。
  2. add_subdirectory() 适用于小型 CMake 项目(源码可用)。
  3. find_package() 适用于已安装的库(标准 CMake 库)。

小型第三方库

对于一些小型的第三方库,比如:

  • Header-only 库:库本身只包含头文件,像 pybind11EigenfmtjsonCatch2
  • 小型 CMake 项目:库本身包含源文件,需要编译但体积较小,像 GoogleTest

对于这些小型项目,我们可以直接在项目根目录创建 external/ 目录,并用 git submodule 添加这些库:

1
git submodule add https://github.com/eigen/eigen.git external/eigen

git submodule 会自动、独立地管理这些子项目的代码库,比如我们把整个 Eigen 库放到了 external/ 目录里,我们原本的 Git 项目记录中不会突然出现一大堆新增文件变动。当我们把这些项目直接拉到自己的项目里面后,也可以有不同的处理方式,这里有三个具体的示例。

Eigen (Header-only)

Eigen 是一个矩阵运算库,实现了一系列线性代数和矩阵运算相关的算法。Eigen 库只包含头文件,不需要编译,因此可以直接 include_directories()

1
include_directories(${CMAKE_SOURCE_DIR}/external/eigen)

或者使用现代 CMake:

1
target_include_directories(my_target PRIVATE ${CMAKE_SOURCE_DIR}/external/eigen)

在这里 include_directories 是指将对应目录添加到编译器的头文件搜索路径中。CMAKE_SOURCE_DIR 是 CMake 项目的根目录,也就是最顶层的 CMakeLists.txt 所在的目录。$ 是用于引用 CMake 变量的符号。target_include_directories 就是只对特定目标才将目录添加到编译器的头文件搜索路径中。这里我之前会有一些关于编译时路径和运行时路径的疑惑,但实际上忘记了 Eigen 只是一个头文件库,而仅有在编译时才需要头文件,编译后的可执行文件不依赖头文件路径。


GoogleTest (需要编译的小型 CMake 项目)

GoogleTest 是一个测试框架,它有源文件需要编译。但它的体积小,编译不会显著影响主项目,而且 CMake 结构良好,可以直接用 add_subdirectory()

1
2
3
4
5
6
add_subdirectory(external/googletest)
enable_testing()

add_executable(my_test test.cpp)
target_link_libraries(my_test PRIVATE gtest gtest_main)
add_test(NAME my_test COMMAND my_test)

add_subdirectory() 用于直接构建子项目,比如对于上面的命令,CMake 会直接进入 external/googletest 目录,执行目录中的 CMakeLists.txt。子项目中的 CMakeLists.txt 会定义子项目的 CMake 目标,比如 googletest 定义了 gtest 目标。主项目可以直接通过 target_link_libraries(my_target PRIVATE gtest) 使用生成的 gtest。CMake 会自动处理目标依赖,将 googletest 生成的 gtest 目标正确链接到主项目。


Pybind11 (Header-only + CMake 支持)

Pybind11 是一个用于将 C++ 代码封装 Python 库的库,它也是 header-only,仅由头文件组成。但是不同的是 Pybind11 提供了 CMake 目标,会额外定义一些 CMake 方法,比如通过 pybind11_add_module(my_module my_module.cpp) 可以直接将 C++ 代码自动设置 Python 绑定模块,不需要我们再去手动处理头文件等。因此,虽然它也是头文件库,但是最好使用 add_subdirectory()find_package() 来将 Pybind 集成到我们的项目中。

使用 add_subdirectory()

适用于 Pybind11 作为 Git Submodule 的情况:

1
git submodule add https://github.com/pybind/pybind11.git external/pybind11

CMakeLists.txt 中:

1
2
add_subdirectory(external/pybind11)
pybind11_add_module(my_module my_module.cpp)
使用 find_package()

CMakeLists.txt 中:

1
2
find_package(pybind11 REQUIRED)
pybind11_add_module(my_module my_module.cpp)

find_package() 主要用于查找已经安装的库,并将它们的头文件路径、库文件路径以及 CMake 目标导入当前 CMake 项目。本质上,它查找的是该库的 CMake 配置文件(通常是 <package>Config.cmakeFind<package>.cmake),并执行这个文件,从而引入库的构建信息。find_package() 的查找路径包括:

  1. 显式设置变量提供路径

    1
    2
    set(GTest_DIR /path/to/gtest)
    find_package(GTest REQUIRED)

    <package>_DIR 是一个特殊的变量,CMake 会自动把 <package> 的部分提取出来用于优先查找对应包的路径。

  2. 查找 CMAKE_PREFIX_PATHCMAKE_FRAMEWORK_PATHCMAKE_APPBUNDLE_PATHPATH

    找到根目录后,CMake 会检查这些目录下的

    1
    2
    3
    <prefix>/(lib/<arch>|lib|share)/cmake/<name>*/          (U)
    <prefix>/(lib/<arch>|lib|share)/<name>*/ (U)
    <prefix>/(lib/<arch>|lib|share)/<name>*/(cmake|CMake)/ (U)

    cmake找到这些目录后,会开始依次找 <package>Config.cmakeFind<package>.cmake 文件。找到后即可执行该文件并生成相关链接信息。

在找到了对应库的配置文件之后,CMake 通常会按照这些文件的指示生成一些变量或者 XXX::YYY 形式的 CMake 目标,比如常见的可能有

1
2
3
<Package>_INCLUDE_DIRS    # 头文件路径
<Package>_LIBRARIES # 需要链接的库文件
<Package>_FOUND # 是否找到 <Package> (ON/OFF)

XXX::YYY 这样形式的 CMake 目标是现代 CMake 的做法,现代 CMake 一般不推荐再使用传统的手动设置库文件和链接文件了。在<package>Config.cmake 中,可能会这样定义一个 CMake 目标:

1
2
3
4
5
add_library(Boost::system UNKNOWN IMPORTED)
set_target_properties(Boost::system PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "/usr/include/boost"
IMPORTED_LOCATION "/usr/lib/libboost_system.so"
)

在这里,CMake 会自动管理 Boost::system 的头文件路径和库文件路径,不需要手动 include_directories()target_include_directories()。假如我们自己的目标依赖于库的话,直接使用

1
2
find_package(Boost REQUIRED COMPONENTS system filesystem)
target_link_libraries(my_project PRIVATE Boost::system Boost::filesystem)

构建即可,CMake 会自动添加 INTERFACE_INCLUDE_DIRECTORIES(不需要 include_directories());同时自动添加 IMPORTED_LOCATION 作为链接库(不需要 target_link_libraries(my_project ${Boost_LIBRARIES}));此外,CMake 还可能自动处理 Boost 可能的额外依赖(如 boost_system 可能依赖 pthread,CMake 会自动处理)。对于一些像 VTK 这样更复杂的库,手动管理 include_directories()link_libraries() 可能会遗漏一些关键组件,但是使用 VTK::XXX 目标这样的方式则非常简单:

1
2
find_package(VTK REQUIRED)
target_link_libraries(my_project PRIVATE VTK::CommonCore VTK::RenderingCore)

当然,如果库没有提供这些目标,就需要手动设置头文件目录和需要链接的库文件了。

什么叫安装(install)一个库

在 CMake 中,安装(install())一个库指的是:

  1. 将编译好的库文件(.so / .a / .lib / .dll)复制到指定的安装路径(如 /usr/local/lib/ 等)。
  2. 将头文件复制到安装路径(如 /usr/local/include/)。
  3. 生成 packageConfig.cmake 等 CMake 配置文件,供 find_package() 查找。
  4. 清理 build/ 目录的临时文件,使项目结构更清晰。

build() 则只是编译,目录中可能包含一系列的中间文件,而且生成的目录结构可能并不是规范的。如果不执行 install() 操作,库文件会留在 build/ 目录里,无法被其他项目使用,find_package() 也无法找到编译好的库。

在 CMake 中,可以在项目根目录运行如下命令来执行 build() 操作:

1
2
3
mkdir build
cmake -G Ninja -B build .
cmake --build build -j8

在上面的命令里,新生成了 build 目录。选择 Ninja 作为构建工具,将生成的 Ninja 构建规则文件放置到 build 目录中。最后,执行 Ninja 构建,并使用8线程并行编译,将编译好的文件放置到 build 目录中。在 build() 后,则可以执行安装操作,复制必要的文件到指定路径:

1
cmake --install build --prefix /usr/local

大型第三方库

小型第三方库我们还可以直接用 git submodule 把项目直接拉取到 external/ 目录中直接构建,对于有着复杂依赖、编译时间非常长的库,这样做是不现实的,比如 VTK、OpenCV 、Qt 。这种大型项目只能自己先进行安装,然后在 CMakeLists.txt 中用 find_package() 进行查找:

1
2
3
find_package(VTK REQUIRED)
target_link_libraries(my_target PRIVATE ${VTK_LIBRARIES})
include_directories(${VTK_INCLUDE_DIRS})

当然,对于大部分流行的大型库,官方都会提供预编译库,不需要我们手动构建。或者像 Qt 这样的,官方直接提供了一个安装包,我们直接用安装包安装就可以了。预编译的意思就是官方已经直接把源码编译成了可以直接使用的二进制文件,比如.so.dll.a.lib等,只需要安装就可以直接使用了。预编译的库通常使用包管理器进行安装,比如在 Linux 系统中可以使用 apt 或者 yum,在 macOS 系统中可以使用 brew 。一行命令搞定,我们也不需要定制什么编译参数了,也能减少出错的概率。

小结

方式 适用库 方式 说明
include_directories() Header-only(Eigen, fmt, json) include_directories(external/eigen) 仅头文件,简单高效
add_subdirectory() 小型 CMake 库(GoogleTest, Pybind11) add_subdirectory(external/googletest) 需要编译,但体积小
find_package() 大型库(VTK, OpenCV, Boost) find_package(VTK REQUIRED) 已安装库,避免编译

VTK的安装和使用

VTK库的安装

vcpkg 安装

vcpkg 是 Microsoft 和 C++ 社区维护的开源 跨平台 C/C++ 包管理器,能够自动下载、编译和管理 C++ 库,vcpkg 的安装和配置非常方便:

1
2
3
4
git clone https://github.com/microsoft/vcpkg
cd vcpkg
./bootstrap-vcpkg.bat # Windows
./bootstrap-vcpkg.sh # Linux/macOS

运行 bootstrap-vcpkg.bat 即可生成 vcpkg.exe 可执行文件。同时这个脚本还可以用于更新 vcpkg ,让 vcpkg 重新构建自身,以获得最新版本。执行:

1
vcpkg.exe install vtk --triplet x64-windows

vcpkg 就会自动下载和编译 VTK 及其依赖项(如 hdf5, glew, jsoncpp),--triplet 指定了系统架构。vcpkg 安装库按照以下流程:

  1. packages/ 目录创建一个临时的 vtk_x64-windows/ 目录。
  2. packages/vtk_x64-windows/ 里下载、解压、构建 VTK。
  3. 构建完成后,把最终的 .lib/.dll/.h 复制到 installed/x64-windows/

在用 vcpkg 安装完成 VTK 之后,CMake 自己项目的时候可以使用如下的指令:

1
2
3
mkdir build
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
cmake --build build

在 CMake 中,命令行中的 -D 选项代表 Define,用于定义 CMake 变量,相当于在 CMakeLists.txt 中使用 set() 设定变量。这里的 -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake 指定了 Vcpkg 的 CMake 工具链文件。CMake 工具链文件可以自动配置 find_package(),使 CMake 能找到相应安装的库。Vcpkg 提供了这个工具链文件,让 CMake 可以自动找到 Vcpkg 里的库。

源代码编译

也可以直接通过源码编译 VTK :

1
2
3
4
5
6
git clone https://github.com/Kitware/VTK.git
mkdir build
mkdir install
cmake VTK -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=install -DVTK_USE_EXTERNAL=OFF
cmake --build build -j8
cmake --install build

CMAKE_BUILD_TYPE 用于指定编译版本,Debug 是调试版本,不做任何优化方便调试。Release 是发布版本,编译时对程序进行优化,降低代码大小提高执行速度。Debug 和 Release 没有本质的区别,只是不同编译选项的配置;CMAKE_INSTALL_PREFIX 用于设置安装目录。在这里只设置了很少的选项,实际上 VTK 的构建有着大量的选项,比如 Python 的支持、Qt 的支持等等,可以根据需要查询手册。VTK 库在 External 目录里放置了它所需的第三方库的源码,VTK_USE_EXTERNAL=OFF 是指让 VTK 用这些文件从头开始构建自己所需要的所有第三方库,确保它们与 VTK 配合使用,不依赖系统安装的版本,这样我们就不需要手动安装第三方库了。

在手动构建安装完成 VTK 之后,CMake 自己项目的时候可以使用如下的指令:

1
2
3
mkdir build
cmake -DCMAKE_PREFIX_PATH=/VTK/install -B build .
cmake --build build

这里把 VTK 的安装目录放到 CMAKE_PREFIX_PATH 中,方便 find_package() 可以找到 VTK 和它所依赖的包。注意只设置 -DVTK_DIR=/VTK/install 这样的方式构建会出现找不到第三方库的错误。这是因为 -DVTK_DIR 只告诉了 CMake 如何查找 VTK 的安装路径,无法找到 VTK 安装时同时安装的第三方库的 <Package>Config.cmake 文件。

可执行文件与 VTK 动态库

在默认配置下,VTK 会编译为动态库(.dll / .so / .dylib),但如果运行时找不到 VTK 的动态库,程序将无法启动。这是因为 可执行文件不会将动态库路径硬编码到程序中,而是依赖系统的动态库加载机制在运行时找到并加载它们。如果 VTK 的动态库不在系统的默认查找路径中,加载将失败,导致程序无法运行。

当一个可执行文件运行时,它会尝试找到并加载所有依赖的动态库(例如 VTK 的 .dll / .so / .dylib),但系统的默认查找路径通常不会自动包含 VTK 的安装目录。操作系统主要从以下位置查找动态库

操作系统 默认动态库查找路径
Windows 1️⃣ 可执行文件所在目录 2️⃣ PATH 环境变量
Linux 1️⃣ 可执行文件所在目录 2️⃣ /lib/usr/lib 3️⃣ LD_LIBRARY_PATH 环境变量
macOS 1️⃣ 可执行文件所在目录 2️⃣ /usr/lib/usr/local/lib 3️⃣ DYLD_LIBRARY_PATH 环境变量

如果 VTK 的动态库不在这些默认路径中,程序就会报错 "找不到动态库"。最简单、最可靠的方法是直接把所有 VTK 相关的动态库复制到可执行文件所在的目录,这样系统会优先查找可执行文件所在目录,保证程序能正常运行。或者把安装好的 VTK 的 bin 目录放到环境变量里,也可以让自己的程序正常运行。在使用 vcpkg 安装的 VTK 时,vcpkg 会自动在 CMake 构建时调用 applocal.ps1 脚本,这个脚本会自动查找所用到的库的 DLL 并将它们复制到构建目录,确保可执行文件在运行时可以找到这些动态库,不需要我们手动处理了。

这里再额外补充一下,对于一个依赖动态库的程序,对于 Linux/macOS 系统,编译时需要这个动态库的 .h + .so 文件,运行时需要 .so 文件;对于 Windows MSVC,运行时需要这个动态库的 .h + .lib 文件,运行时需要 .dll 文件。这里的 .lib 不是静态库,而是这个动态库的导入库,包含一个符号表,用来告诉编译器的 .dll 里有哪些函数。对于 MinGW 编译时则不需要 .lib ,只需要 .dll 文件即可。

VTK库的使用

VTK 模块

VTK 是一个非常庞大且复杂的库,几乎涵盖了所有 3D 渲染、科学计算可视化、医学影像处理、并行计算、甚至物理仿真等领域。VTK 采用了模块化的设计,将不同的功能分散到了不同的模块中,使用时只需要加载需要的模块就可以了。大体上,VTK 包含以下几个常见的模块:

数据处理相关模块(VTK Filters 系列,负责数据过滤和转换)

这些模块负责处理数据,包括数据过滤、几何变换、平滑、插值等。

  • FiltersCore:数据转换的核心,例如 vtkTransformFilter(变换)、vtkThreshold(阈值过滤)。
  • FiltersGeneral:通用数据处理,比如 vtkWarpScalar(根据标量变形网格)。
  • FiltersGeometry:处理几何数据,如 vtkFeatureEdges(提取边界)。
  • FiltersModeling:建模处理,如 vtkDelaunay2D(三角网格化)。
  • FiltersSources:几何数据源。
  • FiltersTexture:纹理处理,如 vtkTextureMapToPlane(将纹理映射到平面)。
渲染和可视化相关模块(VTK Rendering 系列,负责渲染)

这些模块负责 3D 渲染,包括光照、阴影、材质、体渲染等。

  • RenderingCore:渲染核心,如 vtkActorvtkRenderer
  • RenderingOpenGL2:基于 OpenGL 2.0 的渲染。
  • RenderingVolume:体渲染,如 vtkVolume(体数据渲染)。
  • RenderingAnnotation:标注和注释,如 vtkCornerAnnotation(显示角落信息)。
  • RenderingLabel:处理文本标签,如 vtkLabeledDataMapper(数据标签映射)。
  • RenderingLOD:细节层次(Level of Detail,LOD)管理,加速大数据渲染。
  • RenderingRayTracing:光线追踪,如 vtkOSPRayRenderer(支持 OSPRay 渲染)。
  • RenderingGL2PS:支持矢量输出,如 PDF 或 EPS。
交互和 UI 相关模块(VTK Interaction 系列,用户交互)

这些模块控制交互方式,如鼠标、键盘、触摸输入等。

  • InteractionStyle:支持鼠标旋转、缩放、平移。
  • InteractionWidgets:提供交互 UI 控件,如 vtkSliderWidget(滑块)。
  • InteractionImage:专门用于图像交互,如 vtkImageViewer2(交互式图像查看器)。
  • GUISupportQt / GUISupportMFC:分别支持 Qt 和 MFC 界面集成。
计算与数学相关模块(VTK Common 系列,数学和数据结构)

提供 VTK 的基本计算功能,如矩阵运算、数学工具等。

  • CommonCore:VTK 的核心数据结构、内存管理等。
  • CommonDataModel:数据模型,如 vtkPolyData(几何数据)、vtkImageData(图像数据)。
  • CommonMath:数学计算模块,如 vtkMatrix4x4(4×4 矩阵运算)。
  • CommonTransforms:变换模块,如 vtkTransform(仿射变换)。
体数据与医学影像(VTK Imaging 系列,图像处理)

这些模块处理 2D/3D 图像数据,可用于医学影像(DICOM)、卫星图像等。

  • ImagingCore:基本图像处理,如 vtkImageData
  • ImagingHybrid:高级图像处理,如 vtkImageReslice(图像重采样)。
  • ImagingGeneral:常见图像滤波,如 vtkImageThreshold(图像二值化)。
  • ImagingFourier:傅立叶变换,如 vtkImageFFT(快速傅立叶变换)。
  • ImagingStatistics:统计计算,如 vtkImageAccumulate(计算直方图)。
  • IOImage:支持医学图像(DICOM、JPEG、TIFF等)。
I/O 数据读取与写入(VTK IO 系列,数据输入输出)

VTK 支持多种文件格式,如 STL、PLY、VTK、DICOM、CSV、HDF5 等。

  • IOCore:基础 I/O,如 vtkDataReader(读取 VTK 文件)。
  • IOXML:XML 格式,如 vtkXMLPolyDataReader(读取 XML 结构的几何数据)。
  • IOPLY:读取和写入 PLY 文件(常用于 3D 扫描数据)。
  • IOSTL:读取和写入 STL 文件(3D 打印常用)。
  • IODICOM:医学影像读取(DICOM)。
  • IOHDF5:支持 HDF5 数据格式(用于大规模科学数据)。
物理仿真和计算(VTK Physics 和 Parallel 系列,物理仿真和并行计算)

这些模块用于模拟物理现象,如流体力学、刚体碰撞等。

  • PhysicsRigidBody:刚体物理,如 vtkRigidBodyMotion(刚体运动)。
  • ParallelCore:并行计算核心,适用于大数据可视化。
  • ParallelMPI:支持 MPI 并行计算(超算集群)。
  • ParallelDIY:用于分布式数据处理,如 vtkDIYExplicitAssigner
科学可视化(VTK Views 和 Charts 系列)

这些模块用于科学数据可视化,如图表、统计数据等。

  • ViewsCore:通用视图管理,如 vtkView(视图基类)。
  • ViewsInfovis:信息可视化,如 vtkGraphLayoutView(图数据可视化)。
  • ChartsCore:绘制 2D/3D 图表,如 vtkChartXY(折线图)。

VTK 数据结构和智能指针

在 VTK 中,所有的数据处理和可视化都围绕着 VTK 的数据结构展开,VTK 的核心数据对象可以分成以下几类:

  1. 表面几何数据(vtkPolyData)
  2. 体数据(vtkImageData)
  3. 结构化网格数据(vtkStructuredGrid,vtkRectilinearGrid)
  4. 非结构化网格数据(vtkUnstructuredGrid)
  5. 图数据(vtkGraph,vtkTree)
  6. 表格数据(vtkTable)

所有 VTK 的数据结构都继承自 vtkDataObject 基类:

1
2
3
4
5
6
7
vtkDataObject  <-- 所有数据的基类
├── vtkPolyData(几何表面)
├── vtkImageData(体数据)
├── vtkStructuredGrid(规则网格)
├── vtkUnstructuredGrid(非结构化网格)
├── vtkGraph(图数据)
├── vtkTable(表格数据)

另外,在 VTK 中,所有的对象都是基于引用计数的对象,必须通过 vtkSmartPointer 进行管理,以避免内存泄漏。vtkSmartPointer 会自动管理 VTK 对象的引用计数和生命周期,避免手动 delete,确保正确释放资源。

1
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();

等效于

1
2
vtkCubeSource* cube = vtkCubeSource::New();
cube->Delete();

这里,vtkSmartPointer<T> 是一个 C++ 泛型(模板)编程,允许创建适用于不同数据类型的类:

1
2
3
4
5
template <typename T>
class Example {
public:
void print() { std::cout << "This is a template class." << std::endl; }
}

使用时:

1
2
Example<int> intObj;   // 适用于 int 类型
Example<double> dblObj; // 适用于 double 类型

:: 在 C++ 中用于访问类的静态成员或访问某个命名空间中的成员。静态方法属于类,而不是对象,所以必须用 :: 调用。在普通的 C++ 中,创建对象通常使用 new

1
vtkCubeSource* cube = new vtkCubeSource();

但是这样创建的对象需要手动 delete,否则会造成内存泄漏。在 VTK 中,使用 vtkSmartPointer<T>::New() 来管理内存:

1
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();

vtkSmartPointer 自动管理对象的生命周期。当 cube 变量超出作用域时,对象会自动销毁,避免内存泄漏。它类似于 VTK 版的 std::shared_ptr 。在 C++ 里,std::shared_ptr 类似 Python 的对象管理方式,它使用引用计数,当没有 shared_ptr 继续引用对象时,自动销毁。自动管理内存,避免 newdelete ,防止内存泄漏。

在 C++ 里,如果直接用 new 分配内存,必须手动 delete,否则会造成内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
MyClass() { std::cout << "Created!" << std::endl; }
~MyClass() { std::cout << "Destroyed!" << std::endl; }
};

void func() {
MyClass* obj = new MyClass(); // ❌ 需要手动 delete,否则泄漏!
} // obj 超出作用域,但不会自动释放!

int main() {
func();
return 0; // MyClass 没有 delete,发生内存泄漏!
}

✅ 解决方案:使用 std::shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>
#include <iostream>

class MyClass {
public:
MyClass() { std::cout << "Created!" << std::endl; }
~MyClass() { std::cout << "Destroyed!" << std::endl; }
};

void func() {
std::shared_ptr<MyClass> obj = std::make_shared<MyClass>(); // ✅ 自动管理内存
} // obj 超出作用域,引用计数变 0,自动释放!

int main() {
func(); // 不会泄漏!
return 0;
}

常用的四种数据结构:

1. 表面几何数据(vtkPolyData)

vtkPolyDataVTK 最常见的几何数据格式,用于存储 点(Points)、线(Lines)、面(Polygons)、多面体(Polyhedron)

📌 适用于:STL/PLY/OBJ 3D 模型、点云数据(如 LIDAR 扫描数据)、多边形网格、等值面可视化

📌 数据结构:

  • 点(Points)
  • 拓扑结构(Cells):存储点如何组成几何形状(线、三角形、四边形等)
  • 属性数据(Scalars/Vectors):每个点或单元的颜色、速度、法向量等数据

📌 示例:创建一个三角形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建点集,vtkPoints 用于存储三维点坐标,这里添加了3个点
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
points->InsertNextPoint(0.0, 0.0, 0.0);
points->InsertNextPoint(1.0, 0.0, 0.0);
points->InsertNextPoint(0.0, 1.0, 0.0);

// 创建三角形单元,vtkTriangle 代表一个三角形单元(cell),GetPointIds->SetID(i, j) 绑定三角形的顶点,比如这里将三角形的顶点0、1、2分别绑定到 points 中的第0、1、2个点(后面会进行绑定)
vtkSmartPointer<vtkTriangle> triangle = vtkSmartPointer<vtkTriangle>::New();
triangle->GetPointIds()->SetId(0, 0);
triangle->GetPointIds()->SetId(1, 1);
triangle->GetPointIds()->SetId(2, 2);

// 创建单元数组,vtkCellArray 用于存储多个几何单元(cell),InsertNextCell(triangle) 添加三角形到cells数组中
vtkSmartPointer<vtkCellArray> cells = vtkSmartPointer<vtkCellArray>::New();
cells->InsertNextCell(triangle);

// 创建 vtkPolyData,vtkPolyData 是 VTK 的核心数据结构,用于存储点(points)和拓扑结构(polys)。SetPoints(points) 将点集绑定到 polyData。SetPolys(cells) 将三角形单元绑定到 polyData。
vtkSmartPointer<vtkPolyData> polyData = vtkSmartPointer<vtkPolyData>::New();
polyData->SetPoints(points);
polyData->SetPolys(cells);

2. 体数据(vtkImageData)

vtkImageData 代表 规则网格的 3D 体数据,通常用于医学影像(CT, MRI)或流体计算。

📌 适用于:医学 DICOM 数据(CT, MRI)、流体力学(CFD)计算结果、体渲染(Volume Rendering)

📌 数据结构:

  • 规则网格(Structured Grid):每个体素(Voxel)按 x, y, z 规则排列
  • 标量(Scalars):存储体素的颜色、密度等数据

📌 示例:创建一个 3D 体数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vtkImageData 是 VTK 用于存储结构化图像数据的核心数据结构。它用于表示规则网格(structured grid),可以存储 2D 或 3D 体素数据 (volumetric data)
vtkSmartPointer<vtkImageData> imageData = vtkSmartPointer<vtkImageData>::New();

// 设置网格点数量,X轴方向100个点,Y轴方向100个点,Z轴方向50个点,相当于创建了个一个 100 x 100 x 50 的体素数据
imageData->SetDimensions(100, 100, 50);

// 设置每个体素的尺寸
imageData->SetSpacing(1.0, 1.0, 1.5);

// AllocateScalars(dataType, numComponents) 分配存储空间,即分配数组来存储体素数据:VTK_FLOAT:每个体素数据类型为 浮点数 (float),用于存储 灰度值、密度值、温度场等。1:每个体素只有 1 个分量(即单通道数据,例如灰度图像)。如果 numComponents = 3,则表示 RGB 彩色图像(每个体素有 3 个通道)。
imageData->AllocateScalars(VTK_FLOAT, 1);

// 填充 imageData 对象的数据
int* dims = imageData->GetDimensions();
for (int z = 0; z < dims[2]; z++) {
for (int y = 0; y < dims[1]; y++) {
for (int x = 0; x < dims[0]; x++) {
float* pixel = static_cast<float*>(imageData->GetScalarPointer(x, y, z));
*pixel = static_cast<float>(x + y + z); // 设置像素值
}
}
}

3. 结构化网格(vtkStructuredGrid, vtkRectilinearGrid)
  • vtkStructuredGrid:适用于 规则但不均匀间距的网格,如 CFD 数据。
  • vtkRectilinearGrid:适用于 X、Y、Z 轴分别均匀间隔的网格

📌 适用于:流体力学、有限元分析(FEA)

📌 示例:创建结构化网格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vtkSmartPointer<vtkStructuredGrid> structuredGrid = vtkSmartPointer<vtkStructuredGrid>::New();
structuredGrid->SetDimensions(10, 10, 10);

// 填充点数据,vtkStructuredGrid 需要显式指定点坐标,因为它不像 vtkImageData 那样 默认按规则间距生成点。
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();

// 遍历网格,填充点坐标
for (int z = 0; z < 10; z++) {
for (int y = 0; y < 10; y++) {
for (int x = 0; x < 10; x++) {
points->InsertNextPoint(x * 1.0, y * 1.0, z * 1.5); // 可以调整间距
}
}
}

// 绑定点数据到结构化网格
structuredGrid->SetPoints(points);

4. 非结构化网格(vtkUnstructuredGrid)

vtkUnstructuredGrid 适用于 复杂的网格结构,包括:四面体、六面体、金字塔、棱柱等,我们需要 手动添加点坐标单元 (Cell) 数据 来填充它。

📌 适用于:有限元分析(FEA)、复杂网格 CFD 计算

📌 示例:创建一个四面体网格

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
// 1. 创建 vtkUnstructuredGrid 对象,用于存储非结构化网格数据
vtkSmartPointer<vtkUnstructuredGrid> unstructuredGrid = vtkSmartPointer<vtkUnstructuredGrid>::New();

// 2. 创建 vtkPoints 对象,用于存储网格中的点(顶点)
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();

// 3. 添加 4 个点,形成一个四面体(Tetrahedron)
points->InsertNextPoint(0.0, 0.0, 0.0); // 点 0
points->InsertNextPoint(1.0, 0.0, 0.0); // 点 1
points->InsertNextPoint(0.0, 1.0, 0.0); // 点 2
points->InsertNextPoint(0.0, 0.0, 1.0); // 点 3

// 4. 将点数据绑定到非结构化网格
unstructuredGrid->SetPoints(points);

// 5. 创建一个四面体(Tetrahedron)单元
vtkSmartPointer<vtkTetra> tetra = vtkSmartPointer<vtkTetra>::New();

// 6. 设置四面体的 4 个顶点索引(对应 points 中的点)
tetra->GetPointIds()->SetId(0, 0); // 顶点 0
tetra->GetPointIds()->SetId(1, 1); // 顶点 1
tetra->GetPointIds()->SetId(2, 2); // 顶点 2
tetra->GetPointIds()->SetId(3, 3); // 顶点 3

// 7. 将单元(Cell)添加到非结构化网格
unstructuredGrid->InsertNextCell(tetra->GetCellType(), tetra->GetPointIds());

// 8. 创建一个标量数组(用于存储温度等物理量)
vtkSmartPointer<vtkFloatArray> scalars = vtkSmartPointer<vtkFloatArray>::New();
scalars->SetName("Temperature"); // 设置标量名称(可用于可视化)
scalars->SetNumberOfComponents(1); // 每个点只有 1 个标量分量(单通道数据)

// 9. 为每个点分配温度数据
scalars->InsertNextValue(10.0); // 点 0 的温度
scalars->InsertNextValue(20.0); // 点 1 的温度
scalars->InsertNextValue(30.0); // 点 2 的温度
scalars->InsertNextValue(40.0); // 点 3 的温度

// 10. 将标量数据绑定到网格的点数据
unstructuredGrid->GetPointData()->SetScalars(scalars);

VTK 可视化管线

VTK 的可视化管线(Visualization Pipeline)定义了数据从输入到最终渲染的整个流程,是 VTK 后处理的一般流程。在 VTK 中,可视化管线由以下五个核心极端组成:

数据源(Source) -> 数据过滤(Filter) -> 数据映射(Mapper) -> 渲染管理(Renderer) -> 交互控制(Interactor)

1. 数据源(Source)

数据源是 VTK 可视化管线的起点,它负责 生成数据或读取外部数据。数据可以来自:几何数据(点云、网格、曲面);医学影像数据(DICOM, NIfTI);科学计算数据(CFD, FEA, VTK 文件);分子动力学轨迹(如 GROMACS .xtc, LAMMPS .dump)。常用的 VTK 数据源类包括:

类型 VTK 类 说明
基础几何体 vtkCubeSource, vtkSphereSource 生成立方体、球体等基本几何体
点云数据 vtkPointSource 生成随机点云数据
网格数据 vtkStructuredGrid, vtkUnstructuredGrid 处理结构化/非结构化网格
体数据 vtkImageData, vtkRectilinearGrid 适用于医学影像、流体计算数据
文件输入 vtkSTLReader, vtkPLYReader, vtkXMLPolyDataReader 读取 .stl, .ply, .vtk 文件

示例:创建立方体数据

1
2
vtkSmartPointer<vtkCubeSource> cubeSource = vtkSmartPointer<vtkCubeSource>::New();
cubeSource->Update(); // 生成数据

💡 cubeSource->Update(); 强制执行数据更新,确保数据正确传递给后续的管线。


2. 数据过滤(Filter)

数据经过数据源生成后,通常需要进行处理或转换,这就是数据过滤(Filter)的作用。常用的 VTK 过滤类包括:

过滤器类型 VTK 类 作用
变换 vtkTransformFilter 旋转、缩放、平移
平滑 vtkSmoothPolyDataFilter 平滑几何表面
裁剪 vtkClipPolyData 根据平面裁剪
等值面 vtkContourFilter 计算等值面(用于流体/医学影像)
采样 vtkDecimatePro 降低多边形数
网格化 vtkDelaunay2D 生成三角网格

示例:平滑几何体

1
2
3
4
vtkSmartPointer<vtkSmoothPolyDataFilter> smoothFilter = vtkSmartPointer<vtkSmoothPolyDataFilter>::New();
smoothFilter->SetInputConnection(cubeSource->GetOutputPort());
smoothFilter->SetNumberOfIterations(10);
smoothFilter->Update();

3. 数据映射(Mapper)

数据处理完成后,需要将其转换为 图形系统可识别的格式,这就是数据映射(Mapper)的作用。常用的 Mapper 类包括:

Mapper 类型 VTK 类 作用
多边形数据 vtkPolyDataMapper 映射几何体,如点、线、三角面
体数据 vtkVolumeMapper 体渲染(如医学 CT 扫描数据)
矢量数据 vtkGlyph3DMapper 处理流体/场数据(如速度场、磁场)

示例:创建 Mapper

1
2
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(smoothFilter->GetOutputPort());

4. 渲染管理(Renderer)

VTK 使用 Renderer 管理 3D 场景,包括:添加 Actor(3D 物体)设置背景颜色管理光照。VTK 渲染核心类包括:

类型 VTK 类 作用
场景管理 vtkRenderer 管理 3D 物体、光照
渲染窗口 vtkRenderWindow 显示渲染内容
物体 vtkActor 代表一个 3D 物体

示例:创建 Renderer

1
2
3
vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(actor);
renderer->SetBackground(0.1, 0.2, 0.3); // 深蓝色背景

5. 交互控制(Interactor)

VTK 提供交互工具 vtkRenderWindowInteractor,可以用鼠标旋转、缩放、平移 3D 视图。VTK 交互核心类包括:

交互类型 VTK 类 作用
交互窗口 vtkRenderWindowInteractor 允许鼠标/键盘交互
交互样式 vtkInteractorStyleTrackballCamera 轨迹球交互模式
UI 控件 vtkSliderWidget 创建滑块控制

示例:创建交互器

1
2
3
4
vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
interactor->SetRenderWindow(renderWindow);
interactor->Initialize();
interactor->Start(); // 进入交互模式

绘制一个立方体

这里是一个基本的 VTK 库的使用范例,用于绘制一个立方体。从例子也可以看到,VTK 的渲染管线是层层封装的,每一层都包含并管理更基础的组件:

1
2
3
4
5
6
vtkRenderWindowInteractor
├── vtkRenderWindow
│ ├── vtkRenderer
│ │ ├── vtkActor
│ │ │ ├── vtkMapper
│ │ │ │ ├── vtkPolyData / vtkImageData / vtkUnstructuredGrid

交互器(Interactor)

  • vtkRenderWindowInteractor用户交互控制的入口,它控制 鼠标旋转、缩放、键盘输入等
  • 它管理 vtkRenderWindow,使其可以响应用户输入。

渲染窗口(Render Window)

  • vtkRenderWindow真正的 3D 窗口,它 显示所有渲染内容
  • 它管理 vtkRenderer,可以包含多个 Renderer(类似于多个视口)。

渲染器(Renderer)

  • vtkRenderer 负责管理 3D 场景,包括:vtkActor(物体)、光照、背景颜色、摄像机

  • 一个 vtkRenderWindow 可以有多个 vtkRenderer

演员(Actor)

  • vtkActor 代表 3D 物体,它是最终显示在屏幕上的可见对象。
  • vtkActor 不存储几何数据,它只是一个 外观控制器,管理:Mapper(数据映射)颜色、透明度变换(缩放、旋转、平移)

数据映射(Mapper)

  • vtkMapper 负责 将几何数据转换为渲染数据
  • 它接受 PolyData、Volume、Grid 作为输入,并将数据转换为可显示的格式。
  • 常见的 Mapper
    • vtkPolyDataMapper → 处理几何数据
    • vtkVolumeMapper → 体渲染
    • vtkDataSetMapper → 处理网格数据

数据(PolyData, ImageData, Grid)

  • vtkPolyData几何数据(点、线、三角形)
  • vtkImageData体数据(医学 CT、MRI、流体数据)
  • vtkUnstructuredGrid非结构化网格(CFD、有限元计算)
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
// main.cpp
#include <vtkSmartPointer.h>
#include <vtkCubeSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>

int main() {
// 1. 创建立方体
vtkSmartPointer<vtkCubeSource> cubeSource = vtkSmartPointer<vtkCubeSource>::New();
cubeSource->Update(); // 生成立方体数据

// 2. 创建 Mapper,将几何数据转换为可渲染数据
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(cubeSource->GetOutputPort());

// 3. 创建 Actor(演员),用于表示 3D 物体
vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);

// 4. 创建 Renderer(渲染器)
vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(actor); // 添加立方体
renderer->SetBackground(0.1, 0.2, 0.3); // 设置背景颜色(深蓝色)

// 5. 创建 Render Window(渲染窗口)
vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
renderWindow->AddRenderer(renderer);
renderWindow->SetSize(600, 600); // 窗口大小

// 6. 创建交互器,让用户可以旋转、缩放、平移模型
vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
interactor->SetRenderWindow(renderWindow);

// 7. 开始渲染并交互
renderWindow->Render(); // 触发渲染
interactor->Start(); // 进入交互模式

return 0;
}
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
# CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(SimpleCube)

# 1. 显式声明需要的最低VTK版本
find_package(VTK 9.0 REQUIRED COMPONENTS
CommonCore
FiltersSources
InteractionStyle
RenderingOpenGL2
RenderingFreeType # 字体支持
RenderingCore # 渲染核心模块
)

# 2. 先声明可执行文件目标
add_executable(SimpleCube main.cpp)

# 3. 正确位置的模块初始化(必须在目标创建之后)
vtk_module_autoinit(
TARGETS SimpleCube
MODULES ${VTK_LIBRARIES}
)

# 4. 现代目标链接方式
target_link_libraries(SimpleCube PRIVATE
VTK::CommonCore
VTK::FiltersSources
VTK::InteractionStyle
VTK::RenderingOpenGL2
VTK::RenderingFreeType
VTK::RenderingCore
)

构建后,可以得到如下的结果: