开源应用程序架构(卷 1)
VTK

Berk Geveci 和 Will Schroeder

可视化工具包 (VTK) 是一种广泛使用的软件系统,用于数据处理和可视化。它被用于科学计算、医学图像分析、计算几何、渲染、图像处理和信息学。在本节中,我们将简要概述 VTK,包括一些使它成为成功系统的基本设计模式。

要真正理解一个软件系统,不仅要了解它解决什么问题,还要了解它诞生的特定文化。对于 VTK 而言,该软件表面上是作为科学数据的 3D 可视化系统而开发的。但它诞生的文化背景为这项工作增加了重要的背景故事,并有助于解释为什么该软件的设计和部署方式与现在一样。

在 VTK 被构思和编写时,它的最初作者(Will Schroeder、Ken Martin、Bill Lorensen)是通用电气公司研发部的研究人员。我们投入了大量资金在一个名为 LYMB 的前身系统中,它是在 C 编程语言中实现的类似 Smalltalk 的环境。虽然这对当时的系统来说是一个很棒的系统,但作为研究人员,我们在尝试推广我们的工作时始终受到两个主要障碍的阻碍:1) IP 问题和 2) 非标准、专有软件。IP 问题是一个问题,因为一旦公司律师介入,试图将软件分发到通用电气以外几乎是不可能的。其次,即使我们在通用电气内部部署软件,我们许多客户也不愿意学习专有、非标准系统,因为掌握它的努力不会随着员工离开公司而转移,而且它没有标准工具集的广泛支持。因此,VTK 最初的动机是开发一个开放标准或协作平台,通过该平台我们可以轻松地将技术转移给我们的客户。因此,为 VTK 选择一个开源许可证可能是我们做出的最重要的设计决策。

回顾起来,最终选择非互惠、许可性许可证(即 BSD 而不是 GPL)是作者做出的一个典范决定,因为它最终促成了成为 Kitware 的服务和咨询型业务。在我们做出决定的当时,我们主要对降低与学者、研究实验室和商业实体合作的障碍感兴趣。此后我们发现,许多组织避免使用互惠许可证,因为它们可能会造成潜在的混乱。事实上,我们认为互惠许可证在很大程度上阻碍了开源软件的接受,但这又是另一个话题了。这里要说明的是:与任何软件系统相关的重大设计决策之一是版权许可证的选择。重要的是要回顾项目的目标,然后适当地解决 IP 问题。

24.1. 什么是 VTK?

VTK 最初被构思为一个科学数据可视化系统。许多该领域以外的人天真地认为可视化是一种特殊类型的几何渲染:检查虚拟物体并与之交互。虽然这确实是可视化的一部分,但总的来说,数据可视化包括将数据转换为感官输入的整个过程,通常是图像,但也包括触觉、听觉和其他形式。数据形式不仅包括几何和拓扑结构,包括网格或复杂空间分解等抽象,还包括核心结构的属性,例如标量(例如温度或压力)、矢量(例如速度)、张量(例如应力和应变)以及渲染属性,例如表面法线和纹理坐标。

请注意,表示时空信息的通常被认为是科学可视化的一部分。但是,还有更多抽象的数据形式,例如营销人口统计、网页、文档和其他信息,这些信息只能通过抽象(即非时空)关系表示,例如非结构化文档、表格、图形和树。这些抽象数据通常由信息可视化方法处理。在社区的帮助下,VTK 现在能够实现科学可视化和信息可视化。

作为可视化系统,VTK 的作用是获取这些形式的数据,并最终将它们转换为人类感官器官可以理解的形式。因此,VTK 的核心要求之一是它能够创建能够摄取、处理、表示并最终渲染数据的数据流管道。因此,该工具包必然被设计为一个灵活的系统,其设计在许多层面上反映了这一点。例如,我们有意将 VTK 设计为一个工具包,其中包含许多可互换的组件,可以组合起来处理各种数据。

24.2. 架构特征

