ParaView CGNS Reader插件:多zone、边界命名与时间序列 简介这是一款面向科学可视化与CFD数据处理场景的小型ParaView插件使用C实现主要解决CGNS格式网格与数据的读取问题。其设计受VisIt CGNS阅读器启发但改用低级CGNS API加载DataSet以降低内存占用支持多块结构化/非结构化网格、基本时间步、SIDS命名向量并可在单处理器下加载非结构化网格边界补丁适合需要快速查看CGNS算例或进行插件二次开发的工程师与研究者。压缩包共16个文件、约166KB包含cxx/h源码、CMake构建脚本、XML插件描述、说明文档及示例图片等体量轻便便于集成与编译。目前已有1419人学习读者可据此了解ParaView插件组织方式、CGNS数据映射到VTK对象的实现路径以及多块网格与边界补丁的处理思路并可作为自建读取器的参考起点。1. 手里一堆 CGNSParaView 打开能看见网格但边界条件名全变成 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 的 XMLReaderFactory 决定它出现在哪XML 是插件和 ParaView 之间的契约。它声明有哪些属性、属性对应哪个 setter、这个 proxy 出现在 Open 对话框的哪个过滤项里。写错一个标签插件照样加载但菜单里找不到这类问题最费时间。ServerManagerConfiguration ProxyGroup namesources SourceProxy nameCGNSPluginReader classvtkCGNSPluginReader labelCGNS Reader (Plugin) Documentation Reads CGNS bases/zones/patches into a multiblock dataset. /Documentation !-- 文件名由 Open 对话框自动填入不在属性面板里重复展示 -- StringVectorProperty nameFileName commandSetFileName number_of_elements1 panel_visibilitynever/ !-- mm 转 m 之类缩放放在读取阶段比后面用 Calculator 干净 -- DoubleVectorProperty nameScaleFactor commandSetScaleFactor number_of_elements1 default_values1.0/ !-- 只要网格不要场时关掉内存能省一半 -- IntVectorProperty nameLoadSolution commandSetLoadSolution number_of_elements1 default_values1/ Hints ReaderFactory extensionscgns file_descriptionCGNS 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_TYPERelease \ -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::setdouble 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::vectordouble tv(times.begin(), times.end()); outInfo-Set(vtkStreamingDemandDrivenPipeline::TIME_STEPS(), tv.data(), static_castint(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 序号。这里本文还有配套的精品资源点击获取