键鼠交互的功能和逻辑
左键旋转,右键平移,滚轮缩放,中键选点,按键上色。自定义交互样式类,重点是重载几个事件监听调用函数,VTK 初始的那个交互样式说实话不是特别喜欢。也许我的习惯别人也不是很喜欢,打个模板大家自取,有更好的设计欢迎交换。
class CustomInteractorStyle(vtk.vtkInteractorStyleTrackballCamera):
"""
创建一个自定义的交互器样式
"""
def __init__(self):
"""
重载以下几个键鼠监听事件:
"""
# 鼠标左键的按和放
self.AddObserver("LeftButtonPressEvent", self.left_button_down)
self.AddObserver("LeftButtonReleaseEvent", self.left_button_up)
# 鼠标右键的按和放
self.AddObserver("RightButtonPressEvent", self.right_button_down)
self.AddObserver("RightButtonReleaseEvent", self.right_button_up)
# 鼠标中键
self.AddObserver('MiddleButtonPressEvent', self.middle_button_press_event)
# 鼠标的移动
self.AddObserver("MouseMoveEvent", self.mouse_move)
# 鼠标滚轮
self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)
# 键盘的按键
self.AddObserver("KeyPressEvent", self.key_press)
self.event_position = (0, 0) # 用于存储鼠标事件发生时鼠标的位置
self.mouse = 0 # 用 -1 表示左键,1 表示右键,因为用鼠标移动事件来表示拖曳行为,但不同键按下时的拖曳行为对应的效果不同
监听鼠标移动:左键拖曳旋转,右键拖曳平移
实现的逻辑如下:
- 记录下当前鼠标位置
- 判断如果左键是按下的状态,就按照移动量旋转目标;如果右键是按下的状态,就平移相机;如果没有按下任何键,就不动作。
- 用当前鼠标位置更新上一时刻鼠标位置
那么平移和旋转分别如何实现呢?鼠标在二维的屏幕上发生了平移,因此输入的移动量是 dx 和 dy。【旋转】比较简单,-dx(注意是用了负号) 和 -dy 分别对应 VTK 相机对象的 Azimuth() 和 Elevation() 方法就可以设置相机的方位角和俯仰角。需要注意的是,使用 Azimuth() 或 Yaw() 之后,在使用 Elevation() 之前需要重新计算 ViewUp。原因是视向量平行于视平面法向,因此在南极和北极存在奇异点,因此强制正交改变一下相机的坐标系(有一些优化不太好的三维软件就有这个毛病,鼠标控制旋转的时候老是达不到想要的效果,很烦人)。除此以外我在调试的时候还遇到一个问题,就是 VTK + PYQT 使用的时候,如果三维模型稍微大一些,在旋转的时候会有一部分被隐藏掉,其实是因为 VTK 的渲染优化吧,就是只显示比当前模型稍大一些的一个矩形框,框外是不渲染的,所以旋转的时候就需要用 ResetCameraClippingRange() 重置一下这个截断矩形框。
def rotate_camera(self, delta_x, delta_y):
speed = 0.5
camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
camera.Azimuth(-delta_x * speed)
camera.OrthogonalizeViewUp()
camera.Elevation(-delta_y * speed)
# 防止在旋转的时候截断显示
self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCameraClippingRange()
self.GetInteractor().Render()
【平移】原理也很简单,但是实现稍微麻烦一点,因为涉及到窗口的平面坐标系转换到三维世界坐标系。当然 VTK 也是提供了 ComputeWorldToDisplay() 和 ComputeDisplayToWorld() 方法,然后我做的是把当前相机的 FocalPoint 和 Position 一起移动,这样视角会发生轻微的旋转,因为 FocalPoint 并不是无限远的点;而且也不是只改 Position,我个人觉得这样稍微自然一些。
def mouse_move(self, obj, event):
new_position = self.GetInteractor().GetEventPosition()
if self.mouse == -1:
# 左键旋转
delta_x = new_position[0] - self.event_position[0]
delta_y = new_position[1] - self.event_position[1]
self.event_position = self.GetInteractor().GetEventPosition()
self.rotate_camera(delta_x, delta_y)
elif self.mouse == 1:
# 右键平移
delta_x = new_position[0] - self.event_position[0]
delta_y = new_position[1] - self.event_position[1]
self.move_camera(self.event_position[0], self.event_position[1], delta_x, delta_y)
self.event_position = new_position
监听鼠标滚轮:滚动缩放,中键选点
【缩放】就比较简单了,VTK 相机对象的 Zoom() 方法,传递一个比例系数就可以。
def zoom_camera(self, factor):
camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
camera.Zoom(factor)
self.GetInteractor().Render()
【选点】选点的功能可以借助 VTK 的 vtkCellPicker 或 vtkPointPicker,查阅了一下资料,这两个选点器大致相同,唯一的区别是 vtkCellPicker 会返回可见面上对象的信息而 vtkPointPicker 不一定。这是因为 vtkCellPicker 会向三维场景发射一条射线,并返回射线第一次击中的对象信息,如果没有击中任何对象就返回 -1,而 vtkPointPicker 会返回在指定容差范围内投影到射线上距离最近的点的 id,这就说明 vtkPointPicker 返回的不一定是可见面上的点。两个选点器用法差不多,按需求选。同时,为了可视化选点,我还在选点器击中的位置那里画了一个红色小球,当然这样的话,后期如果要清除这些红色小球要从渲染器中遍历对象挨个剔除。
def middle_button_press_event(self, obj, event):
# 获取中键按下时鼠标的位置
pos = self.GetInteractor().GetEventPosition()
# 创建选点器
picker = vtk.vtkCellPicker()
# 从渲染器中选点
render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
picker.Pick(pos[0], pos[1], 0, render)
if picker.GetPointId() != -1:
# 击中对象在世界坐标系中的位置
world_position = picker.GetPickPosition()
print(f'选点位置: ({world_position[0]:.6g}, {world_position[1]:.6g}, {world_position[2]:.6g})')
print("选点的 id", picker.GetPointId())
# 为了高亮显示,所以在击中对象处创建一个红色小球用于标识
sphere_source = vtk.vtkSphereSource()
sphere_source.SetCenter(world_position[0], world_position[1], world_position[2])
sphere_source.SetRadius(0.3)
# 创建一个映射器和渲染对象
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(sphere_source.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(1, 0, 0) # 设置为红色
# 将渲染对象添加到渲染器中
render.AddActor(actor)
self.OnMiddleButtonDown()
监听键盘按键:按 C 一键换色
【键盘】键盘交互肯定不能少的,但按键很多,根据自己需求挨个定义就行,比如我这里定义了一个按下 C 键用随机的颜色涂满所有的 actor。
def key_press(self, obj, event):
key = self.GetInteractor().GetKeySym()
if key == 'c' or key == 'C':
# 产生随机颜色
r = vtk.vtkMath.Random()
g = vtk.vtkMath.Random()
b = vtk.vtkMath.Random()
actors = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActors()
for actor in actors:
actor.GetProperty().SetColor(r, g, b)
# 更新渲染
self.GetInteractor().Render()
完整代码
可以把完整代码保存成一个 .py 文件然后通过下面这种方式导入,在设置 VTK 交互器的时候使用 SetInteractorStyle() 即可。
from VTKQTInteractorStyle import CustomInteractorStyle
完整的类定义,以及一个简单的用法示例如下:
import vtk
class CustomInteractorStyle(vtk.vtkInteractorStyleTrackballCamera):
"""
创建一个自定义的交互器样式
"""
def __init__(self):
"""
重载以下几个键鼠监听事件:
"""
# 鼠标左键的按和放
self.AddObserver("LeftButtonPressEvent", self.left_button_down)
self.AddObserver("LeftButtonReleaseEvent", self.left_button_up)
# 鼠标右键的按和放
self.AddObserver("RightButtonPressEvent", self.right_button_down)
self.AddObserver("RightButtonReleaseEvent", self.right_button_up)
# 鼠标中键
self.AddObserver('MiddleButtonPressEvent', self.middle_button_press_event)
# 鼠标的移动
self.AddObserver("MouseMoveEvent", self.mouse_move)
# 鼠标滚轮
self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)
# 键盘的按键
self.AddObserver("KeyPressEvent", self.key_press)
self.event_position = (0, 0) # 用于存储鼠标事件发生时鼠标的位置
self.mouse = 0 # 用 -1 表示左键,1 表示右键,因为用鼠标移动事件来表示拖曳行为,但不同键按下时的拖曳行为对应的效果不同
def left_button_down(self, obj, event):
# 按下左键
self.mouse = -1
self.event_position = self.GetInteractor().GetEventPosition()
def left_button_up(self, obj, event):
# 松开左键
self.mouse = 0
self.event_position = (0, 0)
def right_button_down(self, obj, event):
# 按下右键
self.mouse = 1
self.event_position = self.GetInteractor().GetEventPosition()
def right_button_up(self, obj, event):
# 松开右键
self.mouse = 0
self.event_position = (0, 0)
def mouse_move(self, obj, event):
new_position = self.GetInteractor().GetEventPosition()
if self.mouse == -1:
# 左键旋转
delta_x = new_position[0] - self.event_position[0]
delta_y = new_position[1] - self.event_position[1]
self.event_position = self.GetInteractor().GetEventPosition()
self.rotate_camera(delta_x, delta_y)
elif self.mouse == 1:
# 右键平移
delta_x = new_position[0] - self.event_position[0]
delta_y = new_position[1] - self.event_position[1]
self.move_camera(self.event_position[0], self.event_position[1], delta_x, delta_y)
self.event_position = new_position
def rotate_camera(self, delta_x, delta_y):
speed = 0.5
camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
camera.Azimuth(-delta_x * speed)
camera.OrthogonalizeViewUp()
camera.Elevation(-delta_y * speed)
# 防止在旋转的时候截断显示
self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCameraClippingRange()
self.GetInteractor().Render()
def move_camera(self, x, y, delta_x, delta_y):
render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
camera = render.GetActiveCamera()
view_focus_3d = camera.GetFocalPoint()
view_focus_2d = [0, 0, 0]
self.ComputeWorldToDisplay(render, view_focus_3d[0], view_focus_3d[1], view_focus_3d[2], view_focus_2d)
new_mouse_point = [0, 0, 0, 1]
self.ComputeDisplayToWorld(render, x, y, view_focus_2d[2], new_mouse_point)
old_mouse_point = [0, 0, 0, 1]
self.ComputeDisplayToWorld(render, x - delta_x, y - delta_y, view_focus_2d[2], old_mouse_point)
motion_vector = [0, 0, 0]
motion_vector[0] = old_mouse_point[0] - new_mouse_point[0]
motion_vector[1] = old_mouse_point[1] - new_mouse_point[1]
motion_vector[2] = old_mouse_point[2] - new_mouse_point[2]
view_focus = camera.GetFocalPoint()
view_point = camera.GetPosition()
camera.SetFocalPoint(motion_vector[0] + view_focus[0], motion_vector[1] + view_focus[1],
motion_vector[2] + view_focus[2])
camera.SetPosition(motion_vector[0] + view_point[0], motion_vector[1] + view_point[1],
motion_vector[2] + view_point[2])
self.GetInteractor().Render()
def mouse_wheel_forward(self, obj, event):
self.zoom_camera(1.1)
def mouse_wheel_backward(self, obj, event):
self.zoom_camera(0.9)
def zoom_camera(self, factor):
camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
camera.Zoom(factor)
self.GetInteractor().Render()
def key_press(self, obj, event):
key = self.GetInteractor().GetKeySym()
if key == 'c' or key == 'C':
# 产生随机颜色
r = vtk.vtkMath.Random()
g = vtk.vtkMath.Random()
b = vtk.vtkMath.Random()
actors = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActors()
for actor in actors:
actor.GetProperty().SetColor(r, g, b)
# 更新渲染
self.GetInteractor().Render()
def middle_button_press_event(self, obj, event):
# 获取中键按下时鼠标的位置
pos = self.GetInteractor().GetEventPosition()
# 创建选点器
picker = vtk.vtkCellPicker()
# 从渲染器中选点
render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
picker.Pick(pos[0], pos[1], 0, render)
if picker.GetPointId() != -1:
# 击中对象在世界坐标系中的位置
world_position = picker.GetPickPosition()
print(f'选点位置: ({world_position[0]:.6g}, {world_position[1]:.6g}, {world_position[2]:.6g})')
print("选点的 id", picker.GetPointId())
# 为了高亮显示,所以在击中对象处创建一个红色小球用于标识
sphere_source = vtk.vtkSphereSource()
sphere_source.SetCenter(world_position[0], world_position[1], world_position[2])
sphere_source.SetRadius(0.3)
# 创建一个映射器和渲染对象
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(sphere_source.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(1, 0, 0) # 设置为红色
# 将渲染对象添加到渲染器中
render.AddActor(actor)
self.OnMiddleButtonDown()
if __name__ == '__main__':
# 创建一个渲染器
renderer = vtk.vtkRenderer()
# 创建一个 STL 文件读取器
reader = vtk.vtkSTLReader()
file_path = "Arduino.STL"
reader.SetFileName(file_path)
reader.Update()
# 获取读取的数据
polyData = reader.GetOutput()
# 创建一个 Mapper 和 Actor 来显示模型
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polyData)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
renderer.AddActor(actor)
# 创建一个渲染窗口
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
# 创建一个交互器
interactor = vtk.vtkRenderWindowInteractor()
interactor.SetRenderWindow(renderWindow)
# 设置自定义的交互器样式
style = CustomInteractorStyle()
style.SetDefaultRenderer(renderer)
interactor.SetInteractorStyle(style)
# 设置相机的裁剪范围
renderer.ResetCamera()
renderer.ResetCameraClippingRange()
# 开始渲染和交互
renderWindow.Render()
interactor.Start()