在深入研究 VTK 的特定架构特征之前,有一些高级概念会对开发和使用该系统产生重大影响。其中之一是 VTK 的混合包装器设施。该设施从 VTK 的 C++ 实现中自动生成对 Python、Java 和 Tcl 的语言绑定(可以并且已经添加了其他语言)。大多数功能强大的开发人员将在 C++ 中工作。用户和应用程序开发人员可以使用 C++,但通常更喜欢上述解释语言。这种混合编译/解释环境结合了两个世界的优点:高性能计算密集型算法以及在原型设计或开发应用程序时的灵活性。事实上,这种多语言计算方法受到了科学计算界许多人的青睐,他们经常将 VTK 作为开发自己软件的模板。

在软件流程方面,VTK 已经采用 CMake 来控制构建;CDash/CTest 用于测试;CPack 用于跨平台部署。事实上,VTK 几乎可以在任何计算机上编译,包括超级计算机,这些超级计算机通常是臭名昭著的原始开发环境。此外,网页、wiki、邮件列表(用户和开发者)、文档生成设施(即 Doxygen)和错误跟踪器 (Mantis) 完善了开发工具。

24.2.1. 核心功能

由于 VTK 是一个面向对象的系统,因此在 VTK 中对类和实例数据成员的访问受到严格控制。通常,所有数据成员都是受保护的或私有的。对它们的访问是通过SetGet 方法进行的,针对布尔数据、模态数据、字符串和向量有特殊变体。这些方法中的许多实际上是通过将宏插入类头文件中创建的。例如

vtkSetMacro(Tolerance,double);
vtkGetMacro(Tolerance,double);

扩展后的

virtual void SetTolerance(double);
virtual double GetTolerance();

除了代码清晰之外,使用这些宏还有很多原因。在 VTK 中,有重要的数据成员控制调试、更新对象的修改时间 (MTime) 以及正确管理引用计数。这些宏正确地操作这些数据,建议高度使用它们。例如,VTK 中一个特别有害的错误是,当对象的 MTime 没有被正确管理时。在这种情况下,代码可能不会在应该执行时执行,或者可能执行得太频繁。

VTK 的优势之一是它表示和管理数据的相对简单方法。通常,各种特定类型的数据数组(例如vtkFloatArray)用于表示连续的信息块。例如,三个 XYZ 点的列表将用一个包含九个条目的vtkFloatArray 表示(x,y,z, x,y,z 等)。在这些数组中存在元组的概念,因此 3D 点是 3 元组,而 3×3 对称张量矩阵用 6 元组表示(其中对称空间节省是可能的)。采用这种设计是有意为之的,因为在科学计算中,通常会与操作数组的系统(例如 Fortran)进行接口,并且在大型连续块中分配和释放内存要高效得多。此外,通信、序列化和执行 IO 通常使用连续数据效率更高。这些核心数据数组(各种类型)代表了 VTK 中的大部分数据,并且具有一系列方便的方法用于插入和访问信息,包括快速访问方法和在添加更多数据时根据需要自动分配内存的方法。数据数组是vtkDataArray 抽象类的子类,这意味着可以使用通用虚拟方法来简化编码。但是,为了获得更高的性能,使用静态、模板函数,这些函数根据类型进行切换,随后直接访问连续数据数组。

通常,C++ 模板在公共类 API 中不可见;尽管模板被广泛用于性能原因。这也适用于 STL:我们通常采用 PIMPL1 设计模式,以将模板实现的复杂性隐藏在用户或应用程序开发人员面前。这在我们之前描述的将代码包装到解释代码时特别有用。避免在公共 API 中使用模板的复杂性意味着,从应用程序开发人员的角度来看,VTK 实现基本上没有数据类型选择复杂性。当然,在幕后,代码执行由数据类型驱动,数据类型通常在访问数据时在运行时确定。

