当前位置: 代码网 > it编程>编程语言>C/C++ > PyQt + VTK 三维软件 1. 自定义交互样式

PyQt + VTK 三维软件 1. 自定义交互样式

2024年08月06日 C/C++ 我要评论
修改了 VTK 自带的交互样式,带详细注释,可作为基础模板。

键鼠交互的功能和逻辑

左键旋转,右键平移,滚轮缩放,中键选点,按键上色。自定义交互样式类,重点是重载几个事件监听调用函数,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 表示右键,因为用鼠标移动事件来表示拖曳行为,但不同键按下时的拖曳行为对应的效果不同

监听鼠标移动:左键拖曳旋转,右键拖曳平移

实现的逻辑如下:

  1. 记录下当前鼠标位置
  2. 判断如果左键是按下的状态,就按照移动量旋转目标;如果右键是按下的状态,就平移相机;如果没有按下任何键,就不动作。
  3. 用当前鼠标位置更新上一时刻鼠标位置

那么平移和旋转分别如何实现呢?鼠标在二维的屏幕上发生了平移,因此输入的移动量是 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()

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com