自己动手写三维引擎(一)

前言

最近比较忙,所以很久没更新。

之前玩过很多三维沙盒游戏,但是很好奇是如何实现那种三维投射的

于是最近写出来了一个三维引擎

过程

我还是用的qt(自从用qt就发现爱上qt了)去实现打开窗口、画线等功能,但是qt自带的三维支持我不用。

准备工作

导入库、创建qt应用、创建空间对象,都不多说。

from PySide6.QtWidgets import QWidget,QApplication
from PySide6.QtGui import QPainter,QColor,QPen,QCursor
from PySide6.QtCore import QTimer,Qt
import math
class Space():
    def __init__(self):
        self.objects=[]
qapp=QApplication([])
def run():
    qapp.exec()

点对象和相机对象

组成图形的基本元素是点。为了方便描述三维空间坐标,我创建了一个点对象。
同时,需要一个相机,点会投射到相机上

class Point():
    def __init__(self,x,y,z):
        self.x=x
        self.y=y
        self.z=z
class Camera(QWidget):
    def __init__(self,space,wwidth,wheight,caption,cwidth,cheight,cdeepth):
        super().__init__()
        self.resize(wwidth,wheight)#窗口宽高
        self.setWindowTitle(caption)#窗口标题
        self.space=space #相机所在的空间
        self.movement=[0,0,0] #相机试试移动方向
        self.rotatex=0
        self.rotatey=90
        self.rotatez=0 #相机旋转角度
        self.posx=0
        self.posy=0 
        self.posz=0 #相机的位置
        self.controled=True #鼠标是否控制
        self.movespeed=1 #移速
        self.setCursor(Qt.CursorShape.BlankCursor)
        self.objects2D=[]
        self.cwidth=cwidth   
        self.cheight=cheight  #相机宽高
        self.cdeepth=cdeepth   #相机视锥深度
    def launch(self):
        self.timer=QTimer()
        self.timer.start(5)
        self.timer.timeout.connect(self.update)
        self.show()

投射

这是整个三维投影中最关键的一点。

可见, △ O A E ∼ △ O F D \triangle OAE \sim \triangle OFD OAEOFD, O A OA OA是相机深度,D是我们要投射的点 ,O是相机的位置,求出BE就可以求出E点的位置,即显示在屏幕上的位置。

从上面看(x坐标)和从右边看(y坐标)这张图都适用。

但是要考虑相机会旋转角度的问题。不妨将相机到投射的点看作一个向量。

假设相机位置为 ( a x , a y , a z ) (a_{x},a_{y},a_{z}) (ax,ay,az),投射的点的位置为 ( c x , c y , c z ) (c_{x},c_{y},c_{z}) (cx,cy,cz),相机长 w w w,宽 h h h,深 d d d

先计算向量旋转后的新向量:
$$
\begin{bmatrix}
d_{x} \
d_{y} \
d_{z}
\end{bmatrix}

\begin{bmatrix}
1 & 0 & 0 \
0 & \cos (\theta_{x}) &\sin(\theta_{x}) \
0 & -\sin(\theta_{x})& \cos(\theta_{x})
\end{bmatrix}
\begin{bmatrix}
\cos(\theta_{y}) &0 & -\sin(\theta_{y}) \
0 & 1& 0 \
\sin(\theta_{y})& 0 & \cos(\theta_{y})
\end{bmatrix}
\begin{bmatrix}
\cos(\theta_{z})&\sin(\theta_{z})& 0 \
-\sin(\theta_{z})&\cos(\theta_{z})&0 \
0&0&1
\end{bmatrix}
\begin{bmatrix}
a_{x}-c_{x} \
a_{y}-c_{y} \
a_{z}-c_{z}
\end{bmatrix}
利用相似三角形对应边成比例,得: 利用相似三角形对应边成比例,得: 利用相似三角形对应边成比例,得:
x=\frac{w}{2}-\frac{d}{d_{z}}d_{x}

y=\frac{h}{2}-\frac{d}{d_{z}}d_{y}
$$
因此我们有了代码:

