1、前言
选择拾取是人机交互过程的一个重要功能。在玩3D游戏时,场景中可能会存在多个角色,有时需要使用鼠标来选择所要控制的角色,这就需要用到拾取功能。另外,在某些三维图形的编辑软件中,经常需要编辑其中的一个点、一个面片或者一个局部区域,这也需要通过拾取功能来完成。VTK中定义了多个拾取功能的类,下图显示了这些拾取类的继承关系。VTK中的所有拾取类都继承自vkAbstractPicker 类,利用这些类可以实现许多复杂的功能。

2、点拾取
1)概述
从图上可以看出,完成点拾取功能类是vtkPointPicker。VTK中的消息是通过vtkRenderWindowInteractor 类来处理的,在类 vtkRenderWindowInteractor 中,有如下函数:
public virtual void SetPicker(vtkAbstractPicker arg0);
该函数用来设置具体的 vkAbstractPicker 对象执行对应的拾取操作,对于点拾取就是设置vtkPointPicker 对象。
vtkRenderWindowInteractor 内部定义了一个 vtkInteractorStyle 对象。vtkInteractorStyle 类是一个虚基类,其子类定义了多种鼠标和键盘消息的处理方法,在实现拾取操作时,需要定制相应的鼠标消息处理函数。比如拾取某个点时,应该响应鼠标的左键按下消息,并在响应该消息的函数中根据鼠标的当前窗口坐标来完成拾取操作。
2)代码
cs
private void TestPointPicker()
{
vtkSphereSource sphereSource = vtkSphereSource.New();
sphereSource.Update();
//create a mapper and actor
vtkPolyDataMapper mapper = vtkPolyDataMapper.New();
mapper.SetInputConnection(sphereSource.GetOutputPort());
vtkActor actor = vtkActor.New();
actor.SetMapper(mapper);
//creat a renderer,render window,and interactor
vtkRenderer renderer = vtkRenderer.New();
vtkRenderWindow renderWindow = vtkRenderWindow.New();
renderWindow.Render();
renderWindow.SetWindowName("PointPicker");
renderWindow.AddRenderer(renderer);
vtkPointPicker pointPicker = vtkPointPicker.New();
vtkRenderWindowInteractor windowInteractor = vtkRenderWindowInteractor.New();
windowInteractor.SetPicker(pointPicker);
windowInteractor.SetRenderWindow(renderWindow);
windowInteractor.LeftButtonPressEvt += WindowInteractor_PickEvt;
vtkInteractorStyleTrackballCamera style = new vtkInteractorStyleTrackballCamera();
windowInteractor.SetInteractorStyle(style);
renderer.AddActor(actor);
renderer.SetBackground(1, 1, 1);
renderWindow.Render();
windowInteractor.Start();
}
cs
private void WindowInteractor_PickEvt(vtkObject sender, vtkObjectEventArgs e)
{
if (sender is vtkRenderWindowInteractor interactor)
{
int[] pos = interactor.GetEventPosition();
Console.WriteLine($"Picking pixel: {pos[0]} {pos[1]}");
// 拾取函数 4个参数 前三个为鼠标的当前窗口坐标,第四个参数是vktRenderer对象
interactor.GetPicker().Pick(pos[0], pos[1], 0, interactor.GetRenderWindow().GetRenderers().GetFirstRenderer());
double[] picked = interactor.GetPicker().GetPickPosition();
Console.WriteLine($"Picked value: {picked[0]} {picked[1]} {picked[2]}");
vtkSphereSource sphereSource = vtkSphereSource.New();
sphereSource.Update();
vtkPolyDataMapper mapper = vtkPolyDataMapper.New();
mapper.SetInputConnection(sphereSource.GetOutputPort());
vtkActor actor = vtkActor.New();
actor.SetMapper(mapper);
actor.SetPosition(picked[0], picked[1], picked[2]);
actor.SetScale(0.05);
actor.GetProperty().SetColor(1, 0, 0);
interactor.GetRenderWindow().GetRenderers().GetFirstRenderer().AddActor(actor);
}
}
3)效果
4)说明
这里是注册LeftButtonPressEvt事件响应函数。在事件函数中,调用了vtkRenderWindowInteractor 的GetEventPosition(函数输出鼠标点击的屏幕坐标(以像素为单位)。实现拾取的函数是:
public virtual int Pick(double selectionX, double selectionY, double selectionZ, vtkRenderer renderer);
该函数需要接收四个参数,前三个为(selectionX,selectionY,selectionZ),即鼠标的当前窗口坐标,其中selectionZ通常为0:第四个参数是vtkRenderer对象。
GetPickPosition()函数输出鼠标当前单击位置的世界坐标系下的坐标值。为了更加直观地显示鼠标左键按下的位置,在鼠标的单击位置生成了一个小红球。
3、单元拾取
1)概述
vtkCellPicker 类用于拾取模型中的某个单元。
2)代码
cs
private void TestCellPicker()
{
vtkSphereSource sphereSource = vtkSphereSource.New();
sphereSource.Update();
polyData = sphereSource.GetOutput();
selectedMapper = vtkDataSetMapper.New();
selectedActor = vtkActor.New();
//create a mapper and actor
vtkPolyDataMapper mapper = vtkPolyDataMapper.New();
mapper.SetInputConnection(sphereSource.GetOutputPort());
vtkActor actor = vtkActor.New();
actor.SetMapper(mapper);
actor.GetProperty().SetColor(0, 1, 0);
//creat a renderer,render window,and interactor
vtkRenderer renderer = vtkRenderer.New();
vtkRenderWindow renderWindow = vtkRenderWindow.New();
renderWindow.Render();
renderWindow.SetWindowName("CellPicker");
renderWindow.AddRenderer(renderer);
vtkCellPicker cellPicker = vtkCellPicker.New();
vtkRenderWindowInteractor windowInteractor = vtkRenderWindowInteractor.New();
windowInteractor.SetPicker(cellPicker);
windowInteractor.SetRenderWindow(renderWindow);
windowInteractor.LeftButtonPressEvt += WindowInteractor_LeftButtonPressEvt;
vtkInteractorStyleTrackballCamera style = new vtkInteractorStyleTrackballCamera();
windowInteractor.SetInteractorStyle(style);
renderer.AddActor(actor);
renderer.SetBackground(1, 1, 1);
renderWindow.Render();
windowInteractor.Initialize();
windowInteractor.Start();
}
cs
private void WindowInteractor_LeftButtonPressEvt(vtkObject sender, vtkObjectEventArgs e)
{
if (sender is vtkRenderWindowInteractor interactor)
{
int[] pos = interactor.GetEventPosition();
Console.WriteLine($"Picking pixel: {pos[0]} {pos[1]}");
vtkCellPicker pick = interactor.GetPicker() as vtkCellPicker;
// 拾取函数 4个参数 前三个为鼠标的当前窗口坐标,第四个参数是vktRenderer对象
pick.Pick(pos[0], pos[1], 0, interactor.GetRenderWindow().GetRenderers().GetFirstRenderer());
if (pick.GetCellId() != -1)
{
vtkIdTypeArray ids = vtkIdTypeArray.New();
ids.SetNumberOfComponents(1);
//拾取完毕,通过GetCellId()函数来得到当前拾取的单元索引号。
ids.InsertNextTuple1(pick.GetCellId());
//vtkPolyData的局部数据提取功能
//声明了要提取的数据的类型
vtkSelectionNode selectionNode = vtkSelectionNode.New();
selectionNode.SetFieldType((int)vtkSelectionNode.SelectionField.CELL); //设置数据的类型为单元
selectionNode.SetContentType((int)vtkSelectionNode.SelectionContent.INDICES); //设置数据的内容为索引号
selectionNode.SetSelectionList(ids);
vtkSelection selection = vtkSelection.New();
selection.Union(selectionNode);
//实现了数据提取功能
vtkExtractSelection extractSelection = vtkExtractSelection.New();
extractSelection.SetInputData(0, polyData);
extractSelection.SetInputData(1, selection);
extractSelection.Update();
selectedMapper.SetInputData(extractSelection.GetOutput() as vtkDataSet);
selectedActor.SetMapper(selectedMapper);
selectedActor.GetProperty().EdgeVisibilityOn();
selectedActor.GetProperty().SetEdgeColor(1, 0, 0);
selectedActor.GetProperty().SetLineWidth(3);
interactor.GetRenderWindow().GetRenderers().GetFirstRenderer().AddActor(selectedActor);
}
}
}
3)效果
4)说明
通过注册vtkRenderWindowInteractor的LeftButtonPressEvt事件响应函数。polyData 为被拾取的模型数据,需要通过外部设置。在响应鼠标左键消息时,首先定义了vtkCellPicker 对象,使用 Pick()函数实现拾取功能。拾取完毕,即可通过 GetCelld()函数来得到当前拾取的单元索引号。为了更方便地显示拾取的结果,可实现单元边的高亮显示。这里就涉及了vtkPolyData的局部数据提取功能。实现该功能时使用了几个新的类。vtkIdTypeAray对象存储当前选中的单元的索引号,每次只选择一个单元,因此每次该对象仅有一个索引号;vtkSelectionNode 对象与 vtkSelection 对象通常搭配使用,vtkSelection 实际上是一个 vtkSelectionNode 的数组,而vtkSelectionNode 则声明了要提取的数据的类型,这里 SetFieldType()设置数据的类型为单元,SetContentType()设置数据的内容为索引号。vtkExtractSelection 实现了数据提取功能,其第一个输入为被提取的 vtkPolyData 数据,第二个输入为 vkSelection 对象,标记要提取的数据类型。提取完毕,即可将提取的结果保存至一个vtkActor 对象,并添加至当前的 vtkRenderer中显示。
2、Prop拾取
1)概述
在渲染场景中拾取某一Prop对象时,使用的类是 vtkPropPicker。
2)代码
cs
private void TestPropPicker()
{
vtkRenderer renderer = vtkRenderer.New();
vtkRenderWindow renderWindow = vtkRenderWindow.New();
renderWindow.Render();
renderWindow.SetWindowName("PropPicker");
renderWindow.AddRenderer(renderer);
vtkRenderWindowInteractor windowInteractor = vtkRenderWindowInteractor.New();
windowInteractor.SetRenderWindow(renderWindow);
lastPickedProperty = vtkProperty.New();
windowInteractor.LeftButtonPressEvt += WindowInteractor_PropPickerEvt;
for (int i = 0; i < 10; i++)
{
vtkSphereSource source = vtkSphereSource.New();
double x, y, z, radius;
x = vtkMath.Random(-5, 5);
y = vtkMath.Random(-5, 5);
z = vtkMath.Random(-5, 5);
radius = vtkMath.Random(0.5, 1.0);
source.SetRadius(radius);
source.SetCenter(x, y, z);
source.SetPhiResolution(11);
source.SetThetaResolution(21);
vtkPolyDataMapper mapper = vtkPolyDataMapper.New();
mapper.SetInputConnection(source.GetOutputPort());
vtkActor actor = vtkActor.New();
actor.SetMapper(mapper);
double r, g, b;
r = vtkMath.Random(0.4, 1.0);
g = vtkMath.Random(0.4, 1.0);
b = vtkMath.Random(0.4, 1.0);
actor.GetProperty().SetDiffuseColor(r, g, b);
actor.GetProperty().SetDiffuse(0.8);
actor.GetProperty().SetSpecular(0.5);
actor.GetProperty().SetSpecularColor(1, 1, 1);
actor.GetProperty().SetSpecularPower(30);
renderer.AddActor(actor);
}
renderer.SetBackground(1, 1, 1);
renderWindow.Render();
windowInteractor.Initialize();
windowInteractor.Start();
}
cs
private void WindowInteractor_PropPickerEvt(vtkObject sender, vtkObjectEventArgs e)
{
/*
* 首先获取鼠标单击的坐标值,然后实例化一个vtkPropPicker对象,并调用Pick()函数实现Prop的拾取。
* 该类的主要功能是当用户单击渲染场景中的某个对象时,对所拾取的对象进行红色高亮显示。
* 为了便于恢复Actor原来的属性设置,程序中先储存当前拾取的Actorn属性值到LastPciedProperty中。
*/
if (sender is vtkRenderWindowInteractor interactor)
{
int[] clickPos = interactor.GetEventPosition();
//pick from this location.
vtkPropPicker picker = vtkPropPicker.New();
picker.Pick(clickPos[0], clickPos[1], 0, interactor.GetRenderWindow().GetRenderers().GetFirstRenderer());
double[] pos = picker.GetPickPosition();
if (lastPickedActor != null)
{
lastPickedActor.GetProperty().DeepCopy(lastPickedProperty);
}
lastPickedActor = picker.GetActor();
if (lastPickedActor != null)
{
// Save the property of the picked actor so that we can restore it next time
// 保存选取的 actor 的属性,以便下次可以恢复它
lastPickedProperty.DeepCopy(lastPickedActor.GetProperty());
// Highlight the picked actor by changing its properties
//通过更改其属性高亮显示选取的角色
lastPickedActor.GetProperty().SetColor(1, 0, 0);
lastPickedActor.GetProperty().SetDiffuse(1);
lastPickedActor.GetProperty().SetSpecular(0);
}
}
}
3)效果

4)说明
通过注册vtkRenderWindowInteractor的LeftButtonPressEvt事件响应函数。
首先获取鼠标单击的坐标值,然后实例化一个 vtkPropPicker对象,并调用Pick()函数实现Prop的拾取。
该类的主要功能是当用户单击渲染场景中的某个对象时,对所拾取的对象进行红色高亮显示。
为了便于恢复Actor原来的属性设置,程序中先储存当前拾取的Actor属性值到LastPickedProperty中,以便在下次拾取其他 Actor 对象时,将先前所拾取的对象恢复到原来的属性。
PropPickerInteractorStyle 类在 main()函数的使用方法与以上两例相似。
其他的拾取类,如vkAreaPicker可以根据用户提供的矩形框来选择该矩形框范围内的vtkActor/vtkProp 对象;vtkWroldPointPicker 可以实现窗口坐标到世界坐标的转换等,实现方法基本都是一致的。实现的关键在于定义和消息处理。