☰
ParaView CGNS Reader插件:多zone、边界命名与时间序列
2026/10/30 7:02:22 网站建设 项目流程

简介:这是一款面向科学可视化与CFD数据处理场景的小型ParaView插件,使用C++实现,主要解决CGNS格式网格与数据的读取问题。其设计受VisIt CGNS阅读器启发,但改用低级CGNS API加载DataSet,以降低内存占用;支持多块结构化/非结构化网格、基本时间步、SIDS命名向量,并可在单处理器下加载非结构化网格边界补丁,适合需要快速查看CGNS算例或进行插件二次开发的工程师与研究者。压缩包共16个文件、约166KB,包含cxx/h源码、CMake构建脚本、XML插件描述、说明文档及示例图片等,体量轻便,便于集成与编译。目前已有1419人学习,读者可据此了解ParaView插件组织方式、CGNS数据映射到VTK对象的实现路径,以及多块网格与边界补丁的处理思路,并可作为自建读取器的参考起点。

1. 手里一堆 CGNS,ParaView 打开能看见网格,但边界条件名全变成 Block0、多 zone 只剩一个、时间滑块拖过去云图不动——这三件事凑在一起,基本就没法做后处理了。CGNSReader_ParaView_Plugin 就是冲这类「读得进但读不对」的场景写的:一份 C++ 源码,把 cgnslib 的节点树按 base/zone/patch 搬到 vtkMultiBlockDataSet 上,编译成 ParaView 的 reader 插件直接挂进 Open 对话框。适合手里有 CFD 结果、要做 Extract Block 和批量脚本后处理的人;只想扫一眼流场形态的,内置读取器够用。

2. CGNS 的节点树与 vtkMultiBlockDataSet:插件到底要搬哪几层数据

写读取器之前得先把两边的东西对应上。CGNS 那边是一棵带语义的节点树,VTK 那边是一个三层的 multiblock,中间还夹着一层编号基数和单元类型映射。这一章先把对应关系和实现路线定下来,代码放后面。

2.1 base、zone、section、solution 四层骨架

CGNS 不是「一个网格文件格式」,而是一套用节点树描述 CFD 数据的约定。打开一个 .cgns,最外层是 CGNSBase_t,一个文件可以有多个 base,每个 base 带 cell_dim 和 phys_dim 两个维度声明——三维流体一般是 3/3,二维是 2/2,也有写 3/3 但 zone 只有一层的薄片算例,这两种在读取器里走的分支完全不同,靠 cell_dim 判断并不可靠。

base 下面是 Zone_t。结构网格用 VertexSize[3] 三个方向的节点数描述,非结构网格用顶点数加单元数描述,网格坐标统一挂在 GridCoordinates_t 下,按 CoordinateX/Y/Z 三个数组存,数据类型可能是 float 也可能是 double。读取时一律用 RealDouble 请求,让 cgnslib 自己做类型提升,比在插件里写两套分支省事得多。

非结构 zone 的单元不直接写在 zone 节点上,而是拆成若干 Elements_t 段,也就是 section。每段声明一种 ElementType_t、节点号区间的起止、以及面单元到体单元的归属。这个结构决定了读取器的循环顺序:先遍历 section 拼 connectivity,再按 section 顺序累加 cell 索引。任何「假设所有 section 都是同一种单元」或者「假设 section 顺序等于单元编号顺序」的写法,后面挂 cell data 时都会整体错位,而且错得很安静——ParaView 不会报错,只是颜色不对。

场数据在 FlowSolution_t 里,每个 solution 带一个 GridLocation_t。取 Vertex 时数组长度等于点数,取 CellCenter 时长度等于单元数,取 IFaceCenter 之类则挂在面上。这是后面分 PointData 和 CellData 的唯一依据,也是最容易漏的一处:不少求解器同一个 zone 里同时写 Vertex 和 CellCenter 两个 solution,名字还都叫 FlowSolution,靠序号区分。

提示:CGNS 的节点大部分是可选的,顺序也不固定。定位节点一律用 cg_goto 加节点名和序号,不要用「第几个子节点」这种写法。

还有两个不属于文件结构但直接影响显示的细节。一是单位,CGNS 不强制单位,很多求解器按 mm、按 rpm 写进去,读到 VTK 里就是裸数字,压力云图上写着 1.2e5 你分不清是 Pa 还是 kPa,所以插件里留一个 ScaleFactor 属性是划算的。二是时间,时间值不是 zone 的属性,而是藏在每个 FlowSolution_t 节点下一个叫 TimeValues 的一维数组里,一个 zone 一份,多个 zone 各存各的,还可能不排序——这一条后面单独讲。

