VTK 设计模式:迭代器模式
系列:Qt / VTK 设计模式 · VTK 04/10
参考:vtkCollection、vtkCellIterator
引子
在一个复杂的 3D 可视化场景中,可能包含成百上千个 vtkActor 组件;而在一个三维网格(vtkUnstructuredGrid)中,可能包含数百万个混合类型的单元(Tetrahedron, Hexahedron, Wedge)。
如果直接暴露底层数组的数据结构去进行循环操作,不仅代码极其脆弱,而且无法处理复杂的混合拓扑几何。解决此问题的经典设计模式是迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
在 VTK 中,迭代器模式既体现为传统的 vtkCollection 集合遍历,也体现在 VTK 9+ 统一拓扑遍历的 vtkCellIterator 中。
核心痛点与 GoF 迭代器结构
痛点场景:依赖内部存储结构的硬编码
// 坏方案:直接依赖特定数据类型的物理内存布局
for (int i = 0; i < grid->GetNumberOfCells(); ++i) {
// 需要用大量的 switch-case 去区分 12 种不同的单元拓扑类型
int type = grid->GetCellType(i);
if (type == VTK_TETRA) { /* ... */ }
else if (type == VTK_HEXAHEDRON) { /* ... */ }
}- 暴露数据结构:客户端强制绑定底层 SOA(Structure of Arrays)或非连续内存布局。
- 扩展性差:一旦引入新的拓扑单元类型,所有遍历代码都需要修改。
GoF 迭代器模式结构映射
| GoF 迭代器角色 | VTK 对应实现 | 职责说明 |
|---|---|---|
| 抽象迭代器 (Iterator) | vtkCellIterator / vtkCollectionIterator | 定义访问和遍历元素的抽象接口 (InitTraversal, GoToNextCell) |
| 具体迭代器 (ConcreteIterator) | vtkGenericCellIterator | 跟踪当前遍历位置,知道如何提取当前 Cell 的 Points 和 PointIds |
| 抽象容器 (Aggregate) | vtkDataSet / vtkCollection | 定义创建对应迭代器对象的抽象接口 (NewCellIterator()) |
| 具体容器 (ConcreteAggregate) | vtkUnstructuredGrid / vtkActorCollection | 返回具体的迭代器实例,持有物理内存数据 |
底层原理:统一拓扑抽象与 SOA 遍历
vtkCellIterator 在 VTK 9+ 中被引入,用来隐藏不同 vtkDataSet(如 vtkPolyData、vtkUnstructuredGrid)复杂的内存存储差异:
无论底层网格如何交错存储,vtkCellIterator 均提供统一的线性遍历能力,避免了暴露底层 vtkCellArray 的偏移量数组(Offsets & Connectivity)。
完整代码示例
1. 经典集合遍历:vtkActorCollection 遍历
#include <vtkNew.h>
#include <vtkRenderer.h>
#include <vtkActorCollection.h>
#include <vtkActor.h>
#include <iostream>
void setAllActorsVisibility(vtkRenderer* renderer, bool visible) {
if (!renderer) return;
// 获取 Actor 集合容器 (Aggregate)
vtkActorCollection* actors = renderer->GetActors();
if (!actors) return;
// 经典 VTK 遍历惯用法:InitTraversal + GetNextActor
actors->InitTraversal();
while (vtkActor* actor = actors->GetNextActor()) {
actor->SetVisibility(visible ? 1 : 0);
}
}2. 现代拓扑遍历:基于 vtkCellIterator 遍历混合网格
#include <vtkNew.h>
#include <vtkSmartPointer.h>
#include <vtkUnstructuredGrid.h>
#include <vtkCellIterator.h>
#include <vtkIdList.h>
#include <vtkGenericCell.h>
#include <iostream>
void analyzeGridCells(vtkUnstructuredGrid* grid) {
if (!grid) return;
// 通过容器工厂方法创建迭代器
vtkSmartPointer<vtkCellIterator> it = grid->NewCellIterator();
// 初始化迭代器
it->InitTraversal();
int cellCount = 0;
// 统一线性循环遍历,消除庞大的 switch-case
for (; !it->IsDoneWithTraversal(); it->GoToNextCell()) {
cellCount++;
int cellType = it->GetCellType();
vtkIdList* pointIds = it->GetPointIds();
std::cout << "Cell #" << cellCount
<< " | Type: " << cellType
<< " | Points Count: " << pointIds->GetNumberOfIds() << std::endl;
}
}易混淆概念对比
| 遍历方式 | 适用于场景 | 优缺点分析 |
|---|---|---|
vtkCellIterator 迭代器 | 复杂的异构混合网格(包含点、线、四面体、六面体) | 优点:屏蔽结构细节、通用性极强;缺点:存在虚函数虚表开销 |
基于 vtkIdType 的索引遍历 | 简单单质网格(如纯三角面 vtkPolyData) | 优点:直接内存访问、速度极快;缺点:需显式处理数据格式 |
最佳实践与踩坑避坑
- 绝对禁止在遍历过程中修改集合结构:
- 在使用
vtkCollection::GetNextItem()或vtkCellIterator时,如果同时在循环体内部调用RemoveItem()或修改网格拓扑,会导致内部指针错乱或野指针崩溃。正确做法是在循环中记录需要删除的节点 ID,遍历结束后统一清理。
- 在使用
- 多线程并发遍历建议:
vtkCellIterator本身并非线程安全。如果在多线程中并发遍历网格,应使用vtkSMPTools结合索引段划分(Range Partitioning),而非共享同一个迭代器。
重点与注意
重点:
vtkCollection使用InitTraversal()+GetNextProp()是 VTK 传统的遍历模式;vtkCellIterator是 VTK 9+ 处理三维网格遍历的标准迭代器。
重点:迭代器模式把底层几何与拓扑的存储细节隐藏起来,使上层分析算法只依赖统一接口。
注意:大网格高性能计算热路径中,若追求极致性能,可配合vtkSMPTools使用底层 API,避免虚函数开销。
小结
VTK 迭代器模式解耦了数据存储与数据遍历,为三维场景管理与网格分析提供了统一的访问协议。
延伸阅读