0%

使用 vtk+imgui+glfw 获得一个可交互的 vtk 可视化区域

孩子们,我回来了。

简介

最近在做毕业设计,主要的工作是看看能不能用 n 卡的新特性整一些 NURBS 曲面求交加速的功能。为了展示结果,我使用了 Python 的 VTK 库,但是同时为了可以使用 imgui 进行一些简单的交互,我也安装了 imgui[glfw] 库。然而,这两者之间如何结合在一起,也就是让 VTK 作为 GLFW 的后端卡了我很长时间,最终解决了这个问题。因此在此进行一个简单的记录。

初始化

首先我们先需要 import 一些库,在这里我们使用的是 glfw 为后端的 imgui 以及 vtk 库,安装方法如下:

1
2
pip install imgui[glfw]
pip install vtk

然后在之后的操作中我们需要的是这些库:

1
2
3
4
5
import imgui
import glfw
import vtk
import numpy as np
from imgui.integrations.glfw import GlfwRenderer

首先是对 glfw 窗口进行初始化,并且将初始化一个 vtk 的渲染窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def impl_glfw_init(window_name, width=1280, height=720):
# Initialize the GLFW library
if not glfw.init():
print("Could not initialize OpenGL context")
exit(1)

# Create a windowed mode window and its OpenGL context
window = glfw.create_window(int(width), int(height), window_name, None, None)
if not window:
glfw.terminate()
print("Could not initialize Window")
exit(1)

# Make the window's context current
glfw.make_context_current(window)

# Create a VTK renderer
renderer = vtk.vtkRenderer()

# Create a VTK render window
render_window = vtk.vtkExternalOpenGLRenderWindow()
render_window.SetSize(width, height)
render_window.AddRenderer(renderer)

return window, render_window, renderer

值得注意的点就是我们在这里使用了 vtk.vtkExternalOpenGLRenderWindow() 作为渲染窗口,从而使得 vtk 可以和 glfw 进行交互。

然后就是对 imgui 和摄像机这些东西进行初始化。

1
2
3
4
5
6
7
# Initialize ImGui and the GLFW renderer
imgui.create_context()
impl = GlfwRenderer(window)

# Reset the camera and set the background color
renderer.ResetCamera()
renderer.SetBackground(1.0, 1.0, 1.0)

鼠标操作

在这里我们实现了左键旋转右键移动滚轮缩放的操作,具体代码如下:

首先我们需要定义一个起始鼠标位置和一个用于控制的操作程度缩放比(在这里我们使用的是窗口的大小)。

1
2
3
# Initialize the last mouse position and the window size
lastX, lastY = width // 2, height // 2
windowSize = [width, height]

然后我们需要定义三个函数,分别是鼠标按钮事件、鼠标滚轮事件和鼠标移动事件。

鼠标按钮事件

1
2
3
4
5
6
7
8
9
10
# Define the mouse button event handler
def on_mouse_button(window, button, action, mods):
global lastX, lastY
if imgui.get_io().want_capture_mouse:
return
if button == glfw.MOUSE_BUTTON_LEFT:
if action == glfw.PRESS:
(xpos, ypos) = glfw.get_cursor_pos(window)
lastX = xpos
lastY = ypos

在按钮事件中,我们设定了初始的指针位置,用于后续的拖动操作。同时注意到在第 4-5 行的代码我们避开了 imgui 的窗口。

鼠标移动事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Define the mouse move event handler
def on_mouse_move(window, xpos, ypos):
global lastX, lastY
if imgui.get_io().want_capture_mouse:
return
camera = renderer.GetActiveCamera()
if glfw.get_mouse_button(window, glfw.MOUSE_BUTTON_LEFT) == glfw.PRESS:
deltaX = xpos - lastX
deltaY = ypos - lastY
# Get the current camera
camera.Azimuth(-deltaX)
camera.Elevation(deltaY)
elif glfw.get_mouse_button(window, glfw.MOUSE_BUTTON_RIGHT) == glfw.PRESS:
deltaX = xpos - lastX
deltaY = ypos - lastY
# Scale
deltaX /= windowSize[0]
deltaY /= windowSize[1]
# Pan the camera
pan_camera(camera, -deltaX, deltaY)

