VTK 设计模式:原型模式
系列:Qt / VTK 设计模式 · VTK 08/10
参考:vtkDataObject、vtkPolyData::DeepCopy
引子
在网格编辑与拓扑剪裁操作中,我们经常需要“保存当前点云/面片的历史快照(Snapshot)”,或者在多线程并行计算中为每个线程提供一份相互隔离的数据副本——直接通过循环重新生成点和面不仅极其繁琐,而且效率低下。
解决此问题的经典设计模式是原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。在 VTK 中,原型模式体现为 vtkDataObject 及其子类(如 vtkPolyData、vtkImageData)提供的 DeepCopy() 与 ShallowCopy() 语义。
核心痛点与 GoF 原型结构
痛点场景:手动逐元素复制的崩溃
cpp
// 坏方案:手动提取原网格的点数组、单元数组与字段属性
vtkPolyData* newMesh = vtkPolyData::New();
// 需要手动拷贝 Points, Polys, PointData, CellData, FieldData...
// 极易遗漏标量场(Scalars)、法向量(Normals)或纹理坐标(TCoords)- 遗漏属性场:数据对象内部除了几何拓扑,还附带有标量、向量、张量等
vtkFieldData属性,手动复制必定遗漏。 - 浅/深拷贝混淆:无法明确表达“共享物理数组”与“独立分离物理数组”的内存语义。
GoF 原型模式结构映射
| GoF 原型角色 | VTK 对应实现 | 职责说明 |
|---|---|---|
| 抽象原型 (Prototype) | vtkDataObject | 定义克隆/复制对象的统一接口 (DeepCopy(), ShallowCopy()) |
| 具体原型 (ConcretePrototype) | vtkPolyData / vtkImageData / vtkUnstructuredGrid | 实现深/浅拷贝逻辑,拷贝几何拓扑与 vtkFieldData 字段 |
| 客户端 (Client) | 算法 Filter / Undo 撤销重做栈 / 缓存管理器 | 调用原型的 DeepCopy() 方法克隆新快照实例 |
底层原理:DeepCopy vs ShallowCopy 内存映射
VTK 原型拷贝机制分为深拷贝与浅拷贝两种:
ShallowCopy(src):创建新的vtkPolyData外壳对象,但内部的vtkPoints和vtkCellArray物理指针直接指向源对象,引用计数ref + 1。修改其中一方的坐标会影响另一方!DeepCopy(src):创建全新的vtkPolyData外壳,同时申请新的内存空间,对所有点坐标、单元扑拓和标量场数组进行完整的物理内存拷贝。修改拷贝对象绝对不影响源对象。
完整代码示例
1. DeepCopy 与 ShallowCopy 行为对比
cpp
#include <vtkNew.h>
#include <vtkSphereSource.h>
#include <vtkPolyData.h>
#include <vtkPoints.h>
#include <iostream>
int main() {
// 1. 生成原型源对象
vtkNew<vtkSphereSource> sphere;
sphere->Update();
vtkPolyData* original = sphere->GetOutput();
// 2. 浅拷贝 (ShallowCopy):物理内存共享
vtkNew<vtkPolyData> shallowCopy;
shallowCopy->ShallowCopy(original);
// 3. 深拷贝 (DeepCopy):物理内存独立
vtkNew<vtkPolyData> deepCopy;
deepCopy->DeepCopy(original);
std::cout << "Original Point 0: " << original->GetPoint(0)[0] << std::endl;
// 4. 修改浅拷贝对象的节点坐标
shallowCopy->GetPoints()->SetPoint(0, 999.0, 0.0, 0.0);
// 验证影响:原对象与浅拷贝对象同步改变
std::cout << "After Shallow Modification -> Original Point 0: "
<< original->GetPoint(0)[0] << " (Changed!)" << std::endl;
// 验证隔离:深拷贝对象完全不受影响
std::cout << "After Shallow Modification -> DeepCopy Point 0: "
<< deepCopy->GetPoint(0)[0] << " (Isolated)" << std::endl;
return 0;
}2. 基于原型模式实现 Undo/Redo (撤销重做) 快照管理器
cpp
#include <vtkSmartPointer.h>
#include <vtkPolyData.h>
#include <vector>
class MeshUndoManager {
public:
// 压入当前快照原型
void pushSnapshot(vtkPolyData* currentMesh) {
if (!currentMesh) return;
// 实例化新克隆原型对象
auto snapshot = vtkSmartPointer<vtkPolyData>::New();
snapshot->DeepCopy(currentMesh); // 完全独立的物理内存深拷贝
m_undoStack.push_back(snapshot);
}
// 恢复上一次快照
bool restoreLastSnapshot(vtkPolyData* targetMesh) {
if (m_undoStack.empty() || !targetMesh) return false;
auto lastSnapshot = m_undoStack.back();
m_undoStack.pop_back();
targetMesh->DeepCopy(lastSnapshot);
return true;
}
private:
std::vector<vtkSmartPointer<vtkPolyData>> m_undoStack;
};易混淆概念对比
| 原型拷贝 API | 内部内存分配 | 依赖关系 | 适用场景 |
|---|---|---|---|
ShallowCopy | 零新物理数组分配(共享指针) | 数据互相绑定 | 临时只读管道分支传递、节省内存 |
DeepCopy | 全量深拷贝新物理数组 | 完全物理隔离 | 撤销重做快照、多线程独立写编辑、状态隔离 |
最佳实践与踩坑避坑
- 多线程并发编辑必须使用
DeepCopy:- 如果多个工作线程并发处理三维网格,绝对不能使用
ShallowCopy。因为即使是简单的网格平滑或标量赋值,也会导致多线程同时修改同一个物理内存块,引发严重的数据死锁或内存崩溃。
- 如果多个工作线程并发处理三维网格,绝对不能使用
- MTime (修改时间戳) 的刷新:
- 执行
DeepCopy()后,目标对象的修改时间戳MTime会更新。如果目标对象连接在 VTK 渲染管线中,管线会自动识别其变动并触发重新绘制。
- 执行
重点与注意
重点:
DeepCopy()= 完全物理隔离的副本;ShallowCopy()= 共享底层数组句柄的浅拷贝。
重点:实现撤销重做(Undo/Redo)快照或多线程数据隔离时,必须使用DeepCopy()。
注意:大网格(千万级点面)执行DeepCopy()内存与时间开销巨大,热路径中应评估是否只需拷贝特定vtkDataArray。
小结
VTK 原型模式为复杂的几何与属性场拷贝提供了标准化的深度与浅度克隆接口,是状态备份与并发处理的基石。
延伸阅读