You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Pygame的Python 3D渲染引擎卡顿问题优化咨询

问题:Pygame 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:20:24