renderer.ResetCameraClippingRange()
camera.OrthogonalizeViewUp()
lastX = xpos
lastY = ypos

简单来说就是我们通过判断按下的是左键还是右键从而决定是旋转还是平移,如果是旋转只需要调用 camera.Azimuth()camera.Elevation 即可,如果需要平移,由于这里我们并没有一个定义好的函数,因此我们需要自己实现pan_camera

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def pan_camera(camera, right_amount, up_amount):
# Get the current position, focal point and view up vector of the camera
position = camera.GetPosition()
focal_point = camera.GetFocalPoint()
view_up = camera.GetViewUp()

# Convert tuples to numpy arrays
position = np.array(position)
focal_point = np.array(focal_point)
view_up = np.array(view_up)

# Compute the direction of projection or the view plane normal
view_plane_normal = position - focal_point
view_plane_normal = view_plane_normal / np.linalg.norm(view_plane_normal)

# Compute the right vector (cross product of view up vector and view plane normal)
right_vector = np.cross(view_up, view_plane_normal)
right_vector = right_vector / np.linalg.norm(right_vector)

# Compute the up vector (cross product of view plane normal and right vector, to ensure orthogonal)
up_vector = np.cross(view_plane_normal, right_vector)
up_vector = up_vector / np.linalg.norm(up_vector)

# Scale the pan movements to match the specified amounts
right_movement = right_vector * right_amount
up_movement = up_vector * up_amount

# Calculate the new camera position and focal point
new_position = position + right_movement + up_movement
new_focal_point = focal_point + right_movement + up_movement

# Update the camera's position and focal point
camera.SetPosition(new_position.tolist())
camera.SetFocalPoint(new_focal_point.tolist())
camera.SetViewUp(
view_up.tolist()
) # Ensure to reset the view up vector too, if it has been changed

简单来说就是根据相机位置和焦点位置得到 look_at 向量,相机中自身储存的 view_up 向量作为第二个向量,两者进行叉乘即可得到一个和 look_at 向量垂直的向量,这个向量和 view_up 向量张成的平面就是相机可以平移的平面,同时它们是正交的,因此我们可以通过移动向量加法对相机进行移动。

在这些操作结束之后,别忘了更新相机的裁切范围以及各个向量。

鼠标滚轮事件

这个就比较简单,对相机焦距进行缩放即可。

1
2
3
4
5
6
7
8
9
10
# Define the mouse scroll event handler
def on_mouse_scroll(window, xoffset, yoffset):
if imgui.get_io().want_capture_mouse:
return
camera = renderer.GetActiveCamera()
if yoffset > 0:
camera.Zoom(1.1)
else:
camera.Zoom(0.9)
renderer.ResetCameraClippingRange()

绑定及主循环

之后我们只需要将上述三个事件进行绑定就行了。

1
2
3
4
# Set the mouse event callbacks
glfw.set_cursor_pos_callback(window, on_mouse_move)
glfw.set_scroll_callback(window, on_mouse_scroll)
glfw.set_mouse_button_callback(window, on_mouse_button)

然后就是主循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Main loop
while not glfw.window_should_close(window):
# Handle ImGui inputs and start a new ImGui frame
impl.process_inputs()
imgui.new_frame()
imgui.begin("Dashboard", True)

# ... Create ImGui windows here

# End the ImGui frame and render the ImGui output
imgui.end()
imgui.render()
render_window.Render()

# ... Some operations for scene updates here

# Render the ImGui output and swap the GLFW buffers
impl.render(imgui.get_draw_data())
glfw.swap_buffers(window)
glfw.poll_events()
# Shutdown ImGui and terminate GLFW
impl.shutdown()
glfw.terminate()

由此,我们就可以使用 vtk 进行场景修改,同时通过 imgui 进行交互操作了。