2.2 三条实现路线:XML 薄封装、继承 vtkCGNSReader、cgnslib 直读

ParaView 自带 CGNS 读取能力,所以插件第一步要回答的问题不是「怎么读」,而是「为什么还要自己读」。把三条路线摊开看,选型理由就很清楚了。

路线具体做法能控住什么代价
A 纯 XML 薄封装SourceProxy 的 class 直接写 vtkCGNSReader,只写 XML几乎没有,等于给内置读取器换个入口命名、拆分、单位一个都改不了
B 继承 vtkCGNSReader覆写 RequestData,在父类输出上做二次加工block 重命名、补 patch、单位缩放父类内部结构不对外,层级一改就崩
C cgnslib 直读自己写 vtkMultiBlockDataSetAlgorithm 子类全流程可控:层级、命名、时间、单位要自己维护单元类型映射和编号转换

A 路线唯一的价值是验证环境:插件框架、编译、加载这一步通不通,用一个几十行的 XML 就能试出来,通了再换 C。刚上手时我一般先做 A,确认 ParaView 能认这个 .so,再往里填东西,否则编译错误和加载错误混在一起,排查成本翻倍。

B 路线看起来最省事,实际上是最容易翻车的一条。vtkCGNSReader 的输出层级是 base→zone→patch 三级,靠内部的 vtkCGNSReaderInternal 维护一套「哪个 block 对应哪个 CGNS 节点」的映射表,这张表不对外暴露。你想在父类输出上重命名 block,就得自己再建一份映射,两份映射一旦不同步,Extract Block 里选中的和实际输出的是两个东西。ParaView 小版本升级时父类内部调整,这类插件往往第一个挂掉。

C 路线是这份插件选的路。理由不复杂:内置读取器已经把「能读」这件事做完了,插件存在的意义是「读成我要的样子」,能控住层级和命名才是核心诉求。代价是一次性的映射表——CGNS 的 ElementType_t 到 VTK 的单元类型就那么十几种,写一遍能用很久。

// CGNS ElementType_t -> VTK cell type,按需补全即可 static int VtkTypeFromCGNS(ElementType_t et) { switch (et) { case NODE: return VTK_VERTEX; case BAR_2: return VTK_LINE; case TRI_3: return VTK_TRIANGLE; case QUAD_4: return VTK_QUAD; case TETRA_4: return VTK_TETRA; case PYRA_5: return VTK_PYRAMID; case PENTA_6: return VTK_WEDGE; case HEXA_8: return VTK_HEXAHEDRON; default: return VTK_EMPTY_CELL; // MIXED 和二阶单元单独处理 } }

这段 switch 里有两个空白点必须提前想清楚。MIXED 段的 connectivity 不是定长数组,而是「类型、节点数、节点号……」交错排列,得自己解包;二阶单元(TETRA_10、HEXA_20 这类)VTK 有对应类型,但节点顺序不一定和 CGNS 一致,没把握就先跳过,别硬映射成线性单元——那样网格看着正常,体积会算错。

工程依赖上,C 路线只要 vtkCommonDataModel、vtkCommonExecutionModel 两个模块加 cgnslib,不需要 vtkIOCGNSReader。少一个模块依赖,插件在别的 ParaView 版本上加载成功率就高一分,这个便宜值得占。

3. 用 CMake 把插件挂进 ParaView:从 paraview_add_plugin 到 reader XML

插件工程最容易卡住的地方不是算法,是构建。ParaView 的插件必须和宿主用同一套 ABI,编译器版本、VTK 版本、甚至 Debug/Release 都得对上,否则编译能过、加载时报符号找不到。这一章把工程骨架和 XML 讲透。

3.1 CMakeLists 的三段结构

ParaView 提供了一个 CMake 宏 paraview_add_plugin,它替你做完三件事:把源码编成动态库、生成插件的初始化函数、处理 VTK 模块的 autoinit。手写 add_library 也能出 .so,但对象工厂注册不上,加载后 ParaView 里什么都不会出现。

