这个专栏是用于记录我在学习VTK过程中的一些心得体会。参考的资料主要有以下三个:
1. 张晓东 罗火灵《VTK图形图像开发进阶》
2. https://examples.vtk.org/site/
3. 沈子恒 《VTK 三维数据渲染进阶》
遇到的一个大问题就是由于版本更新,这些资料中很多代码无法正常运行,需要进行一定的修改,所以这个专栏会记录下来我修改后的程序代码,以便于我之后温习。也希望能给和我有同样困扰的小伙伴们一些帮助。
我使用的版本:VTK9 + VS2022
在上一篇中我介绍了VTK的流水线设计。在这一篇文章中,我将详细介绍VTK的坐标系统以及VTK管线。
坐标系统
计算机图形学里常用的坐标系统主要有四种,分别是:Model坐标系统、World坐标系统、View坐标系统和Display坐标系统。同时有两种表示坐标点的方式:以屏幕像素值为单位和归一化坐标值(各坐标轴取值都为[-1, 1])。
Model坐标系统是定义模型时所采用的坐标系统,通常是局部的笛卡尔坐标系。例如,我们要定义一个表示球体的Actor,一般的做法是将该球体定义在一个柱坐标系统里。
World坐标系统是放置Actor的三维空间坐标系,Actor其中的一个功能就是负责将模型从Model坐标系统变换到World坐标系统。每一个模型可以定义有自己的Model坐标系统,但World坐标系只有一个,每一个Actor必须通过放缩、旋转、平移等操作将Model坐标系变换到World坐标系。World坐标系同时也是相机和光照所在的坐标系统。
View坐标系统表示的是相机所看见的坐标系统。X、Y、Z轴取值为[-1, 1],X、Y值表示像平面上的位置,Z值表示到相机的距离。相机负责将World坐标系变换到View坐标系。
Display坐标系统跟View坐标系统类似,但是各坐标轴的取值不是[-1, 1],而是使用屏幕像素值。屏幕上显示的不同窗口的大小会影响View坐标系的坐标值[-1, 1]到Display坐标系的映射。可以把不同的渲染场景放在同一个窗口进行显示,例如,在一个窗口里,分为左右两个渲染场景,这左右的渲染场景(vtkRenderer)就是不同的视口(Viewport)。
在VTK中,Model坐标系统用得比较少,其他三种坐标经常使用。它们之间的变换由类vtkCoordinate进行管理。以下实例展示了如何将不同的渲染场景放在同一个窗口进行展示。
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
#include <vtkConeSource.h>
#include <vtkCubeSource.h>
#include <vtkCylinderSource.h>
#include <vtkSphereSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkActor.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkSmartPointer.h>
#include <vtkCamera.h>
#include "vtkProperty.h"
int main()
{
vtkSmartPointer<vtkConeSource> cone = vtkSmartPointer<vtkConeSource>::New();
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();
vtkSmartPointer<vtkCylinderSource> cylinder = vtkSmartPointer<vtkCylinderSource>::New();
vtkSmartPointer<vtkSphereSource> sphere = vtkSmartPointer<vtkSphereSource>::New();
vtkSmartPointer<vtkPolyDataMapper> coneMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
coneMapper->SetInputConnection(cone->GetOutputPort());
vtkSmartPointer<vtkPolyDataMapper> cubeMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
cubeMapper->SetInputConnection(cube->GetOutputPort());
vtkSmartPointer<vtkPolyDataMapper> cylinderMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
cylinderMapper->SetInputConnection(cylinder->GetOutputPort());
vtkSmartPointer<vtkPolyDataMapper> sphereMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
sphereMapper->SetInputConnection(sphere->GetOutputPort());
vtkSmartPointer<vtkActor> coneActor = vtkSmartPointer<vtkActor>::New();
coneActor->SetMapper(coneMapper);
coneActor->RotateX(30.0);
coneActor->RotateY(-45.0);
vtkSmartPointer<vtkActor> cubeActor = vtkSmartPointer<vtkActor>::New();
cubeActor->SetMapper(cubeMapper);
cubeActor->RotateX(10.0);
cubeActor->RotateY(30.0);
vtkSmartPointer<vtkActor> cylinderActor = vtkSmartPointer<vtkActor>::New();
cylinderActor->SetMapper(cylinderMapper);
cylinderActor->RotateX(15.0);
cylinderActor->RotateY(-25.0);
vtkSmartPointer<vtkActor> sphereActor = vtkSmartPointer<vtkActor>::New();
sphereActor->SetMapper(sphereMapper);
sphereActor->RotateX(60.0);
sphereActor->RotateY(30.0);
vtkSmartPointer<vtkRenderer> renderer1 = vtkSmartPointer<vtkRenderer>::New();
renderer1->AddActor(coneActor);
renderer1->SetBackground(1.0, 0.0, 0.0);
renderer1->SetViewport(0.0, 0.0, 0.5, 0.5);
vtkSmartPointer<vtkRenderer> renderer2 = vtkSmartPointer<vtkRenderer>::New();
renderer2->AddActor(cubeActor);
renderer2->SetBackground(0.0, 1.0, 0.0);
renderer2->SetViewport(0.5, 0.0, 1.0, 0.5);
vtkSmartPointer<vtkRenderer> renderer3 = vtkSmartPointer<vtkRenderer>::New();
renderer3->AddActor(cylinderActor);
renderer3->SetBackground(0.0, 0.0, 1.0);
renderer3->SetViewport(0.0, 0.5, 0.5, 1.0);
vtkSmartPointer<vtkRenderer> renderer4 = vtkSmartPointer<vtkRenderer>::New();
renderer4->AddActor(sphereActor);
renderer4->SetBackground(1.0, 1.0, 0.0);
renderer4->SetViewport(0.5, 0.5, 1.0, 1.0);
vtkSmartPointer<vtkRenderWindow> renWin = vtkSmartPointer<vtkRenderWindow>::New();
renWin->AddRenderer(renderer1);
renWin->AddRenderer(renderer2);
renWin->AddRenderer(renderer3);
renWin->AddRenderer(renderer4);
renWin->SetSize(640, 480);
renWin->Render();
renWin->SetWindowName("ViewPort");
vtkSmartPointer<vtkRenderWindowInteractor> interactor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
interactor->SetRenderWindow(renWin);
renWin->Render();
interactor->Initialize();
interactor->Start();
return EXIT_SUCCESS;
}
运行结果
coordination
我们可以看到,在这个例子里,一个窗口分为了四个视口,用vtkRenderer::SetViewport()来设置视口范围(取值为[0,1])。
vtkSmartPointer<vtkRenderer> renderer1 = vtkSmartPointer<vtkRenderer>::New();
renderer1->AddActor(coneActor);
renderer1->SetBackground(1.0, 0.0, 0.0);
renderer1->SetViewport(0.0, 0.0, 0.5, 0.5);
vtkSmartPointer<vtkRenderer> renderer2 = vtkSmartPointer<vtkRenderer>::New();
renderer2->AddActor(cubeActor);
renderer2->SetBackground(0.0, 1.0, 0.0);
renderer2->SetViewport(0.5, 0.0, 1.0, 0.5);
vtkSmartPointer<vtkRenderer> renderer3 = vtkSmartPointer<vtkRenderer>::New();
renderer3->AddActor(cylinderActor);
renderer3->SetBackground(0.0, 0.0, 1.0);
renderer3->SetViewport(0.0, 0.5, 0.5, 1.0);
vtkSmartPointer<vtkRenderer> renderer4 = vtkSmartPointer<vtkRenderer>::New();
renderer4->AddActor(sphereActor);
renderer4->SetBackground(1.0, 1.0, 0.0);
renderer4->SetViewport(0.5, 0.5, 1.0, 1.0);
其他部分其实还是遵循了上一篇博文里VTK的流水线编程步骤,在这里就是这些步骤*4了哈哈。
VTK管线
在之前的实例中,我们用到了vtkProp、vtkAbstractMapper、vtkProperty、vtkCamera、vtkLight、vtkRenderer、vtkRenderWindow、vtkRenderWindowInteractor……这些类都是与渲染相关的,它们构成了VTK的渲染引擎(Rendering Engine)。渲染引擎主要负责数据的可视化表达,它是VTK里的两个重要模块之一,另外一个重要的模块就是可视化管线(Visualization Pipeline)。
可视化管线是指用于获取或创建数据,处理数据,以及把数据写入文件或者把数据传递给渲染引擎进行显示的一种结构。数据对象(Data Object)、处理对象(Process Object)和数据流方向(Direction of Data Flow)是可视化管线的三个基本要素。每个VTK程序都会有可视化管线存在。
下面是一个可视化管线的例子:
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
#include<vtkSmartPointer.h>
#include<vtkStructuredPointsReader.h>
#include<vtkPolyDataMapper.h>
#include<vtkMarchingCubes.h>
#include<vtkActor.h>
#include<vtkRenderer.h>
#include<vtkRenderWindow.h>
#include<vtkRenderWindowInteractor.h>
int main(int argc, char* argv[])
{
//读入Structurered_Points类型的vtk文件
vtkSmartPointer<vtkStructuredPointsReader> reader =
vtkSmartPointer<vtkStructuredPointsReader>::New();
reader->SetFileName("D:/head.vtk");//路径
//体绘制方法提取等值面
vtkSmartPointer<vtkMarchingCubes> marchingCubes =
vtkSmartPointer<vtkMarchingCubes>::New();
marchingCubes->SetInputConnection(reader->GetOutputPort());
marchingCubes->SetValue(0, 1500); //等值面应该是关心的解剖结构
//将生成的等值面数据进行Mapper
vtkSmartPointer<vtkPolyDataMapper> mapper =
vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(marchingCubes->GetOutputPort());//送入到数据映射器中处理
//把Mapper的输出送入渲染引擎进行显示
/*************************渲染引擎******************************/
vtkSmartPointer<vtkActor> actor =
vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper); //演员化妆
vtkSmartPointer<vtkRenderer> renderer =
vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(actor);//演员放到舞台上
renderer->SetBackground(1.0, 0, 0);//设置舞台背景
vtkSmartPointer<vtkRenderWindow> renWin =
vtkSmartPointer<vtkRenderWindow>::New();
renWin->AddRenderer(renderer);//舞台搬进戏院
renWin->SetSize(640, 480);//戏院大小
renWin->Render(); //戏院渲染
renWin->SetWindowName("vtkPipelineDemo");//戏院起名
vtkSmartPointer<vtkRenderWindowInteractor> interactor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
interactor->SetRenderWindow(renWin);//与看客交互
interactor->Initialize();
interactor->Start();
return 0;
}
运行结果:
Pipeline
在这个示例里,先读入后缀为vtk的文件(这个文件大家可以在我上传的资源里找到不同的渲染场景放在同一个窗口进行展示实例资源-CSDN文库),然后用体绘制法(vtkMarchingCubes)提取等值面,最后把等值面数据经Mapper送往渲染引擎进行显示。这个步骤和我之前的博文里总结的流水线代码在前面读取数据的部分稍微有些不同,在前面多出了一个vtkMarchingCubes用于处理读入的数据。但是后面Actor、Render以及Window的定义设置参数的步骤还是一样的。所以大家只要记住我前面总结的十个步骤,就能快速写出完整的VTK程序代码啦。
在上面这行代码中,我们可以通过修改这一行
marchingCubes->SetValue(0, 1500); //等值面应该是关心的解剖结构
的参数数值,体会一下体绘制法提取等值面过程中参数的意义。
SetValue(0, 500)
SetValue(0, 1500)
SetValue(0, 2000)
在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是可视化管线的终点,同时也是可视化管线和渲染引擎(有时也称之为图形管线)的桥梁。
可视化管线里各个模块的连接是通过接口SetInputConnection()和GetOutputPort()来完成的。
marchingCubes->SetInputConnection(reader->GetOutputPort());
上行代码把reader的输出(由GetOutputPort()得到)作为marchingCubes的输入(SetInputConnection()设置其输入)。
PS:使用SetInputConnection()和GetOutputPort()连接可视化管线时,还要求连接的两部分之间的数据类型必须一样。由于管线是运行时才执行的,如果连接的两部分类型不匹配,程序运行时就会报错。比如,vtkMarchingCubes要求输入的是vtkImageData类型的数据,如果我们给它输入的是vtkPolyData类型的,程序运行时就会报误。