一些用户想知道为什么 VTK 对内存管理使用引用计数,而不是更友好的方法,例如垃圾回收。基本答案是,VTK 需要完全控制何时删除数据,因为数据大小可能很大。例如,大小为 1000×1000×1000 字节的数据体积是 1 GB。在垃圾收集器决定是否释放数据时,让这些数据闲置不是一个好主意。在 VTK 中,大多数类(vtkObject 的子类)都具有内置的引用计数功能。每个对象都包含一个引用计数,在实例化对象时将其初始化为 1。每次注册对象的使用时,引用计数都会增加 1。类似地,当注销对象的使用(或者等效地删除对象)时,引用计数会减少 1。最终,对象的引用计数会减少到 0,此时它会自我销毁。一个典型的例子如下所示

vtkCamera *camera = vtkCamera::New();   //reference count is 1
camera->Register(this);                 //reference count is 2
camera->Unregister(this);               //reference count is 1
renderer->SetActiveCamera(camera);      //reference count is 2
renderer->Delete();                     //ref count is 1 when renderer is deleted
camera->Delete();                       //camera self destructs

引用计数对 VTK 很重要的另一个重要原因是它提供了高效复制数据的能力。例如,想象一个包含多个数据数组的数据对象 D1:点、多边形、颜色、标量和纹理坐标。现在想象一下,处理这些数据以生成一个新的数据对象 D2,它与第一个数据对象相同,并且还添加了矢量数据(位于点上)。一种浪费的方法是完全(深层)复制 D1 以创建 D2,然后将新的矢量数据数组添加到 D2。或者,我们创建一个空的 D2,然后使用引用计数来跟踪数据所有权,将 D1 中的数组传递到 D2(浅层复制),最后将新的矢量数据数组添加到 D2。后一种方法避免了复制数据,正如我们之前所述,这对于良好的可视化系统至关重要。正如我们将在本章后面看到的那样,数据处理管道会定期执行这种操作,即从算法的输入复制数据到输出,因此引用计数对于 VTK 至关重要。

当然,引用计数也存在一些臭名昭著的问题。偶尔会出现引用循环,循环中的对象以相互支持的配置相互引用。在这种情况下,需要智能干预,或者在 VTK 的情况下,使用在vtkGarbageCollector 中实现的特殊设施来管理参与循环的对象。当识别出这样的类时(这在开发期间是预期的),该类会向垃圾收集器注册自己,并覆盖自己的RegisterUnRegister 方法。然后,随后的对象删除(或注销)方法对本地引用计数网络进行拓扑分析,搜索相互引用的对象的独立岛屿。然后,这些岛屿会被垃圾收集器删除。

大多数 VTK 中的实例化是通过实现为静态类成员的对象工厂来执行的。典型的语法如下所示

vtkLight *a = vtkLight::New();

这里要认识到的重要一点是,实际实例化的对象可能不是vtkLight,它可能是vtkLight的子类(例如,vtkOpenGLLight)。对象工厂有各种各样的动机,其中最重要的是应用程序的可移植性和设备无关性。例如,在上面的例子中,我们在渲染场景中创建了一个灯光。在特定平台上的特定应用程序中,vtkLight::New可能会生成一个OpenGL灯光,但是,在不同的平台上,可能会使用其他渲染库或方法来在图形系统中创建灯光。要实例化的确切派生类是运行时系统信息的函数。在VTK的早期,有许多选择,包括gl,PHIGS,Starbase,XGL和OpenGL。虽然这些选择中的大多数现在已经消失了,但新的方法出现了,包括DirectX和基于GPU的方法。随着时间的推移,使用VTK编写的应用程序不必更改,因为开发人员已经派生了新的设备特定子类到vtkLight和其他渲染类,以支持不断发展的技术。对象工厂的另一个重要用途是启用性能增强变体的运行时替换。例如,vtkImageFFT可能会被访问专用硬件或数值库的类所取代。

24.2.2. 数据表示