# class Point():
	def project(self,camera):
        cx, cy, cz = camera.posx, camera.posy, camera.posz
        thx, thy, thz = -camera.rotatex, camera.rotatey, camera.rotatez
        x = self.x - cx
        y = self.y - cy
        z = self.z - cz
        dx = math.cos(thy)*(math.sin(thz)*y+math.cos(thz)*x)-math.sin(thy)*z
        dy = math.sin(thx)*(math.cos(thy)*z+math.sin(thy)*(math.sin(thz)*y+math.cos(thz)*x))+\
            math.cos(thx)*(math.cos(thz)*y-math.sin(thz)*x)
        dz = math.cos(thx)*(math.cos(thy)*z+math.sin(thy)*(math.sin(thz)*y+math.cos(thz)*x))-\
            math.sin(thx)*(math.cos(thz)*y-math.sin(thz)*x)
        newx= camera.cwidth/2-(camera.cdeepth/dz) * dx
        newy= camera.cheight/2-(camera.cdeepth/dz) * dy 
        if camera.width()/camera.height()>camera.cwidth/camera.cheight:
            rectw=camera.height()/camera.cheight*camera.cwidth
            resultx=camera.width()/2-rectw/2+newx/camera.cwidth*rectw
            resulty=newy/camera.cheight*camera.height()
        else:
            recth=camera.width()/camera.cwidth*camera.cheight
            resultx=newx/camera.cwidth*camera.width()
            resulty=camera.height()/2-recth/2+newy/camera.cheight*recth
        return resultx,resulty,dz>0#当dz<=0时,即投影的点在相机的后面时,投射的位置不正确,需要特判处理

线段

连接两个点的是线段。将一个线段投影之后线段还是直的,所以只需连接两个投影后的端点即可得到投影后的线段。

class Segment():
    def __init__(self,point1,point2):
        self.point1=point1
        self.point2=point2
    def paint(self,camera):
        x1,y1,m1=self.point1.project(camera)
        x2,y2,m2=self.point2.project(camera)
        if m1>0 and m2>0:
            return x1,y1,x2,y2
        elif m1>0:
            return x1,y1,2*x1-x2,2*y1-y2
        elif m2>0:
            return 2*x2-x1,2*y2-y1,x2,y2  #这两都是将投影错误的端点绕着正确的端点旋转180度,即中心对称
        else:
            return -1,-1,-1,-1 #当两个端点都在后面,说明这条线段在摄像机后面,因此不用显示

相机的控制

监听键盘

用WSAD键移动,反引号键(Esc下面那个)呼出呼入鼠标。

# class Camera(QWidget):
    def keyPressEvent(self, event):
        if event.key()==Qt.Key.Key_W:
            self.movement[0]=1
        if event.key()==Qt.Key.Key_S:
            self.movement[0]=-1
        if event.key()==Qt.Key.Key_A:
            self.movement[2]=1
        if event.key()==Qt.Key.Key_D:
            self.movement[2]=-1
        if event.key()==Qt.Key.Key_Space:
            self.movement[1]=1
        if event.key()==Qt.Key.Key_Shift:
            self.movement[1]=-1
        if event.key()==Qt.Key.Key_QuoteLeft:
            if self.controled:
                self.setCursor(Qt.CursorShape.ArrowCursor)
            else:
                self.setCursor(Qt.CursorShape.BlankCursor)
            self.controled=not self.controled
    def keyReleaseEvent(self,event):
        self.movement=[0,0,0]
进行移动和旋转操作

刚刚已经改变了self.movement的值,现在只需要根据值来移动相机,如果鼠标已锁住,同时用鼠标到窗口正中心的偏移来旋转相机,然后把鼠标的位置设置到画面正中心。

QWidget.update()被执行时,就会执行paintEvent(),重写这个函数就可以实现重复执行。

