提问人:Merlon 提问时间:9/14/2023 更新时间:9/18/2023 访问量:37
为什么这个 3D 形状在播放动画时会变形?
Why is this 3D shape deforming while playing the animation?
问:
所以我有这个 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()
`
我无法弄清楚,但我认为这是一个数学问题。
答:
我可以在您的代码中看到至少两个数学问题。加上一个编码注释(不一定是问题)。我从这个开始,因为有必要确保说同一种语言
- 与通常的矩阵逻辑相比,您的矩阵逻辑似乎被移置了(至少在大多数库中很常见):您将矩阵视为列数组。从数学上讲,这是有道理的。正如我们在您自己的矩阵乘法代码中看到的那样:如果矩阵只是一个向量/列的数组,那么矩阵乘以矩阵就是数组(作为矩阵乘法运算符)。这就是您在代码中执行的操作。
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 列的元素。Multiply
Matr[j][i]
通常,尤其是在 numpy(和 python 操作)中,我们将矩阵 (Mij) 表示为数组或 ,作为第 i 行和第 j 列的元素。换句话说,矩阵是行的数组,而不是列的数组。所以从你的表示中移置。同样,这只是一个实现选择,但由于您的选择不寻常,我需要确保我们没问题@
Matr[i,j]
Matr[i][j]
现在两个问题
- 因此,假设您的透视矩阵本来是
/ 1 0 0 0 \
| 0 1 0 0 |
| 0 0 1 0 |
\ 0 0 1/d 0 /
问题是右下角的 0。应为 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 轴,您将沿着该轴投影)。z
d
z=d
z=-d
z=-d
z=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)
所以,我去掉了非线性除法。它仍然是明显的仿射,而不是线性的,因为 .但是在齐坐标中,由于额外的,仿射运算也只是线性运算。+d
1
从向量,可以通过矩阵×向量乘法得到(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)
还有一个问题:正如摄影师可以决定的那样,在拍摄物体照片时,从近距离或更远的距离拍照,但是使用变焦时,您需要校正图像的全局变焦以考虑距离。否则,参数越大,整个图形就越小。我猜你想要的是控制带有参数的透视(与更近的对象相比,进一步的对象是如何缩小的),而不是全局大小。d
d
将所有内容缩放系数 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)
通过这两项修改,您将获得一个代码,该代码模拟了使用相机从远处拍摄图像时会得到什么,并具有补偿性变焦d
d
编辑
这不是要求的,但我无法抗拒用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()
我也删除了你的身份,因为你不使用它。但如果我想保留它,那只是.Matrix
Matrix=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
评论