VTK的优势之一是它能够表示复杂形式的数据。这些数据形式范围从简单的表格到复杂结构,例如有限元网格。所有这些数据形式都是vtkDataObject的子类,如图24.1所示(注意,这是许多数据对象类的部分继承图)。

[Data Object Classes]

图 24.1: 数据对象类

vtkDataObject最重要的特征之一是它可以在可视化管道中进行处理(下一小节)。在显示的许多类中,只有少数几个通常在大多数现实世界应用程序中使用。vtkDataSet及其派生类用于科学可视化(图24.2)。例如,vtkPolyData用于表示多边形网格;vtkUnstructuredGrid用于表示网格,vtkImageData表示2D和3D像素和体素数据。

[Data Set Classes]

图 24.2: 数据集类

24.2.3. 管道架构

VTK由几个主要子系统组成。可能与可视化软件包最相关的子系统是数据流/管道架构。从概念上讲,管道架构由三类基本对象组成:用于表示数据的对象(上面讨论的vtkDataObject)、用于处理、转换、过滤或将数据对象从一种形式映射到另一种形式的对象(vtkAlgorithm);以及用于执行管道(vtkExecutive)的对象,它控制互连的数据和处理对象的连接图(即管道)。图 24.3描绘了一个典型的管道。

[Typical Pipeline]

图 24.3: 典型的管道

虽然从概念上讲很简单,但实际上实现管道架构具有挑战性。原因之一是数据的表示可能很复杂。例如,一些数据集由数据的层次结构或分组组成,因此跨数据执行需要非平凡的迭代或递归。更复杂的是,并行处理(无论是使用共享内存还是可扩展的分布式方法)都需要将数据划分为多个部分,这些部分可能需要重叠以一致地计算边界信息,例如导数。

算法对象也引入了它们自身的特殊复杂性。某些算法可能需要多个输入和/或产生不同类型的多个输出。有些可以在数据上本地运行(例如,计算单元的中心),而另一些则需要全局信息,例如,计算直方图。在所有情况下,算法都将它们的输入视为不可变的,算法仅读取它们的输入以生成它们的输出。这是因为数据可能作为多个算法的输入可用,而且一个算法覆盖另一个算法的输入不是一个好主意。

最后,执行器可能会很复杂,具体取决于执行策略的细节。在某些情况下,我们可能希望缓存过滤器之间的中间结果。这最大程度地减少了如果管道中的某些内容发生变化,必须执行的重新计算量。另一方面,可视化数据集可能非常大,在这种情况下,我们可能希望在不再需要计算时释放数据。最后,还有一些复杂的执行策略,例如数据的多分辨率处理,这需要管道以迭代方式运行。

为了演示其中的一些概念并进一步解释管道架构,请考虑以下 C++ 示例

vtkPExodusIIReader *reader = vtkPExodusIIReader::New();
reader->SetFileName("exampleFile.exo");

vtkContourFilter *cont = vtkContourFilter::New();
cont->SetInputConnection(reader->GetOutputPort());
cont->SetNumberOfContours(1);
cont->SetValue(0, 200);

vtkQuadricDecimation *deci = vtkQuadricDecimation::New();
deci->SetInputConnection(cont->GetOutputPort());
deci->SetTargetReduction( 0.75 );

vtkXMLPolyDataWriter *writer = vtkXMLPolyDataWriter::New();
writer->SetInputConnection(deci->GetOuputPort());
writer->SetFileName("outputFile.vtp");
writer->Write();

在这个例子中,一个读取器对象读取一个大型非结构化网格(或网格)数据文件。下一个过滤器从网格中生成一个等值面。vtkQuadricDecimation过滤器通过对等值面进行抽取(即,减少表示等值线的三角形的数量)来减小等值面的尺寸,等值面是一个多边形数据集。最后,在抽取后,新的、减小后的数据文件被写回磁盘。实际的管道执行发生在写入器调用Write方法时(即,在需要数据时)。

