三维引擎 三维引擎 数学 自己动手写三维引擎(一) 钦烨AdmiBrill 2024-11-24 2024-11-24 前言 最近比较忙,所以很久没更新。
之前玩过很多三维沙盒游戏,但是很好奇是如何实现那种三维投射的
于是最近写出来了一个三维引擎
过程 我还是用的qt(自从用qt就发现爱上qt了)去实现打开窗口、画线等功能,但是qt自带的三维支持我不用。
准备工作 导入库、创建qt应用、创建空间对象,都不多说。
1 2 3 4 5 6 7 8 9 10 from PySide6.QtWidgets import QWidget,QApplicationfrom PySide6.QtGui import QPainter,QColor,QPen,QCursorfrom PySide6.QtCore import QTimer,Qtimport mathclass Space (): def __init__ (self ): self .objects=[] qapp=QApplication([]) def run (): qapp.exec ()
点对象和相机对象 组成图形的基本元素是点。为了方便描述三维空间坐标,我创建了一个点对象。 同时,需要一个相机,点会投射到相机上
1 2 3 4 5 class Point (): def __init__ (self,x,y,z ): self .x=x self .y=y self .z=z
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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()
投射 这是整个三维投影中最关键的一点。
可见,$\triangle OAE \sim \triangle OFD$, $OA$是相机深度,D是我们要投射的点 ,O是相机的位置,求出BE就可以求出E点的位置,即显示在屏幕上的位置。
从上面看(x坐标)和从右边看(y坐标)这张图都适用。
但是要考虑相机会旋转角度的问题。不妨将相机到投射的点看作一个向量。
假设相机位置为$(a{x},a {y},a{z})$,投射的点的位置为$(c {x},c{y},c {z})$,相机长$w$,宽$h$,深$d$
先计算向量旋转后的新向量:
利用相似三角形对应边成比例,得:
因此我们有了代码:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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
线段 连接两个点的是线段。将一个线段投影之后线段还是直的,所以只需连接两个投影后的端点即可得到投影后的线段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 else : return -1 ,-1 ,-1 ,-1
相机的控制 监听键盘 用WSAD键移动,反引号键(Esc下面那个)呼出呼入鼠标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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()
,重写这个函数就可以实现重复执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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) 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 ) QCursor.setPos(self .geometry().center())
多面体 将线段相接,得到多面体。
1 2 3 4 5 6 7 8 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 )
矩体 矩体是一种特殊的多面体。
1 2 3 4 5 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 ]])
平面文字 没有什么特别的,但是设置了一个更新时会执行的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
。
测试代码 1 2 3 4 5 6 7 8 9 10 11 import threedengineimport mathspace=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()
效果
到这里这篇文章就结束了,886!