基于Pygame的Python 3D渲染引擎卡顿问题优化咨询
我最近在学习开发3D渲染引擎,跟着YouTube上的教程用Pygame实现了一个基础版本,代码能正常运行,但当渲染100个以上方块时FPS会降到30左右,方块越多性能越差。想请教各位如何优化代码消除卡顿?
以下是我的代码:
import pygame, sys, math, time #Imports the required Add-Ons needed for the code def rotate2d(pos,rad): x,y=pos; s,c = math.sin(rad),math.cos(rad); return x*c-y*s,y*c+x*s class Cam(): def __init__(self,pos=(0,0,0),rot=(0,0)): self.pos = list(pos) self.rot = list(rot) def events(self,event): if event.type == pygame.MOUSEMOTION: x,y = event.rel x/=200; y/=200 self.rot[0]+=y; self.rot[1]+=x if self.rot[0] > math.pi/2: self.rot[0] = math.pi/2 if self.rot[0] < -math.pi/2: self.rot[0] = -math.pi/2 def update(self,dt,key): s=dt*10 if key[pygame.K_SPACE]: self.pos[1]-=s if key[pygame.K_LSHIFT]: self.pos[1]+=s x,y = s*math.sin(self.rot[1]),s*math.cos(self.rot[1]) if key[pygame.K_w]: self.pos[0]+=x; self.pos[2]+= y if key[pygame.K_s]: self.pos[0]-=x; self.pos[2]-= y if key[pygame.K_a]: self.pos[0]-=y; self.pos[2]+= x if key[pygame.K_d]: self.pos[0]+=y; self.pos[2]-= x pygame.init() #Initialises PyGame pygame.font.init() myfont = pygame.font.SysFont('', 30) w,h = 1440,900 #Width and height of the screen cx,cy = w//2,h//2 #Co-ords of the middle of the screen screen = pygame.display.set_mode((w,h)) screen = pygame.display.set_mode((w,h), pygame.FULLSCREEN) clock = pygame.time.Clock() class Cube: vertices=[(-1,1,-1),(1,1,-1),(1,-1,-1),(-1,-1,-1),(-1,1,1),(1,1,1),(1,-1,1),(-1,-1,1)] edges= [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(3,7),(2,6)] def __init__(self,pos=(0,0,0)): x,y,z = pos self.verts = [(x+X/2,y+Y/2,z+Z/2) for X,Y,Z in self.vertices] cam = Cam((0,0,-5)) cubes_p = [(0,-10,0),(0,-10,1),(1,-10,0),(1,-10,1),(2,0,0),(2,0,1),(3,0,0),(3,0,1),(0,+10,0),(0,+10,1),(1,+10,0),(1,+10,1),(0,-11,-1),(0,-11,0),(1,-11,-1),(1,-11,0),(0,-12,-1),(0,-12,0),(1,-12,-1),(1,-12,0)] cubes = [Cube(cubes_p[i])for i in range(0,len(cubes_p))] pygame.event.get(); pygame.mouse.get_rel() pygame.mouse.set_visible(0); pygame.event.set_grab(1) ctime = time.time()-1 Colour = 0, 0, 0 while True: key = pygame.key.get_pressed() ptime = ctime ctime = time.time() fps = int(1/(ctime-ptime)) #dt = clock.tick()/1000 dt = (ctime-ptime) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() cam.events(event) if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: pygame.quit(); sys.exit() screen.fill((0,0,0)) for obj in cubes: for edge in obj.edges: points = [] disp = True for x,y,z in (obj.verts[edge[0]],obj.verts[edge[1]]): x-=cam.pos[0] y-=cam.pos[1] z-=cam.pos[2] x,z = rotate2d((x,z),cam.rot[1]) y,z = rotate2d((y,z),cam.rot[0]) f = 400/z x,y = x*f, y*f if z <=0: disp = False points+=[(cx+int(x),cy+int(y))] if disp: pygame.draw.line(screen,colour,points[0],points[1],1) pygame.draw.line(screen,(200,200,200),(cx,cy-6),(cx,cy+6),1) pygame.draw.line(screen,(200,200,200),(cx-6,cy),(cx+6,cy),1) textsurface = myfont.render(str(fps)+ ' ' +str(round(cam.pos[0]))+ ',' +str(-(round(cam.pos[1]+2)))+ ',' +str(round(cam.pos[2])), 1, (255,255,255)) wid,hgt = textsurface.get_width(),textsurface.get_height() screen.blit(textsurface,(w-wid,0)) pressed1, pressed2, pressed3 = pygame.mouse.get_pressed() #P1 = LC, P2 = MC, P3 = RC if pressed1: if (round(cam.pos[0]),round(cam.pos[1]+2),round(cam.pos[2])) in cubes_p: del cubes_p[len(cubes_p)-1] cubes_p.append((round(cam.pos[0]),round(cam.pos[1]+2),round(cam.pos[2]))) if key[pygame.K_RETURN]: cubes_p = []; cubes_p.append((0,0,0)) cubes = [Cube(cubes_p[i])for i in range(0,len(cubes_p))] print(len(cubes)) if (len(cubes_p)) > 200: del cubes_p[0] pygame.display.flip() key = pygame.key.get_pressed() cam.update(dt,key) #clock.tick(60)
嘿,我看了你的代码,卡顿的核心原因是每帧都在做大量重复计算,而且Pygame的draw.line是逐像素绘制,调用次数太多就会拖慢速度。下面是几个立竿见影的优化方案,按优先级排序:
1. 提前剔除不可见的方块(视锥体剔除)
你现在是先计算每个方块的所有边,再判断是否可见,这完全是浪费。应该先判断整个方块是否在相机的视野范围内,不在的话直接跳过,不用计算任何边。
实现思路:
给Cube类加一个方法,判断方块的包围球(或轴对齐包围盒)是否在相机的视锥体内。简单点的话,可以计算方块中心到相机的Z距离,或者判断方块的所有顶点中是否有至少一个在相机前方(z>0),如果整个方块都在相机后面,直接跳过。
修改代码:
在Cube类的__init__里计算中心坐标:
def __init__(self,pos=(0,0,0)): x,y,z = pos self.verts = [(x+X/2,y+Y/2,z+Z/2) for X,Y,Z in self.vertices] self.center = (x, y, z) # 新增:存储方块中心
然后在渲染循环里,先判断方块是否在相机前方:
for obj in cubes: # 先判断方块是否在相机前方(简化版视锥体剔除) cube_z = obj.center[2] - cam.pos[2] # 方块最远点的z坐标:中心z + 半边长(你的方块半边长是0.5) if cube_z + 0.5 <= 0: continue # 整个方块在相机后面,直接跳过 # 原来的边渲染代码...
2. 使用Pygame的draw.lines批量绘制,减少渲染调用
你现在每个边都调用一次pygame.draw.line,100个方块就是100*12=1200次调用,这对Pygame来说压力很大。可以把所有可见的边先收集到一个列表里,然后用pygame.draw.lines一次性绘制,能大幅减少API调用次数。
优化后的渲染代码:
screen.fill((0,0,0)) # 先收集所有可见的线段 visible_lines = [] for obj in cubes: cube_z = obj.center[2] - cam.pos[2] if cube_z + 0.5 <= 0: continue for edge in obj.edges: points = [] disp = True for x,y,z in (obj.verts[edge[0]],obj.verts[edge[1]]): x-=cam.pos[0] y-=cam.pos[1] z-=cam.pos[2] x,z = rotate2d((x,z),cam.rot[1]) y,z = rotate2d((y,z),cam.rot[0]) if z <=0: disp = False break # 只要一个点不可见,整个边就不渲染 f = 400/z x,y = x*f, y*f points.append((cx+int(x),cy+int(y))) if disp and len(points)==2: visible_lines.append(points) # 一次性绘制所有可见线段 pygame.draw.lines(screen, (255,255,255), False, visible_lines, 1)
3. 优化数学计算,减少重复运算
你的rotate2d函数每帧会被调用无数次,而且里面的sin和cos是耗时操作。可以提前计算相机旋转的正弦和余弦值,避免重复计算:
修改渲染循环:
在screen.fill之前添加:
# 预计算相机旋转的三角函数值,避免重复计算 rot_y_sin = math.sin(cam.rot[1]) rot_y_cos = math.cos(cam.rot[1]) rot_x_sin = math.sin(cam.rot[0]) rot_x_cos = math.cos(cam.rot[0])
然后替换rotate2d的调用,直接用预计算的值:
# 原来的x,z = rotate2d((x,z),cam.rot[1]) x,z = x*rot_y_cos - z*rot_y_sin, z*rot_y_cos + x*rot_y_sin # 原来的y,z = rotate2d((y,z),cam.rot[0]) y,z = y*rot_x_cos - z*rot_x_sin, z*rot_x_cos + y*rot_x_sin
这样每帧只需要计算4个三角函数值,而不是成千上万次,能节省不少CPU时间。
4. 避免循环内的低效操作
- 把
points+=[(cx+int(x),cy+int(y))]改成points.append(...),列表append比拼接更高效; - 修正代码里的笔误:
Colour变量定义了但没用到,渲染时用的colour是未定义的(应该是你打错了大小写); - 用Pygame的
clock.tick()代替手动计算时间,更可靠且能稳定帧率:dt = clock.tick(60) / 1000 # 限制最大60帧,同时得到秒为单位的dt
把这些优化点都加上后,渲染200个方块应该能轻松跑到60FPS。如果还想进一步优化,可以考虑用Pygame的OpenGL模块做硬件加速,不过那需要学习更多3D渲染知识,先把上面的软件渲染优化做好再说。
内容的提问来源于stack exchange,提问作者M. Campbell

