键鼠交互的功能和逻辑
左键旋转,右键平移,滚轮缩放,中键选点,按键上色。自定义交互样式类,重点是重载几个事件监听调用函数,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()
发表评论