# class Camera(QWidget):
    def paintEvent(self,event):
        self.posx+= self.movement[0]*math.sin(self.rotatey)*self.movespeed*0.01
        self.posx+= self.movement[2]*math.sin(self.rotatey+math.radians(90))*self.movespeed*0.01
        self.posy+= self.movement[1]*self.movespeed*0.01
        self.posz+= self.movement[0]*math.cos(self.rotatey)*self.movespeed*0.01
        self.posz+= self.movement[2]*math.cos(self.rotatey+math.radians(90))*self.movespeed*0.01
        if self.controled:
            centerx=self.geometry().center().x()
            centery=self.geometry().center().y()
            mousex=QCursor.pos().x()
            mousey=QCursor.pos().y()
            offsetx=centerx-mousex
            offsety=centery-mousey
            self.rotatex+=offsety*0.001 
            self.rotatex=min(math.radians(90),self.rotatex)
            self.rotatex=max(math.radians(-90),self.rotatex)#绕x轴旋转的极限
            self.rotatey+=offsetx*0.001
            if self.rotatey>math.radians(180):
                self.rotatey-=math.radians(360)
            if self.rotatey<math.radians(-180):
                self.rotatey+=math.radians(360) #绕y轴旋转处理
            QCursor.setPos(self.geometry().center())

多面体

将线段相接,得到多面体。

class Body():
    def __init__(self,space,vertixes,edges):
        self.vertixes=vertixes
        self.edges=[]
        for i in edges:
            self.edges.append(Segment(Point(*(vertixes[i[0]])),Point(*(vertixes[i[1]]))))
        self.space=space
        self.space.objects.append(self)

矩体

矩体是一种特殊的多面体。

class Cubold(Body):
    def __init__(self,space,x,y,z,length,width,height):
        super().__init__(space,[[x,y,z],[x+length,y,z],[x,y,z+width],[x+length,y,z+height],
                    [x,y+height,z],[x+length,y+height,z],[x,y+height,z+width],[x+length,y+height,z+length]],#描述顶点坐标
                    [[0,1],[1,3],[2,0],[3,2],[4,5],[5,7],[6,4],[7,6],[0,4],[1,5],[2,6],[3,7]])#连棱

平面文字

没有什么特别的,但是设置了一个更新时会执行的函数

class Text2D():
    def __init__(self,camera,x,y,content) :
        self.x=x
        self.y=y
        self.camera=camera
        self.content=content
        self.camera.objects2D.append(self)
        def update(camera):
            pass
        self.updatefunc=update
    def setText(self,content):
        self.content=content
    def setUpdate(self,func):
        self.updatefunc=func

更新相机画面

把当前相机所在空间中的每一个多面体的每一条棱画出来,同时也也要写文字,执行文字更新是的那个函数

使用QPainter

# class Camera(QWidget):
#   def paintEvent(self,event):
		painter = QPainter(self)
        color=QColor()
        color.setRgb(0,0,255)
        pen=QPen()
        pen.setColor(color)
        painter.setPen(pen)
        if self.controled:
            painter.drawLine(self.width()/2-2,self.height()/2,self.width()/2+2,self.height()/2)
            painter.drawLine(self.width()/2,self.height()/2-2,self.width()/2,self.height()/2+2)
        for i in self.space.objects:
            for j in i.edges:
                try:
                    x1,y1,x2,y2=j.paint(self)
                    painter.drawLine(x1,y1,x2,y2)
                except:
                    pass
        for i in self.objects2D:
            if isinstance(i,Text2D):
                painter.drawText(i.x,i.y,i.content)
            i.updatefunc(self)

测试

我把这个文件叫做threedengine.py。同时建了一个测试文件demo.py

测试代码

import threedengine
import math
space=threedengine.Space()
camera=threedengine.Camera(space,1000,500,'三维视图',800,400,300)
camera.launch()
body=threedengine.Cubold(space,1,0,0,1,1,1)
text=threedengine.Text2D(camera,10,10,'')
def update(camera):
    text.setText('位置:(%f,%f,%f) 方向:%f° 俯仰:%f°'%(camera.posx,camera.posy,camera.posz,math.degrees(camera.rotatey),math.degrees(camera.rotatex)))
text.setUpdate(update)
threedengine.run()

效果

image

到这里这篇文章就结束了,886!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值