为什么这个 3D 形状在播放动画时会变形?

Why is this 3D shape deforming while playing the animation?

提问人:Merlon 提问时间:9/14/2023 更新时间:9/18/2023 访问量:37

问:

所以我有这个 python 代码,它工作正常,直到我尝试将一些透视带入 3D 形状,因为 3D 形状一直在变形。我无法弄清楚这个问题是数学问题还是基于代码的问题,请有人我需要一双新的眼睛来帮助我。代码如下:

import pygame
import math
import time

pygame.init()

white = (255,255,255)
black = (0,0,0)

screen = pygame.display.set_mode((800, 800))
triangle = [[90, 90, 0, 1], [180, 90, 0, 1], [180, 270, 0, 1], [120, 120, 80, 1]]
edgeTable = [[0, 1], [1, 2], [2, 0], [0, 3], [2, 3], [1, 3]]

Matrix = [[1, 0, 0, 0] , [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

def Multiply(Matr, Vec):
    res = [0, 0, 0, 0]
    for i in range(4):
        for j in range(4):
            res[i]=res[i]+Matr[j][i]*Vec[j]
    return res

def MatMultiply(M1, M2):
    res = [0, 0, 0, 0]
    for i in range(4):
        res[i] = Multiply(M1, M2[i])
    return res

def perspectiveMat():
    d = 1000
    PerspectiveMatRes = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1/d], [0, 0, 0, 0]]
    return PerspectiveMatRes

