Skip to content

VTK 设计模式:迭代器模式

系列:Qt / VTK 设计模式 · VTK 04/10
参考:vtkCollectionvtkCellIterator


引子

在一个复杂的 3D 可视化场景中,可能包含成百上千个 vtkActor 组件;而在一个三维网格(vtkUnstructuredGrid)中,可能包含数百万个混合类型的单元(Tetrahedron, Hexahedron, Wedge)。

如果直接暴露底层数组的数据结构去进行循环操作,不仅代码极其脆弱,而且无法处理复杂的混合拓扑几何。解决此问题的经典设计模式是迭代器模式(Iterator Pattern)提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示

在 VTK 中,迭代器模式既体现为传统的 vtkCollection 集合遍历,也体现在 VTK 9+ 统一拓扑遍历的 vtkCellIterator 中。


核心痛点与 GoF 迭代器结构

痛点场景:依赖内部存储结构的硬编码

cpp
// 坏方案:直接依赖特定数据类型的物理内存布局
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(如 vtkPolyDatavtkUnstructuredGrid)复杂的内存存储差异:

无论底层网格如何交错存储,vtkCellIterator 均提供统一的线性遍历能力,避免了暴露底层 vtkCellArray 的偏移量数组(Offsets & Connectivity)。


完整代码示例

1. 经典集合遍历:vtkActorCollection 遍历

cpp
#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 遍历混合网格

cpp
#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优点:直接内存访问、速度极快;缺点:需显式处理数据格式

最佳实践与踩坑避坑

  1. 绝对禁止在遍历过程中修改集合结构
    • 在使用 vtkCollection::GetNextItem()vtkCellIterator 时,如果同时在循环体内部调用 RemoveItem() 或修改网格拓扑,会导致内部指针错乱或野指针崩溃。正确做法是在循环中记录需要删除的节点 ID,遍历结束后统一清理
  2. 多线程并发遍历建议
    • vtkCellIterator 本身并非线程安全。如果在多线程中并发遍历网格,应使用 vtkSMPTools 结合索引段划分(Range Partitioning),而非共享同一个迭代器。

重点与注意

重点vtkCollection 使用 InitTraversal() + GetNextProp() 是 VTK 传统的遍历模式;vtkCellIterator 是 VTK 9+ 处理三维网格遍历的标准迭代器。
重点:迭代器模式把底层几何与拓扑的存储细节隐藏起来,使上层分析算法只依赖统一接口。
注意:大网格高性能计算热路径中,若追求极致性能,可配合 vtkSMPTools 使用底层 API,避免虚函数开销。


小结

VTK 迭代器模式解耦了数据存储与数据遍历,为三维场景管理与网格分析提供了统一的访问协议。

延伸阅读

基于 VitePress 强力驱动 | 记录技术与生活