@H_301_1@
4.2 可视化管线
我们回头再看看3.1的示例RenderCylinder。在这个例子及后续的扩展内容里,我们可以找到以下列出的类或其子类:
vtkProp; vtkAbstractMapper;vtkProperty; vtkCamera; vtkLight; vtkRenderer; vtkRenderWindow;vtkRenderWindowInteractor; vtkTransform; vtkLookupTable ……
我们发现,这些类都是与数据显示或者说渲染相关的。用一个专业的词汇来说,它们构成了VTK的渲染引擎(Rendering Engine)。渲染引擎主要负责数据的可视化表达,它是VTK里的两个重要模块之一,另外一个重要的模块就是可视化管线(Visualization Pipeline)。
可视化管线是指用于获取或创建数据,处理数据,以及把数据写入文件或者把数据传递给渲染引擎进行显示,这样的一种结构在VTK里就称之为可视化管线。数据对象(Data Object)、处理对象(Process Object)和数据流方向(Direction of Data Flow)是可视化管线的三个基本要素。每个VTK程序都会有可视化管线存在,比如示例RenderCylinder,其可视化管线可以简单地表示成图4.8。
图4.8 示例RenderCylinder的可视化管线
示例RenderCylinder的可视化管线非常简单,首先是创建一个锥体数据,接着经Mapper后生成的多边形数据(vtkPolyData)直接送入渲染引擎渲染,创建的数据没有经过任何处理。
我们再看一个稍微复杂点的可视化管线(示例4.2.1_vtkPipelineDemo),在这个示例里,我们先读入后缀为vtk的文件(head.vtk),然后用移动立方体法(vtkMarchingCubes)提取等值面,最后把等值面数据经Mapper送往渲染引擎进行显示,示例完整代码如下,运行结果如图4.9左所示,右边是其可视化管线。
///////////////////////////////////////////////////////vtkPipelineDemo.cpp////////////////////////////////////////////////////////
1:#include "vtkSmartPointer.h"
2:#include "vtkStructuredPointsReader.h"
3:#include "vtkRenderer.h"
4:#include "vtkRenderWindow.h"
5:#include "vtkRenderWindowInteractor.h"
6:#include "vtkMarchingCubes.h"
7:#include "vtkPolyDataMapper.h"
8:#include "vtkActor.h"
9:
10:int main(int argc,char *argv[])
11: {
12://读入Structured_Points类型的vtk文件。
13:vtkSmartPointer<vtkStructuredPointsReader> reader =
14:vtkSmartPointer<vtkStructuredPointsReader>::New();
15:reader->SetFileName("../head.vtk");
16:
17://用移动立方体法提取等值面。
18:vtkSmartPointer<vtkMarchingCubes> marchingCubes =
19:vtkSmartPointer<vtkMarchingCubes>::New();
20: marchingCubes->SetInputConnection(reader->GetOutputPort());
21:marchingCubes->SetValue(0,500);
22:
23://将生成的等值面数据进行Mapper
24:vtkSmartPointer<vtkPolyDataMapper> mapper =
25:vtkSmartPointer<vtkPolyDataMapper>::New();
26:mapper->SetInputConnection(marchingCubes->GetOutputPort());
27:
29://////////////////////////////////////渲染引擎部分////////////////////////////////////
30:vtkSmartPointer<vtkActor> actor =vtkSmartPointer<vtkActor>::New();
31:actor->SetMapper(mapper);
32:
33:vtkSmartPointer<vtkRenderWindow> renWin =
34:vtkSmartPointer<vtkRenderWindow>::New();
35:vtkSmartPointer<vtkRenderer> renderer =
36:vtkSmartPointer<vtkRenderer>::New();
37:vtkSmartPointer<vtkRenderWindowInteractor> interactor =
38:vtkSmartPointer<vtkRenderWindowInteractor>::New();
39:
40:renWin->AddRenderer(renderer);
41:interactor->SetRenderWindow(renWin);
42:renderer->AddActor(actor);
43:renderer->Render();
44:
45:interactor->Initialize();
46:interactor->Start();
47://////////////////////////////////////////////////////////////////////////////////////////////////
48:
49:return 0;
50: }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
图4.9示例4.2.1_vtkPipelineDemo运行结果及其可视化管线
比较图4.8和4.9的可视化管线图,可以知道,图4.9多了一个vtkMarchingCubes用于处理读入的数据。在VTK里,我们把与vtkMarchingCubes类似的对数据做处理的类称为Filter。(Filter这个词有些资料翻译成“过滤器”,有些翻译成“滤波器”,这里我们不作翻译,直接用英文表示。)综合图4.8和4.9,我们还可以抽象出更一般的VTK可视化管线结构图(图4.10):
图4.10 VTK可视化管线
Source是指用于创建数据(如vtkCylinderSource)或者读取数据(如vtkBMPReader、vtkStructuredPointsReader等)的类的统称,即VTK的数据源。Source输出的数据作为Filter的输入,经Filter处理以后(可以经多个Filter处理),生成新的数据。Filter的输出可以直接写入文件,或者经Mapper变换后送入渲染引擎进行渲染、显示,结束可视化管线。图4.10所示的箭头方向即为VTK里数据流流动的方向。我们知道,可视化管线的三要素分别是数据对象、处理对象和数据流方向,Source、Filter和Mapper一起就构成了处理对象,它们的区别是基于数据流的初始化、维持和终止。根据数据的生成方式,Source可以分为Procedural对象(如vtkCylinderSource,通过程序代码生成相关的数据)和Reader对象(如vtkBMPReader,从外部文件中导入数据)。
关于Source、Filter和Mapper的区别可以简单地通过图4.11来表示。Source没有输入,但至少有一个输出;Filter可以有一个或多个输入,产生一个或多个输出;Mapper接受一个或多个的输出,但没有输出,写文件的Writer(如vtkBMPWriter)可以看作是Mapper,负责把数据写入文件或者流(Stream)中,因此,Mapper是可视化管线的终点,同时也是可视化管线和渲染引擎(有时也称之为图形管线)的桥梁。
图4.11Source、Filter和Mapper的区别
4.2.1 可视化管线的连接
从示例vtkPipelineDemo中知道,可视化管线里各个模块的连接是通过接口SetInputConnection()和GetOutputPort()来完成的(示例vtkPipelineDemo第20行)。(VTK5.0版本之前,可视化管线之间的连接使用SetInput()和GetOutput(),VTK5.0以后版本的可视化管线使用SetInputConnection()和GetOutputPort()连接,同时也保留了对旧版本的支持。本系列教程的例子使用VTK5.10官方Release版本,可视化管线采用新的结构连接。)
marchingCubes->SetInputConnection(reader->GetOutputPort());
上行代码把reader的输出(由GetOutputPort()得到)作为marchingCubes的输入(SetInputConnection()设置其输入)。
vtkMarchingCubes作为Filter只接受一个输入,Filter概括起来有以下三种类型(图4.12):单个输入,产生单个输出;多个输入,产生单个输出,但输出的数据可有多种用途,比如,我们读入数据以后,可以对其作等值面提取,另外还可以针对读入的数据生成轮廓线(Outline);第三种Filter是单个输入,产生多个输出。
图4.12 不同的Filter类型
使用SetInputConnection()和GetOutputPort()连接可视化管线时,还要求连接的两部分之间的数据类型必须一样。由于管线是运行时才执行的,如果连接的两部分类型不匹配,程序运行时就会报错。比如,vtkMarchingCubes要求输入的是vtkImageData类型的数据,如果我们给它输入的是vtkPolyData类型的,程序运行时就会报如下的错误:
ERROR:In ..\..\VTK-5.10\Filtering\vtkDemandDrivenPipeline.cxx,line 827
vtkStreamingDemandDrivenPipeline(0000000000BAAB90): Input for connection index 0 on input port index 0 foralgorithm vtkMarchingCubes(0000000000BA9590) is of type vtkPolyData,but avtkImageData is required.
注意:我们刚才提过,VTK 5以后版本的管线连接是采用SetInputConnection() / GetOutputPort(),如果你在连接VTK的可视化管线时,不是一个Filter输出连接到另一个Filter的输入,而是某个数据作为另外一个Filter的输出,这种情况下,可以写成如下形式:
filter->SetInputConnection(dataset->GetProducerPort());
4.2.2 可视化管线的执行
可视化管线连接完成后,必须有一种机制来控制管线的执行。比如某些时候,对某一部分数据做了改变,我们希望得到的结果是:改变的这部分数据在可视化管线里作更新,而其他没做改变的数据则不要去惊动它。图4.13,假如Filter D的输入发生了变化,E和F是依赖于D的输入的,所以红色虚线框内的部分是需要重新执行的管线,而C和G是另外一个分支,D输入改变不影响C和G,所以,为了节省运行时间,C和G是不需要重新执行的。毕竟对于三维的应用程序来说,一般所处理的数据都是大得惊人的,如果真能做到这样,也有利于提高程序的运行速率。
图4.13可视化管线部分执行示意图
VTK采用一种叫做“惰性赋值”(LazyEvaluation)的方案来控制管线的执行,惰性赋值是指根据每个对象的内部修改时间来决定什么时候执行管线,只有当你或者程序发出“请求数据”时,管线才会被执行(前面提到vtkObject里有一个重要的成员变量MTime,管线里的每个从vtkObject派生的类的对象都会跟踪自己的内部修改时间,当遇到“请求数据”时,该对象会比较这个修改时间,如果发现修改时间发生了改变,对象就会执行。)。换言之,VTK是采用命令驱动(Demand Driven)的方法来控制管线的执行,这种方法的好处是,当对数据对象作了更改时,不必立即作计算,只有当发出请求时才开始处理,这样能最小化计算所需的时间,以便更流畅地与数据进行交互。比如,我们看看下面的代码段(摘自vtkPipelineExecute示例):
vtkSmartPointer<vtkBMPReader> reader =vtkSmartPointer<vtkBMPReader>::New();
reader->SetFileName("../doling.bmp");
vtkImageData* imageData =reader->GetOutput();
int extent[6];
imageData->GetWholeExtent(extent);
std::cout<<"Extent of image:"<<extent[0]<<" "
<<extent[1]<<" "<<extent[2]<<""<<extent[3]<<" "
<<extent[4]<<" "<<extent[5]<<""<<std::endl;
我们先读入一幅BMP图像,然后把reader的输出值赋给imageData,接着我们想知道读入的图像到底有多长多宽多高,调用的方法是vtkImageData里的GetWholeExtent()。但是我们却得不到我们想要的结果,输出为:“Extent of image: 0 -10 -1 0 -1”,而我们读入的图像是640×480大小,输出结果应该是:“Extent of image: 0 640 0 480 0 0”才对。这就是因为我们没有“请求数据”(RequestData()),在reader->GetOutput()之后,调用reader->Update(),就会迫使管线的执行,也就是reader才会从磁盘中读取数据,从而才可以获取imageData里的相关信息。
通常,我们不用显性地去调用Update()函数,因为在渲染引擎的最后,当我们调用Render()函数的时候,Actor就会收到渲染请求,接着Actor会请求Mapper给它发送数据,而Mapper又会请求上一层的Filter的数据,Filter最后去请求Source给它数据,于是,整条管线就被执行。除非像上面的代码段里列出的,读入数据以后,中间想要输出某些信息,在得到这些信息之前,你就应该显性地调用Update()函数。管线的执行过程大致如图4.14所示。
图4.14可视化管线数据流及其执行方向示意图
4.3 本章小结@H_301_1@
vtkObjectBase和vtkObject是VTK里两个重要的父类,vtkObjectBase采用引用计数和智能指针的技术来管理VTK对象的内存分配与回收。vtkObjectBase定义了运行时类型识别及状态信息输出的相关接口,有助于调试VTK应用程序。VTK框架里,大多数的类都是从vtkObject派生,vtkObject实现了观察者/命令(Observer/Command)设计模式,内部维护一个修改时间,用于控制可视化管线的执行。可视化管线是VTK里的重要概念,管线的连接应该使用SetInputConnection()/GetOutputPort()接口进行连接。VTK采用“惰性赋值”(Lazy Evaluation)的方案来控制管线的执行,只有当发出“请求数据”时,管线才会被执行。好处是,当对数据对象作了更改时,不必立即作计算,只有当发出请求时才开始处理,这样能最小化计算所需的时间,以便更流畅地进行交互。
4.4 本章参考资料@H_301_1@
《More Effective C++》
《C++ primer》
《The VTK User’s Guide – 11thEdition》
《The Visualization Toolkit – AnObject-Oriented Approach To 3D Graphics (4th Edition)》
观察者模式:http://baike.baidu.com/view/1854779.htm
命令模式:http://baike.baidu.com/view/1963264.htm
New Pipeline: http://www.vtk.org/Wiki/VTK/Tutorials/New_Pipeline