如本例所示,VTK 的管道执行机制是按需驱动的。当一个接收器(例如写入器或映射器(数据渲染对象))需要数据时,它会向它的输入请求数据。如果输入过滤器已经拥有适当的数据,它只是将执行控制返回给接收器。但是,如果输入没有适当的数据,则需要计算它。因此,它必须首先向其输入请求数据。此过程将继续沿管道上游进行,直到达到具有“适当数据”的过滤器或源,或者直到管道开头,此时过滤器将按正确顺序执行,数据将流到管道中请求它的位置。

这里我们应该扩展“适当数据”的含义。默认情况下,在 VTK 源或过滤器执行后,其输出将被管道缓存,以避免将来不必要的执行。这样做是为了最大限度地减少计算和/或 I/O,但以内存为代价,并且是可配置的行为。管道不仅缓存数据对象,还缓存有关生成这些数据对象的条件的元数据。这些元数据包括一个时间戳(即,ComputeTime),它捕获数据对象何时被计算。所以在最简单的情况下,“适当数据”是在管道上游的所有对象修改后计算出来的数据。通过考虑以下示例,更容易演示此行为。让我们在前面的 VTK 程序末尾添加以下内容

vtkXMLPolyDataWriter *writer2 = vtkXMLPolyDataWriter::New();
writer2->SetInputConnection(deci->GetOuputPort());
writer2->SetFileName("outputFile2.vtp");
writer2->Write();

如前所述,第一个writer->Write调用会导致整个管道执行。当调用writer2->Write()时,管道将意识到,当它将缓存的抽取过滤器的输出的时间戳与抽取过滤器、等值面过滤器和读取器的时间戳进行比较时,缓存的抽取过滤器的输出是最新的。因此,数据请求不必传播到writer2之外。现在,让我们考虑以下更改。

cont->SetValue(0, 400);

vtkXMLPolyDataWriter *writer2 = vtkXMLPolyDataWriter::New();
writer2->SetInputConnection(deci->GetOuputPort());
writer2->SetFileName("outputFile2.vtp");
writer2->Write();

现在,管道执行器将意识到,等值面过滤器在最后执行等值面过滤器和抽取过滤器的输出之后被修改了。因此,这两个过滤器的缓存已过时,必须重新执行它们。但是,由于读取器在等值面过滤器之前没有被修改,所以它的缓存是有效的,因此读取器不必重新执行。

这里描述的场景是按需驱动的管道的最简单示例。VTK 的管道要复杂得多。当过滤器或接收器需要数据时,它可以提供额外的信息来请求特定的数据子集。例如,过滤器可以通过流式传输数据片段来执行核心外分析。让我们更改前面的示例以演示。

vtkXMLPolyDataWriter *writer = vtkXMLPolyDataWriter::New();
writer->SetInputConnection(deci->GetOuputPort());
writer->SetNumberOfPieces(2);

writer->SetWritePiece(0);
writer->SetFileName("outputFile0.vtp");
writer->Write();

writer->SetWritePiece(1);
writer->SetFileName("outputFile1.vtp");
writer->Write();

这里,写入器要求上游管道加载并处理数据,每个数据都独立地流式传输成两部分。您可能已经注意到,前面描述的简单执行逻辑在这里行不通。按照此逻辑,当第二次调用Write函数时,管道不应该重新执行,因为上游没有发生任何变化。因此,为了解决这种更复杂的情况,执行器具有额外的逻辑来处理这种片请求。VTK 的管道执行实际上由多个阶段组成。数据对象的计算实际上是最后一个阶段。之前的阶段是请求阶段。在这里,接收器和过滤器可以告诉上游它们想要从即将进行的计算中得到什么。在上面的示例中,写入器将通知其输入它想要 2 的第 0 片。此请求实际上会一直传播到读取器。当管道执行时,读取器将知道它需要读取数据的子集。此外,有关缓存数据对应于哪个片段的信息存储在对象的元数据中。下次过滤器向其输入请求数据时,将比较此元数据与当前请求。因此,在此示例中,管道将重新执行以处理不同的片段请求。

