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

如何基于矩阵手动实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 06:12:03