def RotZMat(angle):
    return [[math.cos(angle), math.sin(angle), 0, 0] , [-math.sin(angle), math.cos(angle), 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

def RotXMat(angle):
    return [[1, 0, 0, 0], [0, math.cos(angle), math.sin(angle), 0] , [0, -math.sin(angle), math.cos(angle), 0], [0, 0, 0, 1]]

def RotYMat(angle):
    return [[math.cos(angle), 0, math.sin(angle), 0],  [0, 1, 0, 0], [-math.sin(angle), 0, math.cos(angle), 0], [0, 0, 0, 1]]

def CopyMat(Mat):
    res = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
    for i in range(4):
        for j in range(4):
            res[i][j]=Mat[i][j]
    return res

def CopyVertices(Vert):
    res = []
    for i in range(len(Vert)):
        res[i]=[]
        for j in range(4):
            res[i][j]=Vert[i][j]
    return res

#angle = input(str("angle for x>> "))
angle = "1"
angle = int(angle)*math.pi/180

run = True
while run == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    
    rotM = RotXMat(angle)
    resMat = MatMultiply(perspectiveMat(), rotM)
    cloud = triangle.copy()
    for i in range(len(triangle)):
        cloud[i] = Multiply(resMat, triangle[i])

    screen.fill(white)
    for edge in edgeTable:
        pygame.draw.line(screen, black, (cloud[edge[0]][0]*cloud[edge[0]][3]+400, cloud[edge[0]][1]*cloud[edge[0]][3]+400),(cloud[edge[1]][0]*cloud[edge[1]][3]+400,cloud[edge[1]][1]*cloud[edge[1]][3]+400))
    angle = angle + math.pi/180
    time.sleep(0.04)
    pygame.display.flip()

pygame.quit()

`

我无法弄清楚,但我认为这是一个数学问题。

Python 数学 矩阵

评论


答:

1赞 chrslg 9/14/2023 #1

我可以在您的代码中看到至少两个数学问题。加上一个编码注释(不一定是问题)。我从这个开始,因为有必要确保说同一种语言

  1. 与通常的矩阵逻辑相比,您的矩阵逻辑似乎被移置了(至少在大多数库中很常见):您将矩阵视为列数组。从数学上讲,这是有道理的。正如我们在您自己的矩阵乘法代码中看到的那样:如果矩阵只是一个向量/列的数组,那么矩阵乘以矩阵就是数组(作为矩阵乘法运算符)。这就是您在代码中执行的操作。A=[Col1, Col2, Col3, Col4]B=[Col1', Col2', Col3', Col4'][A@Col1', A@Col2', A@Col3', A@Col4']@MatMultiply

它还显示在您的代码中:矩阵 (Mij) 乘以向量 (Vi) 确实是第 i 个元素为 Σj MijVj 的向量。这就是你编码的,......假设这是数学上记下的 Mij,那就是第 i 行第 j 列的元素。MultiplyMatr[j][i]

通常,尤其是在 numpy(和 python 操作)中,我们将矩阵 (Mij) 表示为数组或 ,作为第 i 行和第 j 列的元素。换句话说,矩阵是行的数组,而不是列的数组。所以从你的表示中移置。同样,这只是一个实现选择,但由于您的选择不寻常,我需要确保我们没问题@Matr[i,j]Matr[i][j]

现在两个问题

  1. 因此,假设您的透视矩阵本来是
/ 1    0    0     0   \
| 0    1    0     0   | 
| 0    0    1     0   |
\ 0    0    1/d   0   /

问题是右下角的 0。应为 1。或者至少是非零的东西。

  1. 显然,这是齐次坐标。习惯是将向量解释为 . 否则,向量和矩阵都等价于比例因子。所以和 一样。一旦我们有一个 1 作为最后一个坐标,我们就可以忘记它来绘制。(x,y,z,a)(x/a, y/a, z/a)(x,y,z,a)(x/a, y/a, z/a, 1)

您的代码另有说明。您的代码说 ,并且在 xy 平面上投影后,(x,y,z,a) ~ (x*a, y*a, z*a)~ (x*a, y*a)

这不是逻辑。同样,齐次坐标只是处理透视和投影运算符的一种方式。但这是有道理的。这样就没有了。这意味着物体离得越近,(z 越小)它们看起来越小。通常,我们认为透视是缩小更多物体距离的东西。*a

逻辑是决定眼睛在场景中(在轴上,因为你使用轴 x 和 y 来绘制)在远处。所以。或者(从另一边观看,这更符合您现有的代码) 因此,在你的眼睛上投射,将图像中事物的大小缩放成一个因子,该因子与眼睛和这些物体之间的距离成反比。如果眼睛在远处,则物体在远处(沿 z 轴,您将沿着该轴投影)。zdz=dz=-dz=-dz=zₒzₒ+d

因此,您要在 2D 屏幕上绘制的是 2D 坐标处的东西 (x / (zₒ+d), y / (zₒ+d)) (如果不服气,只需在纸上举个例子,然后用泰勒斯进行推理,不需要齐次坐标:这确实是视网膜上的坐标)

齐次坐标只是仅使用线性运算编写这种非线性运算(在经典笛卡尔坐标中,从 3D 点 (x,y,z) 到 2D 点 (x/(z+d), y/(z+d)) 的变换不是线性变换)的一种方式。 因为,在齐次坐标 (x/(z+d), y/(z+d), 0, 1) 中,与(x, y, 0, z+d)

所以,我去掉了非线性除法。它仍然是明显的仿射,而不是线性的,因为 .但是在齐坐标中,由于额外的,仿射运算也只是线性运算。+d1

从向量,可以通过矩阵×向量乘法得到(x,y,z,1)(x,y,0,z+d)

/   1   0   0   0   \   / x \
|   0   1   0   0   |   | y |
|   0   0   0   0   |   | z |
\   0   0   1   d   /   \ 1 /

或者,为了更接近你的矩阵(我总是尽量接近初始代码),因为毕竟,你只是忽略了之后的第三个组件,(你从不使用 ),如果我们得到 (x,y,z,z+d) 而不是 (x,y,0,z+d) 并不重要(它只是意味着我们首先执行“透视”, 然后是投影),这就是乘法会得到的结果cloud[...][2]

/  1   0   0   0  \  / x \
|  0   1   0   0  |  | y |
|  0   0   1   0  |  | z |
\  0   0   1   d  /  \ 1 /

使用此矩阵,将 (x,y,z,1) 转换为 (X,Y,Z,A)=(x,y,z,x+d)。如果你正确的话,这就是你想做的,然后画到2d图像平面上,在坐标上,而不是——见第一个问题(X/A, Y/A)(X*A, Y*A)

还有一个问题:正如摄影师可以决定的那样,在拍摄物体照片时,从近距离或更远的距离拍照,但是使用变焦时,您需要校正图像的全局变焦以考虑距离。否则,参数越大,整个图形就越小。我猜你想要的是控制带有参数的透视(与更近的对象相比,进一步的对象是如何缩小的),而不是全局大小。dd

将所有内容缩放系数 d 的一种方法

那就是画画(d×x/(z+d), d×y(z+d))

您可以通过将 x 和 y(以及无动于衷的 z)复用 来获得,从而将之前的矩阵替换为d

/  d   0   0   0  \
|  0   d   0   0  |
|  0   0   d   0  |
\  0   0   1   d  /

但是,由于在齐次坐标下,矩阵和向量在比例因子之后是等价的,因此与矩阵相同

/  1   0   0   0 \
|  0   1   0   0 |
|  0   0   1   0 |
\  0   0   1/d 1 /

这几乎是你的初始矩阵。

因此,对代码进行最小的更正:

  • 添加 1 作为透视矩阵的右下角元素
  • 将绘图代码

    替换为
    pygame.draw.line(screen, black, (cloud[edge[0]][0]*cloud[edge[0]][3]+400, cloud[edge[0]][1]*cloud[edge[0]][3]+400),(cloud[edge[1]][0]*cloud[edge[1]][3]+400,cloud[edge[1]][1]*cloud[edge[1]][3]+400)
    pygame.draw.line(screen, black, (cloud[edge[0]][0]/cloud[edge[0]][3]+400, cloud[edge[0]][1]/cloud[edge[0]][3]+400),(cloud[edge[1]][0]/cloud[edge[1]][3]+400,cloud[edge[1]][1]/cloud[edge[1]][3]+400)

通过这两项修改,您将获得一个代码,该代码模拟了使用相机从远处拍摄图像时会得到什么,并具有补偿性变焦dd

编辑

这不是要求的,但我无法抗拒用numpy重写的冲动

import pygame
import math
import time
import numpy as np

pygame.init()

white = (255,255,255)
black = (0,0,0)

screen = pygame.display.set_mode((800, 800))
triangle = np.array([[90.0, 90, 0, 1], [180, 90, 0, 1], [180, 270, 0, 1], [120, 120, 80, 1]]).T
edgeTable = [[0, 1], [1, 2], [2, 0], [0, 3], [2, 3], [1, 3]]

def perspectiveMat(d):
    return np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 1/d, 1]])

def RotZMat(angle):
    return np.array([[np.cos(angle), np.sin(angle), 0, 0] , [-np.sin(angle), np.cos(angle), 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])

def RotXMat(angle):
    return np.array([[1, 0, 0, 0], [0, np.cos(angle), np.sin(angle), 0] , [0, -np.sin(angle), np.cos(angle), 0], [0, 0, 0, 1]])

def RotYMat(angle):
    return np.array([[np.cos(angle), 0, np.sin(angle), 0],  [0, 1, 0, 0], [-np.sin(angle), 0, np.cos(angle), 0], [0, 0, 0, 1]])

angle = np.pi/180

run = True
while run == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    
    rotM = RotXMat(angle)
    resMat = perspectiveMat(1000) @ rotM
    cloud = resMat @ triangle

    screen.fill(white)
    for edge in edgeTable:
        pygame.draw.line(screen, black, cloud[:2,edge[0]]/cloud[3,edge[0]]+[400,400], cloud[:2,edge[1]]/cloud[3,edge[1]]+[400,400]) 
    angle = angle + np.pi/180
    time.sleep(0.04)
    pygame.display.flip()

pygame.quit()

您可以看到,使用 numpy,我可以删除两个多重(矩阵 × 向量和矩阵 × 矩阵),因为它们只是由 获得的。 也不需要复制函数(我们可以用方法获得副本,但在这里,我们并不真正需要它,因为矩阵积无论如何都会创建一个新结果)。@.copy()

我也删除了你的身份,因为你不使用它。但如果我想保留它,那只是.MatrixMatrix=np.identity(4)

最后,还要简化要绘制的坐标的计算:例如

表示 edge[0]cloudedge[0]cloud[400,400]'。
cloud[:2,edge[0]]/cloud[3,edge[0]]+[400,400]2 first rows of column of, divided by last row of column of. Which is a pair (since I do the same operation on the 2 first rows). To which I add the pair