cmake_minimum_required(VERSION 3.16) project(CGNSReaderPlugin LANGUAGES CXX) # ParaView 的 CMake 配置包,路径要指向 lib/cmake/paraview-x.y find_package(ParaView REQUIRED) # cgnslib 单独找,找不到就手动给头文件和库路径 find_package(CGNS REQUIRED) paraview_add_plugin(CGNSReaderPlugin VERSION "1.0" REQUIRED_ON_SERVER # 纯读取器,不需要客户端部分 MODULES vtkCommonDataModel vtkCommonExecutionModel SERVER_MANAGER_XML CGNSReaderPlugin.xml SOURCES vtkCGNSPluginReader.cxx HEADERS vtkCGNSPluginReader.h) # cgnslib 是普通的第三方库,直接链到插件目标上 target_include_directories(CGNSReaderPlugin PRIVATE ${CGNS_INCLUDE_DIRS}) target_link_libraries(CGNSReaderPlugin PRIVATE ${CGNS_LIBRARIES})

MODULES 里列的是插件用到的 VTK 模块,paraview_add_plugin 会用它们生成 vtk_module_autoinit 调用。这一项漏了会出现一种很迷惑的现象:插件加载成功,菜单里也有条目,但一点就报「无法创建对象」。原因就是 VTK 的对象工厂没被初始化,类名查不到。

REQUIRED_ON_SERVER 的意思是只在服务端(也就是真正跑数据处理的那个进程)加载。读取器不做界面,客户端不需要它,加上这个标记能省掉一堆客户端侧的加载检查。如果后面要加自己的属性面板,再补 REQUIRED_ON_CLIENT 和 GUI_INTERFACES。

不同 ParaView 版本里 paraview_add_plugin 的关键字会略有增减,比如某些版本支持 LINK_LIBRARIES 直接写在宏里。我的习惯是先按最保守的写法来,CMake 报「unknown keyword」再按提示改,比照文档猜快。

3.2 reader 的 XML:ReaderFactory 决定它出现在哪

XML 是插件和 ParaView 之间的契约。它声明有哪些属性、属性对应哪个 setter、这个 proxy 出现在 Open 对话框的哪个过滤项里。写错一个标签,插件照样加载,但菜单里找不到,这类问题最费时间。

<ServerManagerConfiguration> <ProxyGroup name="sources"> <SourceProxy name="CGNSPluginReader" class="vtkCGNSPluginReader" label="CGNS Reader (Plugin)"> <Documentation> Reads CGNS bases/zones/patches into a multiblock dataset. </Documentation> <!-- 文件名由 Open 对话框自动填入,不在属性面板里重复展示 --> <StringVectorProperty name="FileName" command="SetFileName" number_of_elements="1" panel_visibility="never"/> <!-- mm 转 m 之类,缩放放在读取阶段比后面用 Calculator 干净 --> <DoubleVectorProperty name="ScaleFactor" command="SetScaleFactor" number_of_elements="1" default_values="1.0"/> <!-- 只要网格不要场时关掉,内存能省一半 --> <IntVectorProperty name="LoadSolution" command="SetLoadSolution" number_of_elements="1" default_values="1"/> <Hints> <ReaderFactory extensions="cgns" file_description="CGNS files"/> </Hints> </SourceProxy> </ProxyGroup> </ServerManagerConfiguration>

几个属性逐个说。FileName 必须写 panel_visibility=“never”,因为 ParaView 的 Open 对话框会通过 vtkSMReaderFactory 自动设这个属性,面板上再放一个输入框只会让用户困惑:改了没反应。ScaleFactor 用 DoubleVectorProperty,默认 1.0,读取时乘到坐标和标量上——注意是坐标和标量都乘,只乘其一会让流线图整个歪掉。

ReaderFactory 这一项是决定性的。没有它,插件加载成功、Proxy 也注册了,但 File > Open 里根本选不到 .cgns。extensions 支持分号分隔的多个后缀,file_description 就是过滤器下拉框里显示的字符串。

如果文件里有时间序列,还要在 SourceProxy 里补一个 TimeStep 属性并把 FileSeriesReader 相关 hint 加全,否则 ParaView 会把它当静态数据,时间工具栏是灰的。这一步和下一章的 TIME_STEPS 是一套东西,缺一个都不生效。

3.3 编译、加载与第一次冒烟

构建命令没什么花样,关键是 ParaView_DIR 一定要指到 lib/cmake/paraview-x.y 这一层,指到安装根目录是找不到的。

cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ -DParaView_DIR=/opt/paraview/lib/cmake/paraview-5.11 \ -DCGNS_ROOT=/usr/local cmake --build build -j 8 # 方式一:环境变量指向构建目录,重启 ParaView 生效 export PV_PLUGIN_PATH=$PWD/build # 方式二:GUI 里 Tools > Manage Plugins > Load New,选生成的动态库