过滤器可以发出几种类型的请求。这些请求包括对特定时间步长、特定结构化范围或幽灵层数(即,用于计算邻域信息的边界层)的请求。此外,在请求阶段,每个过滤器都允许修改来自下游的请求。例如,无法流式传输的过滤器(例如,流线过滤器)可以忽略片段请求并请求整个数据。

24.2.4. 渲染子系统

乍一看,VTK 具有简单的面向对象的渲染模型,其中类对应于构成 3D 场景的组件。例如,vtkActor是通过vtkRenderervtkCamera一起渲染的对象,可能在vtkRenderWindow中存在多个vtkRenderer。场景由一个或多个vtkLight照亮。每个vtkActor的位置由vtkTransform控制,演员的外观通过vtkProperty指定。最后,演员的几何表示由vtkMapper定义。映射器在 VTK 中起着重要作用,它们用于终止数据处理管道,以及与渲染系统进行交互。请考虑以下示例,其中我们对数据进行抽取并将结果写入文件,然后使用映射器可视化并与结果进行交互

vtkOBJReader *reader = vtkOBJReader::New();
reader->SetFileName("exampleFile.obj");

vtkTriangleFilter *tri = vtkTriangleFilter::New();
tri->SetInputConnection(reader->GetOutputPort());

vtkQuadricDecimation *deci = vtkQuadricDecimation::New();
deci->SetInputConnection(tri->GetOutputPort());
deci->SetTargetReduction( 0.75 );

vtkPolyDataMapper *mapper = vtkPolyDataMapper::New();
mapper->SetInputConnection(deci->GetOutputPort());

vtkActor *actor = vtkActor::New();
actor->SetMapper(mapper);

vtkRenderer *renderer = vtkRenderer::New();
renderer->AddActor(actor);

vtkRenderWindow *renWin = vtkRenderWindow::New();
renWin->AddRenderer(renderer);

vtkRenderWindowInteractor *interactor = vtkRenderWindowInteractor::New();
interactor->SetRenderWindow(renWin);

renWin->Render();

这里创建了一个单独的演员、渲染器和渲染窗口,以及一个将管道连接到渲染系统的映射器。还要注意vtkRenderWindowInteractor的添加,该类的实例捕获鼠标和键盘事件,并将它们转换为摄像机操作或其他操作。此转换过程通过vtkInteractorStyle定义(稍后详细介绍)。默认情况下,幕后设置了许多实例和数据值。例如,会构造一个单位变换,以及一个默认(头部)灯光和属性。

随着时间的推移,此对象模型变得更加复杂。复杂性的很大一部分来自开发专门用于渲染过程某个方面的派生类。vtkActor现在是vtkProp(就像舞台上的道具)的专门化,并且有一系列这样的道具用于渲染 2D 叠加图形和文本、专门的 3D 对象,甚至用于支持高级渲染技术,例如体积渲染或 GPU 实现(参见图 24.4)。

类似地,随着 VTK 支持的数据模型的增长,与渲染系统交互数据的各种映射器也随之发展。另一个显著扩展的领域是变换层次结构。最初是一个简单的线性 4×4 变换矩阵,现在已成为一个强大的层次结构,支持包括薄板样条变换在内的非线性变换。例如,最初的 vtkPolyDataMapper 具有特定于设备的子类(例如,vtkOpenGLPolyDataMapper)。近年来,它已被一个称为“画家”管道的复杂图形管道所取代,该管道在 图 24.4 中进行了说明。

[Display Classes]

图 24.4:显示类

画家设计支持各种渲染数据的技术,这些技术可以组合在一起以提供特殊的渲染效果。这种功能大大超过了 1994 年最初实现的简单的 vtkPolyDataMapper

