<电子幽灵>Pygame实现坐标变换

Pygame实现坐标变换矩阵效果

介绍

费曼学习法最重要的部分,即把知识教给一个完全不懂的孩子——或者小白。
为了更好的自我学习,也为了让第一次接触某个知识范畴的同学快速入门,我会把我的学习笔记整理成电子幽灵系列。
提示:文章的是以解释-代码块-解释的结构呈现的。当你看到代码块并准备复制复现的时候,最好先保证自己看过了代码块前后的解释。

Pygame实现坐标变换矩阵效果介绍

在之前已经学习过Pygame的基础操作,包括创建界面、基础控制、图形绘制等等。现在用Pygame的上述基础操作,手动完成 T 44 T_{44} T44矩阵算法,做一个进行实时点渲染的小程序。
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版本中,将会实现从世界坐标系到镜头坐标系的转换:
思路:
每次进行渲染时:

  1. 算出以视角中心为原点,正方形各个点坐标的相对位置
  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

没事出去跑跑步真的有好处,呼吸都能顺畅不少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

靈镌sama

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值