第一次冒烟按这个顺序走,能最快定位问题在哪一层:先在 Manage Plugins 里看插件能不能加载、有没有报错;再从 Open 对话框选一个 .cgns,看过滤器里有没有出现「CGNS files」;最后加载后看 Properties 面板里是不是只有 FileName、ScaleFactor、LoadSolution 三个属性。三步里任何一步不对,问题分别出在构建、XML 的 ReaderFactory、XML 的属性定义,界限很清楚。

有个前置条件容易被忽略:编译插件的机器和运行 ParaView 的机器,编译器大版本要一致。用 GCC 编的插件拿到 Clang 编的 ParaView 上加载,符号名对不上,报错信息还特别含糊,只说「无法加载」。跨平台分发插件时这一点尤其要提前说清楚。

4. 读取器内核:从 cg_open 到 vtkMultiBlockDataSet

工程通了之后才是真正的活。读取器的执行分两段:RequestInformation 只读元信息,把时间步和数组清单报给管线;RequestData 才真正搬网格和场数据。这个分法不是形式主义,ParaView 会在拖时间滑块时反复调 RequestData,但 RequestInformation 只调一次,把它写重了,交互立刻变卡。

4.1 RequestInformation:先把时间轴立起来

时间轴是 CGNS 读取里最别扭的一块。时间值不在 zone 上,而在每个 FlowSolution_t 节点下的 TimeValues 数组里,一个 zone 存一份。三个 zone、每个 zone 十个时刻,文件里就有三十个 TimeValues,还可能顺序不一致。直接拿第一个 zone 的列表当全局时间轴,多 zone 算例必然翻车。

int vtkCGNSPluginReader::RequestInformation( vtkInformation*, vtkInformationVector**, vtkInformationVector* outVec) { int fn = -1; if (cg_open(this->FileName, CG_MODE_READ, &fn) != CG_OK) { vtkErrorMacro("cg_open failed: " << cg_get_error()); return 0; } int nbases = 0; cg_nbases(fn, &nbases); std::set<double> times; // set 自带去重和升序,正好是 TIME_STEPS 要的顺序 this->ZoneSolutionMap.clear(); // key: (base, zone),value: [(solution, time)] for (int b = 1; b <= nbases; ++b) { int cellDim = 0, physDim = 0; char baseName[33] = {0}; cg_base_read(fn, b, &cellDim, &physDim, baseName); int nzones = 0; cg_nzones(fn, b, &nzones); for (int z = 1; z <= nzones; ++z) { int nsols = 0; cg_nsols(fn, b, z, &nsols); for (int s = 1; s <= nsols; ++s) { GridLocation_t loc; char solName[33] = {0}; cg_sol_info(fn, b, z, s, &loc, solName); // TimeValues 挂在 FlowSolution_t 节点下,用 cg_goto 定位后按数组名找 cg_goto(fn, b, "Zone_t", z, "FlowSolution_t", s, ""); int narr = 0; cg_narrays(fn, &narr); for (int a = 1; a <= narr; ++a) { DataType_t dt; int dim = 0; cgsize_t dimVals[3] = {0, 0, 0}; char arrName[33] = {0}; cg_array_info(fn, a, arrName, &dt, &dim, dimVals); if (std::strcmp(arrName, "TimeValues") == 0 && dim == 1) { double t = 0.0; cg_array_read(fn, a, &t); times.insert(t); this->ZoneSolutionMap[{b, z}].emplace_back(s, t); } } } } } cg_close(fn); // 元信息读完就关,不占文件句柄 vtkInformation* outInfo = outVec->GetInformationObject(0); if (!times.empty()) { std::vector<double> tv(times.begin(), times.end()); outInfo->Set(vtkStreamingDemandDrivenPipeline::TIME_STEPS(), tv.data(), static_cast<int>(tv.size())); double range[2] = {tv.front(), tv.back()}; outInfo->Set(vtkStreamingDemandDrivenPipeline::TIME_RANGE(), range, 2); } return 1; }

几个关键点。用 std::set 收集时间不是随手写的,它同时解决去重和排序两件事,而 TIME_STEPS 必须是严格升序的——ParaView 的时间滑块按下标走,数组乱序的时候拖到第 2 格显示第 5 帧,这种问题肉眼很难第一时间反应过来。

ZoneSolutionMap 把「哪个 zone 的哪个 solution 对应哪个时刻」记下来,RequestData 里按当前时间做一次最近邻查找就能定位到具体 solution 序号。这里

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询