PyOpenGL ArcBall导航:视图坐标转物体坐标及旋转轴对齐问题求助
解决ArcBall旋转轴与相机视图对齐的问题
我明白你的困扰——当你旋转模型后,ArcBall的旋转轴跟着模型走,导致后续操作不符合“所见即所得”的预期。核心问题是你现在把相机视图变换和物体模型变换混在同一个OpenGL模型视图矩阵里了,旋转轴的空间参考系被打乱了。下面是具体的解决思路和Python实现:
核心思路
要让旋转轴始终对齐相机视图,我们需要:
- 分离相机视图变换和物体模型变换:分别维护相机的视图矩阵和物体的模型矩阵,不再依赖OpenGL的矩阵栈累积变换。
- 将相机空间的旋转轴转换到物体局部空间:ArcBall计算出的旋转轴是相机空间的,我们需要通过逆矩阵把它转换到物体的局部坐标系,这样旋转就会基于你看到的相机视角来执行。
修改后的完整代码
import pygame from pygame.locals import * from OpenGL.GL import * from OpenGL.GLU import * import math import numpy as np size = 30 amplitude_amplificator = 80 color_table = ((1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (0,1,1), (1,0.5,0), (0.5,1,0), (0.5,1,0.5), (0,0.5,0) ) locations = ((0,-975, 0), (0, 975, 0), (-1273,-975, 0), (-1273, 975, 0), (-2482, -975, 0), (-2482, 975, 0), (-3737, -975, 0), (-3737, 975, 0) ) lines = ((0,2), (2, 4), (4, 6), (1, 3), (3, 5), (5, 7), (0, 1), (2, 3), (4, 5), (6, 7), ) amplitudes = ((3.38829249165602, 2.38305866657961, 2.52151563664636), (5.08487438107113, 2.36432294667884, 3.0843991148654), (3.44312569856563, 1.23112415468012, 1.29869765112226), (4.0421066637935, 1.40655294535107, 1.36083778879317), (3.78074337117764, 0.648255908566916, 0.752239154016233), (5.08887133464996, 0.607037324785205, 0.543523234321567), (4.49095206021647, 0.432732677308301, 2.18289872563964), (5.14707697114171, 0.335119576625248, 2.15666871777855) ) phases = ((-146.873017352057,0,-95.316526141321), (-149.008372080797, 5.24886681104675, 78.3075732082314), (-148.241584335287, 5.54327579087787, -118.279685417256), (-151.844141596427, 6.48705235395368, -113.246406750217), (-148.14233553496, 27.9523171503408, 65.8254568277543), (-157.058723259828, 38.8760924034639, 85.2339573112435), (-153.417593784393, -120.329988461629, 16.0421535833842), (-156.779107376825, 83.2350395893582, 10.7592173681729) ) # DRAW CUBE def Cube(po,si,co): POS = ( (po[0]+si, po[1]-si, po[2]-si), (po[0]+si, po[1]+si, po[2]-si), (po[0]-si, po[1]+si, po[2]-si), (po[0]-si, po[1]-si, po[2]-si), (po[0]+si, po[1]-si, po[2]+si), (po[0]+si, po[1]+si, po[2]+si), (po[0]-si, po[1]-si, po[2]+si), (po[0]-si, po[1]+si, po[2]+si) ) edges = ( (0,1), (0,3), (0,4), (2,1), (2,3), (2,7), (6,3), (6,4), (6,7), (5,1), (5,4), (5,7) ) glBegin(GL_LINES) for edge in edges: for vertex in edge: glColor3f(co[0],co[1],co[2]) glVertex3fv(POS[vertex]) glEnd() #DRAW ORIGINAL SHAPE IN LINES def Line_orig(po): glBegin(GL_LINES) for edge in po: for vertex in edge: glVertex3fv(locations[vertex]) glEnd() # 规范化鼠标坐标到单位半球 def get_arcball_point(mx, my, display_width, display_height): # 转换到[-1,1]范围,同时翻转Y轴(PyGame Y轴从上到下,OpenGL从下到上) nx = 2 * mx / display_width - 1 ny = 1 - 2 * my / display_height length_sq = nx*nx + ny*ny # 如果超出单位圆,归一化到圆上 if length_sq > 1.0: scale = 1.0 / math.sqrt(length_sq) nx *= scale ny *= scale length_sq = 1.0 nz = math.sqrt(1.0 - length_sq) return (nx, ny, nz) # Calculate angle of two spatial vectors def angle_calculation(a,b): dot = np.dot(a, b) norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) # 避免数值误差导致acos超出范围 dot = np.clip(dot, -1.0, 1.0) r = math.degrees(math.acos(dot/(norm_a*norm_b))) return r # 构造绕轴旋转的矩阵(Rodrigues公式) def create_rotation_matrix(axis, angle_deg): angle_rad = math.radians(angle_deg) cos_a = math.cos(angle_rad) sin_a = math.sin(angle_rad) one_minus_cos = 1.0 - cos_a x, y, z = axis return np.array([ [cos_a + x*x*one_minus_cos, x*y*one_minus_cos - z*sin_a, x*z*one_minus_cos + y*sin_a, 0], [y*x*one_minus_cos + z*sin_a, cos_a + y*y*one_minus_cos, y*z*one_minus_cos - x*sin_a, 0], [z*x*one_minus_cos - y*sin_a, z*y*one_minus_cos + x*sin_a, cos_a + z*z*one_minus_cos, 0], [0, 0, 0, 1] ], dtype=np.float32) def main(): mouse_pressed = 0 pygame.init() display = (1200,800) pygame.display.set_mode(display, DOUBLEBUF|OPENGL) # 设置投影矩阵 glMatrixMode(GL_PROJECTION) glLoadIdentity() gluPerspective(45, (display[0]/display[1]), 0.1, 30000.0) # 初始化视图矩阵(相机变换:初始向后移动10000) view_matrix = np.identity(4, dtype=np.float32) view_matrix = np.dot(view_matrix, np.array([ [1,0,0,0], [0,1,0,0], [0,0,1,-10000], [0,0,0,1] ], dtype=np.float32)) # 初始化模型矩阵(物体变换:初始无变换) model_matrix = np.identity(4, dtype=np.float32) p1 = None # 存储鼠标按下时的ArcBall点 while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) time = pygame.time.get_ticks()/1000 # 处理鼠标拖动 mouse_state = pygame.mouse.get_pressed() if mouse_state[0] == 1: if mouse_pressed == 0: mouse_pressed = 1 # 获取初始ArcBall点 mx, my = pygame.mouse.get_pos() p1 = get_arcball_point(mx, my, display[0], display[1]) else: # 获取当前ArcBall点 mx, my = pygame.mouse.get_pos() p2 = get_arcball_point(mx, my, display[0], display[1]) # 计算旋转轴(相机空间) axis_camera = np.cross(p1, p2) axis_camera_norm = axis_camera / np.linalg.norm(axis_camera) # 计算旋转角度 angle = angle_calculation(p1, p2) if angle > 0.01: # 避免微小旋转 # 1. 将相机空间轴转换到世界空间:用视图矩阵的逆 inv_view_matrix = np.linalg.inv(view_matrix) inv_view_rot = inv_view_matrix[:3, :3] # 提取旋转部分(忽略平移) axis_world = np.dot(inv_view_rot, axis_camera_norm) axis_world = axis_world / np.linalg.norm(axis_world) # 2. 将世界空间轴转换到物体局部空间:用模型矩阵的逆 inv_model_matrix = np.linalg.inv(model_matrix) inv_model_rot = inv_model_matrix[:3, :3] axis_local = np.dot(inv_model_rot, axis_world) axis_local = axis_local / np.linalg.norm(axis_local) # 3. 创建旋转矩阵并更新模型矩阵 rot_matrix = create_rotation_matrix(axis_local, angle) model_matrix = np.dot(rot_matrix, model_matrix) # 更新p1为当前点,实现连续旋转 p1 = p2 else: mouse_pressed = 0 p1 = None # 处理键盘相机平移 keys=pygame.key.get_pressed() translate_step = 100 if keys[K_w]: # 相机向上移动 translate_mat = np.array([ [1,0,0,0], [0,1,0,translate_step], [0,0,1,0], [0,0,0,1] ], dtype=np.float32) view_matrix = np.dot(view_matrix, translate_mat) if keys[K_s]: translate_mat = np.array([ [1,0,0,0], [0,1,0,-translate_step], [0,0,1,0], [0,0,0,1] ], dtype=np.float32) view_matrix = np.dot(view_matrix, translate_mat) if keys[K_a]: translate_mat = np.array([ [1,0,0,-translate_step], [0,1,0,0], [0,0,1,0], [0,0,0,1] ], dtype=np.float32) view_matrix = np.dot(view_matrix, translate_mat) if keys[K_d]: translate_mat = np.array([ [1,0,0,translate_step], [0,1,0,0], [0,0,1,0], [0,0,0,1] ], dtype=np.float32) view_matrix = np.dot(view_matrix, translate_mat) # 设置模型视图矩阵:视图矩阵 × 模型矩阵 glMatrixMode(GL_MODELVIEW) glLoadIdentity() # OpenGL使用列主序,numpy是行主序,所以需要转置 glLoadMatrixf(view_matrix.T) glMultMatrixf(model_matrix.T) # Drawing the Cubes at Nodes Loactions for item, el in enumerate(locations): Cube((el[0] + amplitudes[item][0]*math.sin(time + phases[item][0]*(math.pi/180))*amplitude_amplificator, el[1] + amplitudes[item][1]*math.sin(time + phases[item][1]*(math.pi/180))*amplitude_amplificator, el[2] + amplitudes[item][2]*math.sin(time + phases[item][2]*(math.pi/180))*amplitude_amplificator ), size, color_table[item]) # Drawing the Original Shapes (Specified nodes in Lines Tuple) Line_orig(lines) # Drawing the Deformed Shape glBegin(GL_LINES) for edge in lines: for vertex in edge: glVertex3fv((locations[vertex][0] + amplitudes[vertex][0]*math.sin(time + phases[vertex][0]*(math.pi/180))*amplitude_amplificator, locations[vertex][1] + amplitudes[vertex][1]*math.sin(time + phases[vertex][1]*(math.pi/180))*amplitude_amplificator , locations[vertex][2] + amplitudes[vertex][2]*math.sin(time + phases[vertex][2]*(math.pi/180))*amplitude_amplificator, )) glEnd() # OpenGL Management pygame.display.flip() pygame.time.wait(10) main()
关键修改说明
分离视图与模型矩阵:
view_matrix:维护相机的位置和朝向,键盘WASD控制的是这个矩阵,不会影响物体的旋转参考系。model_matrix:单独维护物体的旋转和变换,所有ArcBall旋转都作用在这个矩阵上。
旋转轴空间转换:
- 首先把相机空间的旋转轴通过
inv_view_matrix转换到世界空间,再通过inv_model_matrix转换到物体局部空间。这样旋转就始终基于你当前看到的相机视角,实现“所见即所得”。
- 首先把相机空间的旋转轴通过
鼠标坐标修正:
- 翻转了Y轴坐标,解决PyGame和OpenGL坐标系方向不一致导致的旋转
相关产品推荐
相关产品推荐

