1.可视化管道综述
可视化管线是指用于获取或创建数据,处理数据,以及把数据写入文件或者把数据传递给渲染引擎进行显示,这样的一种结构在VTK里就称之为可视化管线。数据对象(Data Object)、处理对象(Process Object)和数据流方向(Direction of Data Flow)是可视化管线的三个基本要素。每个VTK程序都会有可视化管线存在。
2.可视化管道细节
在VTK里,我们把与vtkMarchingCubes类似的对数据做处理的类称为Filter。
更一般的VTK可视化管道流程如下所示:
Source是指用于创建数据(如vtkCylinderSource)或者读取数据(如vtkBMPReader、vtkStructuredPointsReader等)的类的统称,即VTK的数据源。Source输出的数据作为Filter的输入,经Filter处理以后(可以经多个Filter处理),生成新的数据。Filter的输出可以直接写入文件,或者经Mapper变换后送入渲染引擎进行渲染、显示,结束可视化管线。
可视化管线的三要素分别是数据对象、处理对象和数据流方向,Source、Filter和Mapper一起就构成了处理对象,它们的区别是基于数据流的初始化、维持和终止。根据数据的生成方式,Source可以分为Procedural对象(如vtkCylinderSource,通过程序代码生成相关的数据)和Reader对象(如vtkBMPReader,从外部文件中导入数据)。
关于Source、Filter和Mapper的区别可以简单地通过下图区分。Source没有输入,但至少有一个输出;Filter可以有一个或多个输入,产生一个或多个输出;Mapper接受一个或多个的输出,但没有输出,写文件的Writer(如vtkBMPWriter)可以看作是Mapper,负责把数据写入文件或者流(Stream)中,因此,Mapper是可视化管线的终点,同时也是可视化管线和渲染引擎(有时也称之为图形管线)的桥梁。
3.如何连接可视化管道?
可视化管线里各个模块的连接是通过接口SetInputConnection()和GetOutputPort()来完成的。
marchingCubes->SetInputConnection(reader->GetOutputPort());
上行代码把reader的输出(由GetOutputPort()得到)作为marchingCubes的输入(SetInputConnection()设置其输入)。vtkMarchingCubes作为Filter只接受一个输入,Filter概括起来有以下三种类型:单个输入,产生单个输出;多个输入,产生单个输出,但输出的数据可有多种用途,比如,我们读入数据以后,可以对其作等值面提取,另外还可以针对读入的数据生成轮廓线(Outline);第三种Filter是单个输入,产生多个输出。
使用SetInputConnection()和GetOutputPort()连接可视化管线时,还要求连接的两部分之间的数据类型必须一样。由于管线是运行时才执行的,如果连接的两部分类型不匹配,程序运行时就会报错。比如,vtkMarchingCubes要求输入的是vtkImageData类型的数据,如果我们给它输入的是vtkPolyData类型的,程序运行时就会报误。
4.可视化管道是如何执行的?
可视化管线连接完成后,必须有一种机制来控制管线的执行。比如某些时候,对某一部分数据做了改变,我们希望得到的结果是:改变的这部分数据在可视化管线里作更新,而其他没做改变的数据则不要去惊动它。如下图,假如Filter D的输入发生了变化,E和F是依赖于D的输入的,所以红色虚线框内的部分是需要重新执行的管线,而C和G是另外一个分支,D输入改变不影响C和G,所以,为了节省运行时间,C和G是不需要重新执行的。毕竟对于三维的应用程序来说,一般所处理的数据都是大得惊人的,如果真能做到这样,也有利于提高程序的运行速率。
VTK采用一种叫做“惰性赋值”(LazyEvaluation)的方案来控制管线的执行,惰性赋值是指根据每个对象的内部修改时间来决定什么时候执行管线,只有当你或者程序发出“请求数据”时,管线才会被执行(前面提到vtkObject里有一个重要的成员变量MTime,管线里的每个从vtkObject派生的类的对象都会跟踪自己的内部修改时间,当遇到“请求数据”时,该对象会比较这个修改时间,如果发现修改时间发生了改变,对象就会执行)。换言之,VTK是采用命令驱动(Demand Driven)的方法来控制管线的执行,这种方法的好处是,当对数据对象作了更改时,不必立即作计算,只有当发出请求时才开始处理,这样能最小化计算所需的时间,以便更流畅地与数据进行交互。
解释代码如下: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()函数。管线的执行过程大致如下图所示。