You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyOpenGL ArcBall导航:视图坐标转物体坐标及旋转轴对齐问题求助

解决ArcBall旋转轴与相机视图对齐的问题

我明白你的困扰——当你旋转模型后,ArcBall的旋转轴跟着模型走,导致后续操作不符合“所见即所得”的预期。核心问题是你现在把相机视图变换和物体模型变换混在同一个OpenGL模型视图矩阵里了,旋转轴的空间参考系被打乱了。下面是具体的解决思路和Python实现:

核心思路

要让旋转轴始终对齐相机视图,我们需要:

  1. 分离相机视图变换和物体模型变换:分别维护相机的视图矩阵和物体的模型矩阵,不再依赖OpenGL的矩阵栈累积变换。
  2. 将相机空间的旋转轴转换到物体局部空间: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()

关键修改说明

  1. 分离视图与模型矩阵:

    • view_matrix:维护相机的位置和朝向,键盘WASD控制的是这个矩阵,不会影响物体的旋转参考系。
    • model_matrix:单独维护物体的旋转和变换,所有ArcBall旋转都作用在这个矩阵上。
  2. 旋转轴空间转换:

    • 首先把相机空间的旋转轴通过inv_view_matrix转换到世界空间,再通过inv_model_matrix转换到物体局部空间。这样旋转就始终基于你当前看到的相机视角,实现“所见即所得”。
  3. 鼠标坐标修正:

    • 翻转了Y轴坐标,解决PyGame和OpenGL坐标系方向不一致导致的旋转
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:17:26