我正在用 Python 开发体素光线追踪器,刚刚从 Tkinter 移植到 Pygame,用于窗口管理和像素绘制。我使用线程池对每个像素进行光线跟踪,在我的原始代码中,
trace
函数在将颜色作为十六进制字符串返回之前执行各种计算:主循环在主线程上定期运行(例如:每秒 30 次)对于 30 FPS)并调用具有范围的池来请求新跟踪并更新所有像素颜色,每个索引都被转换为 2D 位置以了解每个颜色引用的位置。在这个简化的示例中,我遗漏了与我的问题无关的函数,例如我如何将索引 i
转换为自定义向量类中的两个 x, y
整数位置,与十六进制到 r, g, b
转换器相同...是的我有一种方法可以在退出时打破 while True
循环,下面的表示按预期运行。
import multiprocessing as mp
import pygame as pg
def trace(i):
# Rays are calculated here, for simplicity of example return a fixed color
return "ff7f00"
pg.init()
screen = pg.display.set_mode((64, 16))
clock = pg.time.Clock()
pool = mp.Pool()
while True:
# Raytrace each pixel and draw the new color, 64 * 16 = 1024
result = pool.map(trace, range(0, 1024)):
for i, c in enumerate(result):
pos = vec2_from_index(i)
col = rgb_from_hex(c)
screen.set_at((pos.x, pos.y), (col.r, col.g, col.b))
clock.tick(30)
但是有一个问题:主线程的性能非常慢,成为瓶颈,因此跟踪线程甚至无法充分发挥其潜力。在更高的分辨率下,有更多的像素,例如:
result
数组中有 240 x 120 = 28800 个条目;仅仅获取它而不对结果做任何事情就会给主线程带来负担,枚举结果来应用颜色会让情况变得更糟。我希望通过更改直接在计算它的线程上跟踪的像素来消除这种负担,而不是帮助线程仅仅返回 6 个字符的十六进制字符串,而主线程必须处理它。理想的代码应该是这样的:
import multiprocessing as mp
import pygame as pg
pg.init()
screen = pg.display.set_mode((64, 16))
clock = pg.time.Clock()
pool = mp.Pool()
def trace(i):
# Rays are calculated here, for simplicity of example return a fixed color
pos = vec2_from_index(i)
col = rgb_from_hex("ff7f00")
screen.set_at((pos.x, pos.y), (col.r, col.g, col.b))
while True:
# Raytrace each pixel and draw the new color, 64 * 16 = 1024
pool.map(trace, range(0, 1024)):
clock.tick(30)
然而,由于线程的工作方式,这种方法注定会失败:线程只能在函数结束时返回修改后的结果,它们不能以主线程或其他线程可以看到的方式直接从外部编辑变量线程。因此,该进程所做的任何更改都是临时的,并且仅在该线程完成之前存在于该线程的现实中。
如果有可能比我当前的方法更好的话,您认为最好的解决方案是什么?有没有办法让线程在屏幕表面上执行
pygame.set_at
并获得永久结果?另外,在这种情况下,我不需要线程池来返回结果...我应该使用 pool.map
以外的其他东西来提高效率吗?
我设法找到了完美的解决方案,也很乐意在这里分享!我将修改我最初的示例以展示我大致做了什么。
import multiprocessing as mp
import pygame as pg
import math
pg.init()
screen = pg.display.set_mode((64, 16))
clock = pg.time.Clock()
pool = mp.Pool()
threads = mp.cpu_count()
def draw_trace(i):
# Rays are calculated here, for simplicity of example return a fixed color
return rgb(255, 127, 0)
def draw(thread):
# Create a new surface and draw every pixel on it
srf = pg.Surface((64, math.ceil(16 / threads)))
for i in range(64 * math.ceil(16 / threads)):
pos = vec2_from_index(i)
col = draw_trace(i)
srf.set_at((pos.x, pos.y), (col.r, col.g, col.b))
return pg.image.tobytes(srf, "RGB")
while True:
# Raytrace each pixel and draw the new color, 64 * 16 = 1024
result = pool.map(draw, range(0, 1024)):
for i, s in enumerate(result):
srf = pg.image.frombytes(s, (64, math.ceil(16 / threads)), "RGB")
screen.blit(srf, (0, math.ceil(16 / threads) * i))
clock.tick(30)
这正是我想要的,一个线程始终在自己的垂直切片上工作:每个线程创建自己的表面,并在获得颜色后将像素绘制到其上。然后使用
tobytes
打包该表面,通过线程池发送到主线程,并使用 frombytes
解包...不这样做会导致有关“pickling”的错误,我不完全理解。然后主线程计算哪个垂直图块属于哪里,并将其传输到主画布以更新它。
性能提升非常明显:pygame 时钟报告超过 15 FPS,而我之前只有 10 FPS,在实践中感觉至少快了一倍!这可能是我将在架构上实现的最后一个重大性能改进,但肯定会使整个项目更加可用。如果有人有兴趣检查一下,您可以在 Github 上找到我的项目,其中现在包含此解决方案: