如何基于矩阵手动实现Python 3D透视投影的第一人称相机视角?
问题
我正在学习Python中的3D透视投影与矩阵运算,已掌握基础内容,但不清楚如何将当前移动物体的代码修改为类似《我的世界》(Minecraft)的第一人称相机移动逻辑。此前我通过PyOpenGL的GL矩阵命令实现过类似效果,但底层逻辑被封装。我知道相机移动效果需要同时对物体进行旋转和平移,但在基于相机局部轴控制移动速度、角度时存在困惑。我希望不依赖PyOpenGL函数,纯手动实现该功能,恳请提供示例或原理讲解。
以下是我参考GitHub仓库修改后的代码:
import pygame import os import math from matrix import matrix_multiplication os.environ["SDL_VIDEO_CENTERED"]='1' black, white, blue = (20, 20, 20), (230, 230, 230), (0, 154, 255) width, height = 640, 480 pygame.init() pygame.display.set_caption("3D cube Projection") screen = pygame.display.set_mode((width, height)) clock = pygame.time.Clock() fps = 60 Xangle = 0 Yangle = 0 Zangle = 0 cube_position = [width//2, height//2] scale = 600 speed = 0.01 points = [n for n in range(8)] points[0] = [[-1], [-1], [1]] points[1] = [[1], [-1], [1]] points[2] = [[1], [1], [1]] points[3] = [[-1], [1], [1]] points[4] = [[-1], [-1], [-1]] points[5] = [[1], [-1], [-1]] points[6] = [[1], [1], [-1]] points[7] = [[-1], [1], [-1]] def connect_point(i, j, k): a = k[i] b = k[j] pygame.draw.line(screen, black, (a[0], a[1]), (b[0], b[1]), 2) run = True while run: clock.tick(fps) screen.fill(white) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: Yangle += speed if keys[pygame.K_RIGHT]: Yangle -= speed if keys[pygame.K_UP]: Xangle += speed if keys[pygame.K_DOWN]: Xangle -= speed if keys[pygame.K_p]: Zangle += speed if keys[pygame.K_o]: Zangle -= speed if keys[pygame.K_r]: Yangle = 0 Xangle = 0 Zangle = 0 index = 0 projected_points = [j for j in range(len(points))] rotation_x = [[1, 0, 0], [0, math.cos(Xangle), -math.sin(Xangle)], [0, math.sin(Xangle), math.cos(Xangle)]] rotation_y = [[math.cos(Yangle), 0, -math.sin(Yangle)], [0, 1, 0], [math.sin(Yangle), 0, math.cos(Yangle)]] rotation_z = [[math.cos(Zangle), -math.sin(Zangle), 0], [math.sin(Zangle), math.cos(Zangle), 0], [0, 0 ,1]] for point in points: rotated_2d = matrix_multiplication(rotation_y, point) rotated_2d = matrix_multiplication(rotation_x, rotated_2d) rotated_2d = matrix_multiplication(rotation_z, rotated_2d) distance = 5 z = 1/(distance - rotated_2d[2][0]) projection_matrix = [[z, 0, 0], [0, z, 0]] projected_2d = matrix_multiplication(projection_matrix, rotated_2d) x = int(projected_2d[0][0] * scale) + cube_position[0] y = int(projected_2d[1][0] * scale) + cube_position[1] projected_points[index] = [x, y] pygame.draw.circle(screen, blue, (x, y), 10) index += 1 #draw edges for m in range(4): connect_point(m, (m+1)%4, projected_points) connect_point(m+4, (m+1)%4 + 4, projected_points) connect_point(m, m+4, projected_points) #Xangle += speed #Yangle += speed #Zangle += speed pygame.display.update() pygame.quit()
原理讲解
第一人称相机的核心逻辑是模拟相机在3D空间中的位置和朝向,而非直接旋转/移动物体。等价于:保持相机不动,将整个3D世界进行反向的平移和旋转变换,让世界看起来像是相机在移动。
相机状态存储
需要记录相机的3D位置(如[x,y,z]),以及朝向角度(偏航角Yaw=Y轴旋转,俯仰角Pitch=X轴旋转,Roll=Z轴旋转,Minecraft中通常忽略Roll)。局部坐标系转换
相机的移动是基于自身局部坐标系的:- 前进/后退:沿着相机的前方向向量移动,该向量由Yaw和Pitch计算得出
- 左右平移:沿着相机的右方向向量移动,该向量由Yaw计算得出(与前方向垂直)
- 上下移动:可直接沿世界坐标系Y轴或相机上方向移动
矩阵变换顺序
对世界中的每个点,需先执行反向平移(减去相机位置,模拟相机移动),再执行反向旋转(用相机角度的负角度旋转,模拟相机转向),最后进行透视投影。
修改后的代码示例
以下是纯手动实现的第一人称相机版本,核心修改包括添加相机位置管理、基于局部轴的移动逻辑,调整矩阵变换顺序:
import pygame import os import math from matrix import matrix_multiplication os.environ["SDL_VIDEO_CENTERED"]='1' black, white, blue = (20, 20, 20), (230, 230, 230), (0, 154, 255) width, height = 640, 480 pygame.init() pygame.display.set_caption("First-Person 3D Projection") screen = pygame.display.set_mode((width, height)) clock = pygame.time.Clock() fps = 60 # 相机状态:3D位置、朝向角度 camera_pos = [0.0, 0.0, 5.0] # 初始在Z轴正方向,距离物体5单位 yaw = 0.0 # Y轴旋转(左右转向) pitch = 0.0 # X轴旋转(上下抬头) roll = 0.0 # Z轴旋转(倾斜,Minecraft中一般不用) scale = 600 move_speed = 0.1 # 相机移动速度 rotate_speed = 0.02 # 相机旋转速度 # 立方体顶点(世界坐标) points = [ [[-1], [-1], [1]], [[1], [-1], [1]], [[1], [1], [1]], [[-1], [1], [1]], [[-1], [-1], [-1]], [[1], [-1], [-1]], [[1], [1], [-1]], [[-1], [1], [-1]] ] def connect_point(i, j, k): a = k[i] b = k[j] pygame.draw.line(screen, black, (a[0], a[1]), (b[0], b[1]), 2) run = True while run: clock.tick(fps) screen.fill(white) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False keys = pygame.key.get_pressed() # 相机旋转控制,限制俯仰角避免翻转 if keys[pygame.K_LEFT]: yaw += rotate_speed if keys[pygame.K_RIGHT]: yaw -= rotate_speed if keys[pygame.K_UP]: pitch = max(-math.pi/2, pitch - rotate_speed) if keys[pygame.K_DOWN]: pitch = min(math.pi/2, pitch + rotate_speed) if keys[pygame.K_r]: yaw = pitch = roll = 0.0 camera_pos = [0.0, 0.0, 5.0] # 计算相机局部方向向量 forward_x = -math.sin(yaw) * math.cos(pitch) forward_y = math.sin(pitch) forward_z = -math.cos(yaw) * math.cos(pitch) right_x = math.cos(yaw) right_z = -math.sin(yaw) # 相机移动控制(WASD+空格+左Shift) if keys[pygame.K_w]: camera_pos[0] += forward_x * move_speed camera_pos[1] += forward_y * move_speed camera_pos[2] += forward_z * move_speed if keys[pygame.K_s]: camera_pos[0] -= forward_x * move_speed camera_pos[1] -= forward_y * move_speed camera_pos[2] -= forward_z * move_speed if keys[pygame.K_a]: camera_pos[0] -= right_x * move_speed camera_pos[2] -= right_z * move_speed if keys[pygame.K_d]: camera_pos[0] += right_x * move_speed camera_pos[2] += right_z * move_speed if keys[pygame.K_SPACE]: camera_pos[1] += move_speed if keys[pygame.K_LSHIFT]: camera_pos[1] -= move_speed # 构建反向旋转矩阵(模拟相机朝向) rotation_x = [ [1, 0, 0], [0, math.cos(-pitch), -math.sin(-pitch)], [0, math.sin(-pitch), math.cos(-pitch)] ] rotation_y = [ [math.cos(-yaw), 0, -math.sin(-yaw)], [0, 1, 0], [math.sin(-yaw), 0, math.cos(-yaw)] ] rotation_z = [ [math.cos(-roll), -math.sin(-roll), 0], [math.sin(-roll), math.cos(-roll), 0], [0, 0, 1] ] projected_points = [] for point in points: # 1. 反向平移:转换为相机空间坐标 translated_point = [ [point[0][0] - camera_pos[0]], [point[1][0] - camera_pos[1]], [point[2][0] - camera_pos[2]] ] # 2. 反向旋转:模拟相机视角 rotated_point = matrix_multiplication(rotation_y, translated_point) rotated_point = matrix_multiplication(rotation_x, rotated_point) rotated_point = matrix_multiplication(rotation_z, rotated_point) # 3. 透视投影,过滤相机后方的点 z = rotated_point[2][0] if z <= 0: projected_points.append([width//2, height//2]) continue projection_scale = 1 / z projection_matrix = [ [projection_scale, 0, 0], [0, projection_scale, 0] ] projected_2d = matrix_multiplication(projection_matrix, rotated_point) # 转换为屏幕坐标 x = int(projected_2d[0][0] * scale) + width//2 y = int(projected_2d[1][0] * scale) + height//2 projected_points.append([x, y]) pygame.draw.circle(screen, blue, (x, y), 10) # 绘制立方体边 for m in range(4): connect_point(m, (m+1)%4, projected_points) connect_point(m+4, (m+1)%4 + 4, projected_points) connect_point(m, m+4, projected_points) pygame.display.update() pygame.quit()
内容的提问来源于stack exchange,提问作者FlashStopFall
相关产品推荐
相关产品推荐

