3.10 将所有内容整合起来
本节概述了图形对象以及如何在 VTK 中使用它们。
图形模型
我们已经讨论了许多在场景渲染中起作用的对象。现在是将它们整合到一个全面的图形和可视化对象模型中的时候了。
在可视化工具包中,有七个基本对象用于渲染场景。幕后有许多其他对象,但这七个是我们最常用的。这些对象列在以下,并在图 3-24 中进行了说明。
vtkRenderWindow — 管理显示设备上的窗口;一个或多个渲染器绘制到 vtkRenderWindow 的一个实例中。
vtkRenderer — 协调涉及光源、摄像机和演员的渲染过程。
vtkLight — 用于照亮场景的光源。
vtkCamera — 定义场景的视图位置、焦点和其他视图属性。
vtkActor — 表示场景中渲染的对象,包括其属性和在世界坐标系中的位置。(注意:vtkActor 是 vtkProp 的一个子类。vtkProp 是包括注释和 2D 绘图类在内的更一般形式的演员。有关更多信息,请参阅“组件和其他类型的 vtkProp”)
vtkProperty — 定义演员的外观属性,包括颜色、透明度和光照属性,如高光和漫反射。还包括线框和实体表面等表现属性。
vtkMapper — 演员的几何表示。多个演员可以引用同一个映射器。
vtkRenderWindow 类将渲染过程联系在一起。它负责管理显示设备上的窗口。对于运行 Windows 的 PC,这将是一个 Microsoft 显示窗口,对于 Linux 和 UNIX 系统,这将是一个 X 窗口,在 Mac(OSX)上是一个 Quartz 窗口。在 VTK 中,vtkRenderWindow 的实例是与设备无关的。这意味着您不需要担心使用的底层图形硬件或软件是什么,软件会自动适应您的计算机,因为 vtkRenderWindow 的实例被创建。 (有关更多信息,请参阅“实现设备无关性”)
除了窗口管理,vtkRenderWindow 对象还用于管理渲染器并存储显示窗口的图形特性,如大小、位置、窗口标题、窗口深度和双缓冲标志。窗口的深度表示每个像素分配了多少位。双缓冲是一种技术,其中一个窗口被逻辑地分为两个缓冲区。在任何给定时间,一个缓冲区当前对用户可见。同时,第二个缓冲区可以用于绘制动画中的下一帧图像。渲染完成后,可以交换两个缓冲区,使新图像可见。这种常见的技术允许动画显示而用户看不到基元的实际渲染。高端图形系统在硬件中执行双缓冲。典型系统将具有深度为 72 位的渲染窗口。前 24 位用于存储前缓冲区的红色、绿色和蓝色(RGB)像素分量。接下来的 24 位存储后缓冲区的 RGB 值。最后的 24 位用作 z 缓冲区。
vtkRenderer 类负责协调其光源、摄像机和演员以生成图像。每个实例在特定场景中维护演员、光源和一个活动摄像机的列表。至少必须定义一个演员,但如果未定义光源和摄像机,渲染器将自动创建它们。在这种情况下,演员将居中在图像中,而默认摄像机视图朝下看 z 轴。vtkRenderer 类的实例还提供了指定背景和环境光照颜色的方法。还提供了用于在世界、视图和显示坐标系之间转换的方法。
渲染器的一个重要方面是它必须与一个 vtkRenderWindow 类的实例相关联,该实例将绘制到其中,并且渲染窗口中绘制的区域必须由一个矩形视口定义。视口由规范化坐标(0,1)在 x 和 y 图像坐标轴上定义。默认情况下,渲染器绘制到渲染窗口的整个范围(视点坐标 (0,0,1,1))。可以指定一个较小的视口。并且可以让多个渲染器绘制到同一个渲染窗口中。
vtkLight 类的实例照亮场景。有各种实例变量可用于定位和定位光源。还可以打开/关闭灯光以及设置它们的颜色。通常至少有一个灯是“开启”的,以照亮场景。如果未定义并打开灯光,则渲染器会自动构建一个灯光。VTK 中的灯光可以是定位的或无限的。定位光源具有关联的锥角和衰减因子。无限光源将光线平行投射。
摄像机由 vtkCamera 类构建。重要参数包括摄像机位置、焦点、前后裁剪平面位置、视图向上矢量和视野。摄像机还具有特殊方法,以简化操作,如本章前面描述的那样。
这些包括高程、方位、缩放和滚动。与 vtkLight 类似,如果未定义,则渲染器将自动创建 vtkCamera 的实例。
vtkActor 类的实例代表场景中的对象。特别是,vtkActor 结合了对象属性(颜色、着色类型等)、几何定义和世界坐标系中的方向。这是通过维护引用 vtkProperty、vtkMapper 和 vtkTransform 实例的实例变量来实现的。通常情况下,您无需显式创建属性或变换,因为这些将使用 vtkActor 的方法自动创建和操作。您确实需要创建 vtkMapper 的实例(或其子类之一)。映射器将数据可视化管线与图形设备联系起来。(我们将在下一章中更多地讨论管线。)
在 VTK 中,演员实际上是 vtkProp(任意道具)和 vtkProp3D(可以在 3D 空间中变换的道具)的子类。(“道具”一词源自舞台,道具是场景中的一个对象。)有其他子类的道具和演员具有专门的行为(有关更多信息,请参见“集合和其他类型的 vtkProp”)。一个例子是 vtkFollower。这个类的实例始终面向活动摄像机。在设计必须从场景中的任何摄像机位置可读的标志或文本时,这是有用的。
vtkProperty 类的实例影响演员的渲染外观。创建演员时,会自动创建一个属性实例。还可以直接创建属性对象,然后将属性对象与一个或多个演员关联。通过这种方式,演员可以共享公共属性。
最后,vtkMapper(及其子类)定义对象几何和可选的顶点颜色。此外,vtkMapper 引用用于对几何着色的颜色表(即 vtkLookupTable)。 (我们将在第 6 章的“颜色映射”中讨论数据到颜色的映射。)我们将在第 6 章的“映射器设计”中更详细地研究映射过程。现在假设 vtkMapper 是一个代表几何和其他类型的可视化数据的对象。
还有另一个重要对象,vtkRenderWindowInteractor,用于在渲染窗口中捕获事件(如鼠标点击和鼠标移动)。vtkRenderWindowInteractor 捕获这些事件,然后触发诸如摄像机拉伸、平移和旋转、演员拾取、进入/退出立体模式等操作。使用 SetRenderWindow() 方法将此类的实例与渲染窗口关联。
实现设备无关性
使用 VTK 构建的应用程序的一个理想特性是它们是设备无关的。这意味着在具有特定软件/硬件配置的一个操作系统上运行的计算机代码在不同操作系统和软件/硬件配置上运行时保持不变。这样做的优点是程序员不需要在不同计算机系统之间移植应用程序。此外,现有应用程序不需要重写以利用硬件或软件技术的新发展。相反,VTK 通过继承和一种称为对象工厂的技术的组合来透明地处理这一点。
图:3.24 展示了使用继承实现设备无关性的方法。像vtkActor这样的某些类被分为两部分:一个是设备无关的超类,另一个是设备相关的子类。关键在于用户通过在设备无关的超类中调用特殊构造函数New()来创建一个设备相关的子类。例如,在C++中,我们会使用
vtkActor *anActor = vtkActor::New()
来创建一个vtkActor的设备相关实例。用户看不到设备相关的代码,但实际上anActor是vtkActor的设备相关子类的指针。图3-25b是构造方法New()的代码片段,它使用了VTK的对象工厂机制。反过来,vtkGraphicsFactory(用于实例化图形类)在请求实例化演员时会生成适当的具体子类,如图3-25c所示。
使用New()方法实现的对象工厂允许我们创建设备无关的代码,可以在不同计算机之间移动并适应不断变化的技术。例如,如果有一个新的图形库可用,我们只需要创建一个新的设备相关子类,然后修改图形工厂以根据环境变量或其他系统信息实例化适当的子类。这个扩展是局部的,只需一次完成,所有基于这些对象工厂的应用程序将自动转移而无需更改。
例子:
这一部分通过使用VTK图形对象实现了一些简单的应用程序。重点是基础知识:如何创建渲染器、灯光、摄像机和演员。后面的章节将这些基本原理联系在一起,创建用于数据可视化的应用程序。
渲染一个圆锥。以下的C++代码使用了本节介绍的大部分对象来创建一个圆锥的图像。vtkConeSource生成一个圆锥的多边形表示,vtkPolyDataMapper将几何图形(与演员一起)映射到底层图形库中。(此示例的源代码可以在Cone.cxx中找到。源代码中还包含额外的文档。)
#include <vtkSmartPointer.h>
#include <vtkConeSource.h>#include <vtkActor.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkNamedColors.h>int main(int, char *[])
{//Create a coneauto coneSource =vtkSmartPointer<vtkConeSource>::New();coneSource->SetHeight( 3.0);coneSource->SetRadius( 1.0);coneSource->SetResolution( 10);coneSource->Update();//Create a mapper and actorauto mapper =vtkSmartPointer<vtkPolyDataMapper>::New();mapper->SetInputConnection(coneSource->GetOutputPort());auto colors =vtkSmartPointer<vtkNamedColors>::New();auto actor =vtkSmartPointer<vtkActor>::New();actor->SetMapper(mapper);actor->GetProperty()->SetDiffuseColor(colors->GetColor3d("bisque").GetData());//Create a renderer, render window, and interactorauto renderer =vtkSmartPointer<vtkRenderer>::New();auto renderWindow =vtkSmartPointer<vtkRenderWindow>::New();renderWindow->AddRenderer(renderer);renderWindow->SetSize(640, 480);auto renderWindowInteractor =vtkSmartPointer<vtkRenderWindowInteractor>::New();renderWindowInteractor->SetRenderWindow(renderWindow);//Add the actors to the scenerenderer->AddActor(actor);renderer->SetBackground(colors->GetColor3d("Salmon").GetData());//Render and interactrenderWindow->Render();renderWindowInteractor->Start();return EXIT_SUCCESS;
}
一些关于这个例子的评论。包含文件vtk__.h包含了在VTK中编译这个例子所需的对象类定义。我们在这个例子中使用构造函数New()来创建对象,并使用方法Delete()来销毁对象。在VTK中,使用New()和Delete()是强制性的,以确保设备独立性并正确管理引用计数。(详见VTK用户指南。)在这个例子中,使用Delete()实际上并不是必要的,因为对象在程序终止时会被自动删除。但一般来说,你应该始终为每次调用New()使用Delete()。(未来的例子将不会在main()程序的范围内显示Delete()方法,以节省空间,也不会显示必需的#include语句。)
在这个例子中,代表圆锥(一组多边形)的数据是通过将一系列对象链接在一起形成一个管线来创建的(这是下一章的主题)。首先,使用vtkConeSource创建圆锥的多边形表示,并将其作为输入传递给数据映射器,如SetInput()方法所指定的那样。SetMapper()方法将映射器的数据与coneActor关联起来。下一行将coneActor添加到渲染器的演员列表中。圆锥在一个循环中以360度的角度渲染。由于上面的例子中没有定义相机或灯光,VTK会自动生成一个默认的灯光和相机作为用户的便利。相机通过GetActiveCamera()方法访问,并应用一个度的方位角。每次对任何对象进行更改时,都会调用Render()方法来生成相应的图像。一旦循环完成,所有分配的对象都将被销毁,程序退出。
在VTK中有许多类似于vtkConeSource的源对象,如图3-26所示。在下一章中,我们将学习更多关于源和其他类型的过滤器。
事件和观察者。像VTK这样的可视化工具包经常用于交互式应用程序,或者可能需要在操作过程中提供状态。此外,与GUI工具包等其他软件包的集成是一个常见的任务。支持这些功能需要一种将用户功能插入软件的机制。在VTK中,采用了命令/观察者设计模式[Gamma95]来实现这一目的。
在VTK中实现的这种设计模式的基础是事件的概念。事件表示软件中发生了重要的操作。例如,如果用户在渲染窗口中按下左鼠标按钮,VTK将调用LeftButtonPressEvent。观察者是注册对特定事件或事件感兴趣的对象。当这些事件中的一个被调用时,观察者会收到通知,并可能在那时执行任何有效的操作;也就是说,执行与观察者相关联的命令。命令/观察者设计模式的好处在于它在概念和实现上都很简单,但却为用户提供了重大的权力。然而,它确实需要软件实现在操作时调用事件。
在下一个例子中,一个观察者会在渲染器在开始渲染过程时触发的StartEvent上进行监视。观察者反过来执行其关联的命令,简单地打印出相机的当前位置。
#include "vtkCommand.h"
// Callback for the interaction
class vtkMyCallback : public vtkCommand
{
public:static vtkMyCallback *New(){ return new vtkMyCallback; }virtual void Execute(vtkObject *caller, unsigned long, void*){vtkRenderer *ren =reinterpret_cast<vtkRenderer*>(caller);cout << ren->GetActiveCamera()->GetPosition()[0] << " "ren->GetActiveCamera()->GetPosition()[1] << " "ren->GetActiveCamera()->GetPosition()[2] << "n";}
};int main( int argc, char *argv[] )
{vtkConeSource *cone = vtkConeSource::New();cone->SetHeight( 3.0 );cone->SetRadius( 1.0 );cone->SetResolution( 10 );vtkPolyDataMapper *coneMapper = vtkPolyDataMapper::New();coneMapper->SetInputConnection( cone->GetOutputPort() ); vtkActor*coneActor = vtkActor::New(); coneActor->SetMapper( coneMapper );vtkRenderer *ren1= vtkRenderer::New();ren1->AddActor( coneActor );ren1->SetBackground( 0.1, 0.2, 0.4 );vtkRenderWindow *renWin = vtkRenderWindow::New();renWin->AddRenderer( ren1 ); renWin->SetSize( 300, 300 );vtkMyCallback *mo1 = vtkMyCallback::New();ren1->AddObserver(vtkCommand::StartEvent,mo1); mo1->Delete();int i;for (i = 0; i < 360; ++i){// render the imagerenWin->Render();// rotate the active camera by one degreeren1->GetActiveCamera()->Azimuth( 1 );}cone->Delete();coneMapper->Delete();coneActor->Delete();ren1->Delete();renWin->Delete();return 0;
}
观察者是通过从类vtkCommand派生而创建的。Execute()方法需要由vtkCommand的任何具体子类实现(即,该方法是纯虚拟的)。生成的子类vtkMyCommand被实例化,并使用AddObserver()方法向渲染器实例ren1注册。在这种情况下,StartEvent是被观察的事件。
这个简单的例子并没有展示命令/观察者设计模式的真正威力。在本章后面(“解释代码”)我们将看到如何使用这个功能将一个简单的GUI集成到VTK中。在第7章-高级计算机图形学中,将介绍三维交互小部件和用户交互(“3D小部件和用户交互”。
创建多个渲染器。下一个例子稍微复杂一些,使用多个共享单个渲染窗口的渲染器。我们使用视口来定义渲染器应该在渲染窗口中绘制的位置。(此C++代码可以在Cone3.cxx中找到。)
Figure 3-27. Four frames of output from Cone3.cxx. See Cone3.cxx and Cone3.py
vtkRenderer *ren1= vtkRenderer::New();
ren1->AddActor( coneActor );
ren1->SetBackground( 0.1, 0.2, 0.4 );
ren1->SetViewport(0.0, 0.0, 0.5, 1.0);vtkRenderer *ren2= vtkRenderer::New();
ren2->AddActor( coneActor );
ren2->SetBackground( 0.2, 0.3, 0.5 );
ren2->SetViewport(0.5, 0.0, 1.0, 1.0);vtkRenderWindow *renWin = vtkRenderWindow::New();
renWin->AddRenderer( ren1 ); renWin->AddRenderer( ren2 );
renWin->SetSize( 600, 300 );ren1->GetActiveCamera()->Azimuth(90);int i;
for (i = 0; i < 360; ++i){// render the image renWin->Render();// rotate the active camera by one degreeren1->GetActiveCamera()->Azimuth( 1 );ren2->GetActiveCamera()->Azimuth( 1 );}
正如你所看到的,很多代码与前一个例子相同。第一个不同之处在于我们创建了两个渲染器而不是一个。我们将相同的演员分配给两个渲染器,但将每个渲染器的背景设置为不同的颜色。我们设置两个渲染器的视口,使一个位于渲染窗口的左半部分,另一个位于右半部分。渲染窗口的大小指定为600乘以300像素,这导致每个渲染器绘制到300乘以300像素的视口中。
多个渲染器的一个很好的应用是显示同一世界的不同视图,就像这个例子所演示的那样。在这里,我们通过一个90度的方位调整第一个渲染器的相机。然后我们开始一个循环,围绕锥体旋转这两个相机。图3-27显示了这个动画的四个帧。
属性和变换。之前的例子并没有显式创建属性或变换对象,也没有应用影响这些对象的演员方法。相反,我们接受了默认实例变量值。这个过程是VTK应用程序的典型做法。大多数实例变量已经预设为生成可接受的结果,但是方法总是可用的,让你可以覆盖默认值。
这个例子创建了两个不同颜色和镜面属性的圆锥体的图像。此外,我们将其中一个对象进行了变换,使其靠近另一个对象。这个例子的C++源代码可以在Cone4.cxx中找到。
vtkActor *coneActor = vtkActor::New();
coneActor->SetMapper(coneMapper);
coneActor->GetProperty()->SetColor(0.2, 0.63, 0.79);
coneActor->GetProperty()->SetDiffuse(0.7);
coneActor->GetProperty()->SetSpecular(0.4);
coneActor->GetProperty()->SetSpecularPower(20);vtkProperty *property = vtkProperty::New();
property->SetColor(1.0, 0.3882, 0.2784);
property->SetDiffuse(0.7);
property->SetSpecular(0.4);
property->SetSpecularPower(20);vtkActor *coneActor2 = vtkActor::New();
coneActor2->SetMapper(coneMapper);
coneActor2->GetProperty()->SetColor(0.2, 0.63, 0.79);
coneActor2->SetProperty(property); coneActor2->SetPosition(0, 2, 0);vtkRenderer *ren1= vtkRenderer::New();
ren1->AddActor( coneActor );
ren1->AddActor( coneActor2 );
ren1->SetBackground( 0.1, 0.2, 0.4 );
我们通过修改演员自动创建的属性对象来设置圆锥体演员coneActor的属性。这与演员coneActor2不同,我们直接创建一个属性,然后将其分配给演员。通过应用SetPosition()方法,我们将ConeActor2从其默认位置移动。这个方法会影响演员的一个实例变量——变换矩阵。生成的图像显示在图3-28中。
介绍vtkRenderWindowInteractor。之前的例子都不是交互式的。也就是说,如果不修改和重新编译C++代码,就无法直接与数据进行交互。一种常见的交互类型是改变相机位置,这样我们就可以从不同的视角查看场景。在可视化工具包中,我们提供了一套方便的对象来实现这一点:vtkRenderWindowInteractor、vtkInteractorStyle及其派生类。
vtkRenderWindowInteractor类的实例捕获渲染窗口中特定于窗口系统的鼠标和键盘事件,然后将这些事件转换为VTK事件。例如,在X11或Windows应用程序中的鼠标移动(发生在渲染窗口中)将被vtkRenderWindowInteractor转换为VTK的MouseMoveEvent。对于此事件注册的任何观察者都将收到通知(请参阅“事件和观察者”)。通常,vtkInteractorStyle的实例与vtkRenderWindowInteractor结合使用,定义与特定事件相关联的行为。例如,我们可以通过使用不同的鼠标按钮和移动组合来执行相机的拉近、平移和旋转。以下代码片段展示了如何实例化和使用这些对象。这个例子与我们的第一个例子相同,只是增加了交互器和交互器样式。完整的C++代码示例在Cone5.cxx中。
vtkRenderWindowInteractor *iren = vtkRenderWindowInteractor::New();
iren->SetRenderWindow(renWin);vtkInteractorStyleTrackballCamera *style =vtkInteractorStyleTrackballCamera::New();iren->SetInteractorStyle(style);
iren->Initialize();
iren->Start();
创建交互器后,我们必须使用SetRenderWindow()方法告诉它要在哪个渲染窗口中捕获事件。为了使用交互器,我们必须使用Initialize()和Start()方法初始化并启动事件循环,这与窗口系统的事件循环一起工作,开始捕获事件。一些更有用的事件包括按下"w"键,绘制所有演员的线框;按下"s"键,以表面形式绘制演员;按下"3"键,在支持此功能的系统中切换进入和退出3D立体声;按下"r"键,重置相机视图;按下"e"键,退出应用程序。此外,鼠标按钮围绕相机焦点旋转、平移和拉近。两个高级功能是按下"u"键执行用户定义的函数;按下"p"键,选择鼠标指针下的演员。
解释性代码。在前面的例子中,我们看到如何创建一个交互器样式对象,结合vtkRenderWindowInteractor,使我们能够通过在渲染窗口中移动鼠标来操纵相机。尽管这为许多应用程序提供了灵活性和交互性,但在本文的许多示例中,我们想要修改其他参数。这些参数范围从演员属性,比如颜色,到输入文件的名称。当然,我们总是可以编写或修改C++代码来实现这一点,但在许多情况下,进行更改并查看结果之间的反馈时间太长。改善系统整体交互性的一种方法是使用解释性接口。解释性系统允许我们修改对象并立即看到结果,而无需重新编译和重新链接源代码。解释性语言还提供许多工具,比如GUI(图形用户界面)工具,简化了应用程序的创建。
可视化工具包在其编译过程中内置了自动生成语言绑定到[Ousterhout94]的能力。这种所谓的包装过程自动生成了一个层,位于C++ VTK库和解释器之间,如图3-29所示。对于系统中的大多数对象和方法,C++方法和Python C++函数之间存在一对一的映射。为了演示这一点,以下示例重复了前面的C++示例,只是用Python脚本实现。(脚本可以在Cone5.py中找到。
Figure 3-29. In VTK the C++ library is automatically wrapped with the interpreted languages Python and Java.
#!/usr/bin/env python
import vtk
colors = vtk.vtkNamedColors ()cone = vtk.vtkConeSource ()
cone.SetHeight ( 3.0 )
cone.SetRadius ( 1.0 )
cone.SetResolution ( 10 )coneMapper = vtk.vtkPolyDataMapper ()
coneMapper.SetInputConnection (cone.GetOutputPort ())coneActor = vtk.vtkActor ()
coneActor.SetMapper ( coneMapper )ren1 = vtk.vtkRenderer ()
ren1.AddActor( coneActor )
ren1.SetBackground (colors.GetColor3d ("MidnightBlue"))renWin = vtk.vtkRenderWindow ()
renWin.AddRenderer (ren1)
renWin.SetSize (300 , 300)iren = vtk.vtkRenderWindowInteractor ()
iren.SetRenderWindow (renWin)style = vtk.vtkInteractorStyleTrackballCamera ()
iren.SetInteractorStyle (style)iren.Initialize ()
iren.Start ()
Figure 3-30. Using Tcl and Tk to build an interpreted application.
这个例子开始加载一些共享库,定义了各种VTK类。接下来,标准的可视化流水线是从vtkConeSource和vtkPolyDataMapper创建的。渲染类的创建与C++示例完全相同。一个主要的添加是一个观察者,用于在渲染窗口中监视UserEvent(默认情况下是“keypress-u”)。观察者触发调用一个Tcl脚本来调用一个名为vtkInteract的Tk交互GUI小部件。这个GUI允许直接输入Tcl语句,如图3-30所示,由前面在脚本中执行的Tcl命令包require vtkinteraction定义。(注意:Tk是一种流行的GUI工具包,用于解释性语言,并作为Tcl的一部分分发。)
从这个例子中我们可以看到,Tcl示例的代码行数比等效的C++代码少。此外,许多C++的复杂性被隐藏在解释性语言中。使用这个用户界面GUI,我们可以创建、修改和删除对象,并修改它们的实例变量。当应用Render()方法或渲染窗口中的鼠标事件导致渲染发生时,结果的更改会立即显示出来。我们鼓励您使用Tcl(或其他解释器之一)快速创建图形和可视化示例。当您需要更高性能的应用程序时,最好使用C++。
变换矩阵
变换矩阵在可视化工具包中随处可见。演员(vtkProp3D的子类)使用它们来定位和定向自己。各种过滤器,包括vtkGlyph3D和vtkTransformFilter,使用变换矩阵来实现它们自己的功能。作为用户,您可能永远不会直接使用变换矩阵,但理解它们对于成功使用许多VTK类是很重要的。
应用变换矩阵最重要的方面是理解变换的应用顺序。如果您将复杂的一系列变换分解为简单的平移、缩放和旋转的组合,并仔细跟踪应用顺序,那么您将大大掌握它们的使用方法。
变换矩阵的一个很好的演示例子是查看vtkActor如何使用它的内部矩阵。vtkActor有一个内部实例变量Transform,它将许多方法委托给它,或者使用矩阵来实现它的方法。例如,RotateX()、RotateY()和RotateZ()方法都委托给Transform。SetOrientation()方法使用Transform来定向演员。vtkActor类以我们认为对大多数用户来说是自然的顺序应用变换。
1. 将演员平移到其原点。缩放和旋转将围绕该点发生。缩放和旋转应用后,初始平移将被对立方向的平移抵消。
2. 缩放几何体。
3. 绕y轴、然后x轴、最后z轴旋转演员。
4. 撤消步骤1的平移,并将演员移动到最终位置。
变换的顺序很重要。在VTK中,旋转的顺序是按照大多数情况下自然的顺序排列的。我们建议您花一些时间学习软件,了解这些变换如何与您自己的数据一起工作。
变换最令人困惑的方面可能是旋转及其对方向实例变量的影响。通常情况下,用户不会直接设置方向,大多数用户更倾向于使用RotateX()、RotateY()和RotateZ()方法来指定旋转。这些方法按照用户指定的顺序围绕x、y和z轴进行旋转。新的旋转将应用在旋转变换的右侧。如果您需要围绕单个轴旋转演员,演员将按您期望的方式旋转,并且结果方向向量将如预期那样。例如,执行RotateY(20)将产生方向(0,20,0),而RotateZ(20)将产生(0,0,20)。然而,执行RotateY(20)然后RotateZ(20)不会产生(0,20,20),而是产生方向(6.71771, 18.8817, 18.8817)!这是因为公式3-14中的旋转部分是按照旋转顺序z、x、y构建的。为了验证这一点,执行RotateZ(20)然后RotateY(20)确实会产生方向(0,20,20)。添加第三个旋转甚至可能更加令人困惑。
一个很好的经验法则是只使用SetOrientation()方法来重置方向为(0,0,0)或者设置其中一个旋转。当需要多个角度时,优先使用RotateX()、RotateY()和RotateZ()方法,而不是SetOrientation()。请记住,这些旋转是以相反的顺序应用的。图3-31说明了旋转方法的使用。我们使用渲染窗口的EraseOff()方法关闭帧之间的擦除,以便我们可以看到旋转的效果。请注意,在第四幅图中,尽管在y轴旋转之前进行了x轴旋转,但牛仍然围绕自己的y轴旋转。
我们已经看到,VTK通过使用比转换矩阵更自然的实例变量来隐藏一些矩阵变换的复杂性。但是,在某些情况下,actor执行的预定义变换顺序可能不够。vtkActor有一个实例变量UserMatrix,其中包含一个4 x 4的变换矩阵。这个矩阵在actor组合的变换之前被应用。当你对4 x 4变换矩阵更加熟悉时,你可能想要构建自己的矩阵。vtkTransform对象创建和操作这些矩阵。与actor不同,vtkTransform的实例没有用于位置、缩放、原点等的实例变量。你可以直接控制矩阵的组合。
下面的语句创建了一个与actor创建的相同的4 x 4矩阵:
vtkTransform *myTrans = vtkTransform::New ();
myTrans->Translate(position[0],position[1],position[2]);
myTrans->Translate(origin[0],origin[1],origin[2]);
myTrans->RotateZ(orientation[2]);myTrans->RotateX (orientation[0]);
myTrans->RotateZ(orientation[1];
myTrans->Scale (scale[0],scale[1],scale[2]);
myTrans->Translate (-origin[0],-origin[1],-origin[2]);
将这个变换操作序列与方程3-14中的变换进行比较。
我们的最后一个示例展示了使用vtkTransform构建的变换与使用vtkActor构建的变换之间的比较。在这个示例中,我们将转换我们的奶牛,使她围绕世界坐标原点(0,0,0)旋转。她将看起来像是围绕原点行走。我们有两种方法来实现这一点:一种是使用vtkTransform和actor的UserMatrix,另一种是使用actor的实例变量。
首先,我们将奶牛沿着z轴移动五英尺,然后围绕原点旋转她。我们总是按照它们应用的相反顺序指定变换:
vtkTransform *walk = vtkTransform::New();
walk->RotateY(0,20,0);
walk->Translate(0,0,5);vtkActor *cow=vtkActor::New();
cow->SetUserMatrix(walk->GetMatrix());
这些操作产生了变换序列:
现在我们使用奶牛的实例变量进行相同操作:
vtkActor *cow=vtkActor::New();
cow->SetOrigin(0,0,-5);
cow->RotateY(20);
cow->SetPosition(0,0,5);
当actor构建其变换时,将是:
取消最右边平移矩阵中的减号并组合位置和原点平移,产生了我们用vtkTransform构建的等效变换。图3-32展示了奶牛按指定的变换顺序旋转。你的偏好是一种品味,取决于你对矩阵变换的熟悉程度。随着你变得更加熟练(和你的需求更大),你可能更喜欢始终构建自己的变换。VTK给了你选择的权力。
有一种最终而强大的操作会影响actor的方向。你可以围绕位于actor原点的任意矢量旋转actor。这是通过actor(和变换的)RotateWXYZ()方法完成的。操作的第一个参数指定围绕由接下来的三个参数指定的矢量旋转的角度。图3-33展示了如何围绕通过奶牛鼻子的矢量旋转奶牛。起初,我们将原点保留在(0,0,0)。这显然不是我们想要的。第二幅图展示了当我们将奶牛的旋转原点改变为她鼻子尖时的旋转。