可视化系统另一个重要方面是选择子系统。在 VTK 中,存在一个“拾取器”层次结构,大致分为基于硬件方法选择 vtkProp 的对象和基于软件方法(例如,射线投射)选择 vtkProp 的对象;以及在拾取操作后提供不同级别信息的物体。例如,一些拾取器只提供 XYZ 世界空间中的位置,而不指示它们选择了哪个 vtkProp;另一些拾取器不仅提供所选的 vtkProp,还提供构成定义道具几何体的网格的特定点或单元。

24.2.5. 事件和交互

与数据交互是可视化的重要组成部分。在 VTK 中,这可以通过多种方式实现。在最简单的层面上,用户可以通过命令(命令/观察者设计模式)观察事件并做出适当的响应。所有 vtkObject 的子类都维护一个观察者列表,这些观察者向该对象注册自己。在注册过程中,观察者会指示他们感兴趣的特定事件,并添加一个相关命令,如果事件发生,该命令就会被调用。为了了解其工作原理,请考虑以下示例,其中一个过滤器(这里是一个多边形简化过滤器)有一个观察者,它会观察三个事件 StartEventProgressEventEndEvent。当过滤器开始执行时、在执行期间定期执行以及在执行完成后,会调用这些事件。在以下示例中,vtkCommand 类有一个 Execute 方法,它会打印出与执行算法所需时间相关的适当信息

class vtkProgressCommand : public vtkCommand
{
  public:
    static vtkProgressCommand *New() { return new vtkProgressCommand; }
    virtual void Execute(vtkObject *caller, unsigned long, void *callData)
    {
      double progress = *(static_cast<double*>(callData));
      std::cout << "Progress at " << progress<< std::endl;
    }
};

vtkCommand* pobserver = vtkProgressCommand::New();

vtkDecimatePro *deci = vtkDecimatePro::New();
deci->SetInputConnection( byu->GetOutputPort() );
deci->SetTargetReduction( 0.75 );
deci->AddObserver( vtkCommand::ProgressEvent, pobserver );

虽然这是一种原始的交互形式,但它是许多使用 VTK 的应用程序的基础元素。例如,上面的简单代码可以很容易地转换为显示和管理 GUI 进度条。此命令/观察者子系统也是 VTK 中 3D 小部件的核心,这些小部件是用于查询、操作和编辑数据的复杂交互对象,将在下面介绍。

参考上面的示例,需要注意的是,VTK 中的事件是预定义的,但有一个用于用户定义事件的后门。vtkCommand 类定义了枚举事件集(例如,上面示例中的 vtkCommand::ProgressEvent)以及用户事件。UserEvent 只是一个整数值,通常用作一组应用程序用户定义事件的起始偏移值。因此,例如 vtkCommand::UserEvent+100 可能指的是 VTK 定义事件集之外的特定事件。

从用户的角度来看,VTK 小部件看起来像是场景中的一个角色,只是用户可以通过操作手柄或其他几何特征(手柄操作和几何特征操作基于前面描述的拾取功能)与之交互。与该小部件的交互相当直观:用户抓住球形手柄并移动它们,或抓住线条并移动它。然而,在幕后,事件会发出(例如,InteractionEvent),并且经过适当编程的应用程序可以观察这些事件,然后采取相应的行动。例如,它们通常在 vtkCommand::InteractionEvent 上触发,如下所示

vtkLW2Callback *myCallback = vtkLW2Callback::New();
  myCallback->PolyData = seeds;    // streamlines seed points, updated on interaction
  myCallback->Actor = streamline;  // streamline actor, made visible on interaction

vtkLineWidget2 *lineWidget = vtkLineWidget2::New();
  lineWidget->SetInteractor(iren);
  lineWidget->SetRepresentation(rep);
  lineWidget->AddObserver(vtkCommand::InteractionEvent,myCallback);

VTK 小部件实际上是使用两个对象构建的:vtkInteractorObserver 的子类和 vtkProp 的子类。vtkInteractorObserver 只观察渲染窗口中的用户交互(即鼠标和键盘事件)并处理它们。vtkProp(即角色)的子类只是由 vtkInteractorObserver 操作。通常,这种操作包括修改 vtkProp 的几何形状,包括突出显示手柄、更改光标外观和/或变换数据。当然,小部件的细节要求编写子类来控制小部件行为的细微差别,并且系统中目前有 50 多种不同的小部件。

