Pygame实现坐标变换矩阵效果
介绍
费曼学习法最重要的部分,即把知识教给一个完全不懂的孩子——或者小白。
为了更好的自我学习,也为了让第一次接触某个知识范畴的同学快速入门,我会把我的学习笔记整理成电子幽灵系列。
提示:文章的是以解释-代码块-解释的结构呈现的。当你看到代码块并准备复制复现的时候,最好先保证自己看过了代码块前后的解释。
Pygame实现坐标变换矩阵效果介绍
在之前已经学习过Pygame的基础操作,包括创建界面、基础控制、图形绘制等等。现在用Pygame的上述基础操作,手动完成
T
44
T_{44}
T44矩阵算法,做一个进行实时点渲染的小程序。
Pygame基础操作: <电子幽灵>Pygame入门笔记
Pygame实现坐标变换矩阵目录
创建界面
import pygame as pg
pg.init()
size = width,length = 1280,720
screen=pg.display.set_mode(size)#当前打开的页面赋值给Screen对象
while pg.get_init():#若成功打开页面
for event in pg.event.get():#遍历当前所有事件
if event.type == pg.QUIT:#若为关闭界面
pg.quit()#退出程序
先实现二维的变换:平移、旋转
Pygame中可以在2D层面上直接根据点绘制图像,所以可以先实现简单的二维。
'''画出正方形'''
import pygame as pg
from pygame import draw
Tangle = [(255,255),(255,400),(400,400),(400,255)]#长方形的点坐标
def drawtangle(Tangle,Screen,Color,width):
'''因为pg自带的画长方形不满足所需的要求,所以重新定义一个函数'''
draw.line(Screen,Color,Tangle[0],Tangle[1],width)
draw.line(Screen,Color,Tangle[0],Tangle[3],width)
draw.line(Screen,Color,Tangle[1],Tangle[2],width)
draw.line(Screen,Color,Tangle[2],Tangle[3],width)
pg.init()
size = length,height = 1280,720
screen = pg.display.set_mode(size)
color = (255,255,255)
width = 1
while pg.get_init():#若成功打开页面
drawtangle(Tangle,screen,color,width)#调用函数
pg.display.flip()#让结果显示在屏幕上
for event in pg.event.get():#遍历当前所有事件
if event.type == pg.QUIT:#若为关闭界面
pg.quit()#退出程序
绘制函数确定可以调用后,就可以进行矩阵函数的构造了。
为了确定能够正常运行,同时指定按键对应的操作,以便于调试。
'''准备工作ver0.1'''
import pygame as pg
import numpy as np
from pygame import key
from pygame import draw
Tangle = [(255,255),(255,400),(400,400),(400,255)]#长方形的点坐标
size = length,height = 1280,720
T33ori = np.array([[1,0,0],[0,1,0],[0,0,1]])#原始矩阵
'''因为坐标变换矩阵是正交矩阵,所以理论上只要求逆就可以是现在相反操作'''
T33right = T33ori + np.array([[0,0,1],[0,0,0],[0,0,0]])
T33left = np.linalg.inv(T33right)
T33up = T33ori + np.array([[0,0,0],[0,0,1],[0,0,0]])
T33down = np.linalg.inv(T33up)
#T33down = T33ori + np.array([[0,0,0],[0,0,-10],[0,0,0]])
T33CW = np.array([
[np.cos(1*np.pi/180),np.sin(1*np.pi/180),0],
[-np.sin(1*np.pi/180),np.cos(1*np.pi/180),0],
[0,0,1]
])#顺时针旋转矩阵
T33CCW = np.linalg.inv(T33CW)#逆时针旋转矩阵
OpKeys = [pg.K_RIGHT,pg.K_LEFT,pg.K_UP,pg.K_DOWN,pg.K_q,pg.K_e]
def MatrixANDKeys(valid_Key):
'''用来定义按哪个键是用哪个矩阵。
由于摄像头应该是“系变点不变”,所以向上是坐标向下变换,以此类推'''
match valid_Key:
case pg.K_RIGHT:
return T33left
case pg.K_LEFT:
return T33right
#这里,由于pygame中的屏幕坐标系是以向下、向右为正方向的,所以加像素值反而是向下走。
case pg.K_UP:
return T33up
case pg.K_DOWN:
return T33down
case pg.K_q:
return T33CCW
case pg.K_e:
return T33CW
case _:
pass
def drawtangle(Tangle,Screen,Color,width):
'''因为pg自带的画长方形不满足所需的要求,所以重新定义一个函数'''
#print(Tangle)
draw.line(Screen,Color,Tangle[0],Tangle[1],width)
draw.line(Screen,Color,Tangle[0],Tangle[3],width)
draw.line(Screen,Color,Tangle[1],Tangle[2],width)
draw.line(Screen,Color,Tangle[2],Tangle[3],width)
def dotmulmat(matrix,dot):
'''用来把点和矩阵相乘进行坐标变换,并把结果应用到点上'''
dot_enlarged = [dot[0],dot[1],1]
dot_multed = np.dot(matrix,dot_enlarged)
return (dot_multed[0],dot_multed[1])
注意:上面关于矩阵的部分偷了懒,但是 此时 对整体性能影响不大。
'''正式测试ver0.1'''
screen = pg.display.set_mode(size)
color = (255,255,255)
pg.init()
clock = pg.time.Clock()
while pg.get_init():
keys = pg.key.get_pressed()
for key in OpKeys:
if keys[key]:
for dot in range(len(Tangle)):
Tangle[dot] = dotmulmat(MatrixANDKeys(key),Tangle[dot])
drawtangle(Tangle,screen,color,2)
pg.display.flip()
screen.fill('black')
clock.tick(60)
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
在上述ver0.1版本中,用T33矩阵实现了二维平面上的平移和旋转;但是存在的问题是,现在的旋转是以左上角(0,0)为圆心,以x、y坐标轴为基准旋转,对我们摄像头视角非常的不友好;我们希望它以我们视线中心为原点,以摄像头坐标轴为基准进行旋转。
因此,在0.2版本中,将会实现从世界坐标系到镜头坐标系的转换:
思路:
每次进行渲染时:
- 算出以视角中心为原点,正方形各个点坐标的相对位置
- 对各个点以视角中心为原点进行旋转变换。这里若每个点顺时针旋转 θ \theta θ度,则代表视角坐标系和世界坐标系相比逆时针旋转 θ \theta θ度。
准备工作ver0.2:
因为之前每次按键只需要移动1个像素/1度角,所以偷了懒,接下来我们要真正的构造更一般化的矩阵。
'''准备工作ver0.2'''
from enum import Enum
import pygame as pg
import numpy as np
size = length,height = 1280,720
Worldplot = (0,0)#世界坐标系原点,这里和屏幕左上角重合
class MyEyes:
'''定义视角的各项初始参数'''
def __init__(self,center=(0,0),angle=(0,0)):
self.center = center
self.angle = angle#视角坐标系初始原点、初始角度
def axis(self):
return [self.center,self.angle]
'''因为世界坐标系(Pygame坐标系)正方向为向右/向下,而我们常见的视角是向右/向上,调转方向又有些麻烦(T^T)
所以坐标系意义上的向下和向上和视角意义上的向下和向上会调转'''
camera = MyEyes((length/2,height/2),(0,0))
T33ori = np.array([[1,0,0],[0,1,0],[0,0,1]])#原始矩阵
Tangle = [(255,255),(255,400),(400,400),(400,255)]#长方形的点坐标
class Matrixs_mods(Enum):
origin = 'ori'#原始矩阵,什么都不做
translation = 'trans'#平移矩阵
rotate = 'rot'#旋转矩阵
def Transmation_Matrix(mods='ori',data=(0,0)):
'''创建一个接口,输入矩阵对应的变换类型和相应数据,输出我们想要的矩阵
这里的矩阵,是“点相对于坐标系的变换”所用的矩阵。'''
match Matrixs_mods(mods):
case Matrixs_mods.origin:
return T33ori
case Matrixs_mods.translation:
return T33ori + np.array([[0,0,data[0]],[0,0,data[1]],[0,0,0]])
case Matrixs_mods.rotate:
return np.array([[np.cos(np.pi*data[0]/180),-np.sin(np.pi*data[0]/180),0],
[np.sin(np.pi*data[0]/180),np.cos(np.pi*data[0]/180),0],
[0,0,1]])
def drawtangle(Tangle,Screen,Color,width):
'''因为pg自带的画长方形不满足所需的要求,所以重新定义一个函数'''
draw.line(Screen,Color,Tangle[0],Tangle[1],width)
draw.line(Screen,Color,Tangle[0],Tangle[3],width)
draw.line(Screen,Color,Tangle[1],Tangle[2],width)
draw.line(Screen,Color,Tangle[2],Tangle[3],width)
def dotmulmat(matrix,dot):
'''用来把点和矩阵相乘进行坐标变换,并把结果应用到点上'''
dot_enlarged = [dot[0],dot[1],1]
dot_multed = np.dot(matrix,dot_enlarged)
return (dot_multed[0],dot_multed[1])
def Tanglebycamera(camera,Tangle):
'''这里,求出长方形中 每 个 点 对 摄 像 机 视角的相对位置,先平移得到视角坐标系原点和每个点的相对位置、再旋转得到每个点在视角的哪个位置。
秉承“系变点不变”的原则,当从世界坐标系变为视角坐标系的时候,点的平移、旋转应该与视角的参数相反,即取负数'''
move_to_camera=((-camera.axis()[0][0],-camera.axis()[0][1]),(-camera.axis()[1][0],-camera.axis()[1][1]))#相对于视角的变换
Tanglebc = []#返回的点的矩阵
for i in range(len(Tangle)):
temp_dot = (Tangle[i][0],Tangle[i][1])
translated_dot = dotmulmat(Transmation_Matrix('trans',move_to_camera[0]),temp_dot)
rotated_dot = dotmulmat(Transmation_Matrix('rot',move_to_camera[1]),translated_dot)
Tanglebc.append(rotated_dot)
#print(Tanglebc[i])
return Tanglebc
#Tangleforcamera = Tanglebycamera(camera,Tangle)
def Tangleinscreen(camera,Tanglebc):
Tangleis = []
for i in range(len(Tanglebc)):
Tangleis.append((Tanglebc[i][0]+length/2,Tanglebc[i][1]+height/2))
return Tangleis
OpKeys = [pg.K_RIGHT,pg.K_LEFT,pg.K_UP,pg.K_DOWN,pg.K_q,pg.K_e]
def switchANDKeys(valid_Key,camera):
'''用来定义按哪个键是 视 角 的如何变化,以及视角坐标系的参数如何变化。
摄像头应该是“系变点不变”,在上一个函数中,我们已经让正方形转换到视角坐标系中了,在这里,我们控制的是摄像头。
这里,为了便于我们观察,我们的视角是以向上/向右为正方向,视角中心的坐标却是以世界坐标系为基准,参数以向下/向右为正方向;
因此需要考虑视角坐标系和世界坐标系的角度问题。
由于pygame中的屏幕坐标系是以向下、向右为正方向的,所以加像素值反而是向下走。
我们所谓的摄像机视角向“上”平移,其实在世界坐标系是向“下”平移;
我们所谓的视角顺时针转动,其实在世界坐标系是向"逆时针"转动。这些问题是坐标系问题导致的。在世界坐标系不同或者可以调整时,应当具体问题具体分析。
'''
ct = list(camera.center)
ca = list(camera.angle)
match valid_Key:
case pg.K_RIGHT:
ct[0] += np.cos(ca[0]*np.pi/180)
ct[1] += np.sin(ca[0]*np.pi/180)
case pg.K_LEFT:
ct[0] -= np.cos(ca[0]*np.pi/180)
ct[1] -= np.sin(ca[0]*np.pi/180)
case pg.K_UP:
ct[0] += np.sin(ca[0]*np.pi/180)
ct[1] -= np.cos(ca[0]*np.pi/180)
case pg.K_DOWN:
ct[0] -= np.sin(ca[0]*np.pi/180)
ct[1] += np.cos(ca[0]*np.pi/180)
case pg.K_q:
ca[0] -= 1
case pg.K_e:
ca[0] += 1
case _:
pass
camera.center = tuple(ct)
camera.angle = tuple(ca)
'''正式测试ver0.2'''
from pygame import draw
pg.init()
screen = pg.display.set_mode(size)
#Clock = pg.time.Clock()
while pg.get_init():
Tangleforcamera = Tangleinscreen(camera,Tanglebycamera(camera,Tangle))#生成摄像机视角下各个点在屏幕上的位置
#print(Tangleforcamera)
keys = pg.key.get_pressed()#追踪按键状态
for key in OpKeys:
if keys[key]:
switchANDKeys(key,camera)#根据按键进行变换
drawtangle(Tangleforcamera,screen,(255,255,255),1)
#print(camera.axis())
pg.display.flip()
screen.fill('black')
#为了便于观察视角坐标变化,可以用每秒刷新一次来查看
#Clock.tick(1)
for event in pg.event.get():
if event.type == pg.QUIT:
pg.quit()
到此为止,已经制作出了一个只能查看二维空间的摄像头,可以根据操作进行多角度的观察。
日志
动笔:2024-7-5
二维部分写完:2024-7-9
没事出去跑跑步真的有好处,呼吸都能顺畅不少。