优化光线追踪命中函数的性能

14 投票
5 回答
3465 浏览
提问于 2025-04-16 20:33

我在用Python写一个简单的光线追踪程序。渲染一张200x200的图片要花4分钟,这对我来说实在太久了。我想改善这个情况。

有几点需要说明:我每个像素发射多条光线(为了减少锯齿),每个像素总共发射16条光线。200x200x16总共就是640000条光线。每条光线都要检测它是否与场景中的多个球体相交。光线的定义其实比较简单。

class Ray(object):
    def __init__(self, origin, direction):
        self.origin = numpy.array(origin)
        self.direction = numpy.array(direction)

而球体的定义稍微复杂一点,它包含了判断是否相交的逻辑:

class Sphere(object):
    def __init__(self, center, radius, color):
        self.center = numpy.array(center)
        self.radius = numpy.array(radius)
        self.color = color

    @profile 
    def hit(self, ray):
        temp = ray.origin - self.center
        a = numpy.dot(ray.direction, ray.direction)
        b = 2.0 * numpy.dot(temp, ray.direction)
        c = numpy.dot(temp, temp) - self.radius * self.radius
        disc = b * b - 4.0 * a * c

        if (disc < 0.0):
            return None
        else:
            e = math.sqrt(disc)
            denom = 2.0 * a
            t = (-b - e) / denom 
            if (t > 1.0e-7):
                normal = (temp + t * ray.direction) / self.radius
                hit_point = ray.origin + t * ray.direction
                return ShadeRecord.ShadeRecord(normal=normal, 
                                               hit_point=hit_point, 
                                               parameter=t, 
                                               color=self.color)           

            t = (-b + e) / denom

            if (t > 1.0e-7):
                normal = (temp + t * ray.direction) / self.radius                hit_point = ray.origin + t * ray.direction
                return ShadeRecord.ShadeRecord(normal=normal, 
                                               hit_point=hit_point, 
                                               parameter=t, 
                                               color=self.color)       

        return None    

现在,我做了一些性能分析,发现处理时间最长的部分在hit()这个函数里。

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  2560000  118.831    0.000  152.701    0.000 raytrace/objects/Sphere.py:12(hit)
  1960020   42.989    0.000   42.989    0.000 {numpy.core.multiarray.array}
        1   34.566   34.566  285.829  285.829 raytrace/World.py:25(render)
  7680000   33.796    0.000   33.796    0.000 {numpy.core._dotblas.dot}
  2560000   11.124    0.000  163.825    0.000 raytrace/World.py:63(f)
   640000   10.132    0.000  189.411    0.000 raytrace/World.py:62(hit_bare_bones_object)
   640023    6.556    0.000  170.388    0.000 {map}

这并不让我感到意外,我想尽量减少这个时间。我进行了逐行分析,结果是:

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    12                                               @profile
    13                                               def hit(self, ray):
    14   2560000     27956358     10.9     19.2          temp = ray.origin - self.center
    15   2560000     17944912      7.0     12.3          a = numpy.dot(ray.direction, ray.direction)
    16   2560000     24132737      9.4     16.5          b = 2.0 * numpy.dot(temp, ray.direction)
    17   2560000     37113811     14.5     25.4          c = numpy.dot(temp, temp) - self.radius * self.radius
    18   2560000     20808930      8.1     14.3          disc = b * b - 4.0 * a * c
    19                                                   
    20   2560000     10963318      4.3      7.5          if (disc < 0.0):
    21   2539908      5403624      2.1      3.7              return None
    22                                                   else:
    23     20092        75076      3.7      0.1              e = math.sqrt(disc)
    24     20092       104950      5.2      0.1              denom = 2.0 * a
    25     20092       115956      5.8      0.1              t = (-b - e) / denom
    26     20092        83382      4.2      0.1              if (t > 1.0e-7):
    27     20092       525272     26.1      0.4                  normal = (temp + t * ray.direction) / self.radius
    28     20092       333879     16.6      0.2                  hit_point = ray.origin + t * ray.direction
    29     20092       299494     14.9      0.2                  return ShadeRecord.ShadeRecord(normal=normal, hit_point=hit_point, parameter=t, color=self.color)

所以,看起来大部分时间都花在了这段代码上:

        temp = ray.origin - self.center
        a = numpy.dot(ray.direction, ray.direction)
        b = 2.0 * numpy.dot(temp, ray.direction)
        c = numpy.dot(temp, temp) - self.radius * self.radius
        disc = b * b - 4.0 * a * c

在这里,我并没有看到很多可以优化的地方。你们有没有什么建议,能让我在不使用C语言的情况下提高这段代码的性能?

5 个回答

1

像这样的代码,你可以通过记住一些常用的计算结果来提高效率,比如把 self.radius * self.radius 记作 self.radius2,还有 1 / self.radius 记作 self.one_over_radius。不过,Python解释器的开销可能会让这些小改进的效果不太明显。

7

1) 光线追踪很有趣,但如果你在乎性能的话,还是放弃Python,换成C语言。C++就不推荐了,除非你是那种超级专家,直接用C就好。

2) 如果场景里有很多物体(比如20个以上),使用空间索引可以减少碰撞检测的次数,这样能提高效率。常见的选择有kD树、八叉树和AABB。

3) 如果你认真想了解这方面的知识,可以去ompf.org网站看看,那是个很好的资源。

4) 不要在ompf上用Python问优化的问题——那里的大多数人每秒能在一个有10万个三角形的室内场景中射出100万到200万条光线……每个核心都是这样。

我喜欢Python和光线追踪,但绝对不会把它们放在一起。在这种情况下,正确的做法是换一种编程语言。

9

看你的代码,主要问题是你有一些代码行被调用了2560000次。无论你在这些代码里做什么,这样都会耗费很多时间。不过,使用numpy,你可以把很多工作合并成少量的numpy调用。

首先,你需要把你的光线(ray)合并成大的数组。与其使用一个包含1x3向量的Ray对象来表示起点和方向,不如使用Nx3的数组,这样可以包含你所有需要的光线用于碰撞检测。你的碰撞检测函数的顶部最终会像这样:

temp = rays.origin - self.center
b = 2.0 * numpy.sum(temp * rays.direction,1)
c = numpy.sum(numpy.square(temp), 1) - self.radius * self.radius
disc = b * b - 4.0 * c

接下来,你可以使用

possible_hits = numpy.where(disc >= 0.0)
a = a[possible_hits]
disc = disc[possible_hits]
...

来继续处理那些通过判别式测试的值。这样做可以轻松获得数量级的性能提升。

撰写回答