24.2.6. 库的总结

VTK 是一个大型软件工具包。目前该系统包含大约 150 万行代码(包括注释但不包括自动生成的包装软件),以及大约 1000 个 C++ 类。为了管理系统的复杂性并减少构建和链接时间,系统被划分为几十个子目录。表 24.1 列出了这些子目录,并简要概述了库提供的功能。

Common 核心 VTK 类
Filtering 用于管理管道数据流的类
Rendering 渲染、拾取、图像查看和交互
VolumeRendering 体绘制技术
Graphics 3D 几何处理
GenericFiltering 非线性 3D 几何处理
Imaging 成像管道
Hybrid 需要图形和成像功能的类
Widgets 复杂的交互
IO VTK 输入和输出
Infovis 信息可视化
Parallel 并行处理(控制器和通信器)
Wrapping 支持 Tcl、Python 和 Java 包装
Examples 广泛的、有良好文档的示例

表 24.1:VTK 子目录

24.3. 回顾/展望

VTK 一直是一个非常成功的系统。虽然第一行代码是在 1993 年编写的,但在撰写本文时,VTK 仍然发展强劲,而且发展速度还在加快。2 在本节中,我们将讨论一些经验教训和未来的挑战。

24.3.1. 管理增长

VTK 冒险中最令人惊讶的方面之一是该项目的寿命。发展速度归因于几个主要原因

虽然增长令人兴奋,证实了软件系统的创建,并为 VTK 的未来预示着良好,但它可能非常难以管理。因此,VTK 的近期目标更多地集中在管理社区的增长以及软件本身。在这方面已采取了一些措施。

首先,正在创建正式的管理结构。已建立了一个架构审查委员会,指导社区和技术的发展,侧重于高级战略问题。VTK 社区还正在建立一个公认的主题负责人团队,指导特定 VTK 子系统的技术开发。

其次,计划进一步模块化工具包,部分原因是 git 引入的工作流程功能,但也为了认识到用户和开发人员通常希望使用工具包的小子系统,并且不希望针对整个包进行构建和链接。此外,为了支持不断增长的社区,重要的是要支持新的功能和子系统的贡献,即使它们不一定是工具包核心的部分。通过创建一个松散的、模块化的模块集合,可以容纳外围的大量贡献,同时保持核心稳定性。

24.3.2. 技术添加

除了软件流程之外,开发管道中还有许多技术创新。

24.3.3. 开放科学

最后,Kitware 和更广泛的 VTK 社区致力于开放科学。实际上,这意味着我们将推广开放数据、开放出版和开源——这些功能对于确保我们创建可重复的科学系统是必要的。虽然 VTK 长期以来一直作为开源和开放数据系统发布,但文档过程一直很缺乏。虽然有一些不错的书籍 [Kit10,SML06],但收集技术出版物(包括新的源代码贡献)的方法一直多种多样。我们通过开发新的出版机制(如 VTK 杂志3)来改进这种情况,该机制使文章能够包含文档、源代码、数据和有效的测试图像。该杂志还支持对代码进行自动审查(使用 VTK 的质量软件测试流程)以及对提交内容的人工审查。

24.3.4. 经验教训

虽然 VTK 已经取得了成功,但我们有很多事情做得不好

像 VTK 这样的开源系统的一大优点是,随着时间的推移,许多这些错误可以并且将会得到纠正。我们有一个积极、有能力的开发社区,他们每天都在改进系统,我们预计这种情况将在可预见的未来持续下去。

脚注

  1. http://en.wikipedia.org/wiki/Opaque_pointer.
  2. 查看最新的 VTK 代码分析,网址为http://www.ohloh.net/p/vtk/analyses/latest
  3. http://www.midasjournal.org/?journal=35