Подтвердить что ты не робот

Улучшение производительности функции трассировки лучей

У меня есть простой raytracer в python. рендеринг изображения 200x200 занимает 4 минуты, что, безусловно, слишком много для моего вкуса. Я хочу улучшить ситуацию.

Некоторые моменты: я снимаю несколько лучей на каждый пиксель (для обеспечения сглаживания) для общей суммы 16 лучей на пиксель. 200x200x16 - это общее количество 640000 лучей. Каждый луч должен быть проверен на воздействие на несколько объектов Sphere в сцене. Ray также является довольно тривиальным объектом

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

Сфера немного сложнее и несет логику для hit/nohit:

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?

4b9b3361

Ответ 1

Глядя на ваш код, похоже, что ваша главная проблема в том, что у вас есть строки кода, которые вызываются 2560000 раз. Это займет много времени, независимо от того, какую работу вы выполняете в этом коде. Однако, используя NumPy, вы можете объединить большую часть этой работы в небольшое количество NUPY-вызовов.

Первое, что нужно сделать, это объединить ваши лучи вместе в большие массивы. Вместо использования объекта Ray с векторами 1x3 для начала и направления используйте массивы 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]
...

продолжить только со значениями, которые проходят дискриминантный тест. Таким образом, вы можете легко повысить производительность на несколько порядков.

Ответ 2

1) Трассировка лучей - это весело, но если вы вообще не заботитесь о производительности, дамп-питон и переключитесь на C. Не С++, если вы не являетесь супер-экспертом, просто C.

2) Большая победа в сценах с несколькими (20 или более) объектами заключается в использовании пространственного индекса для уменьшения числа тестов пересечения. Популярные варианты - kD-деревья, OctTrees, AABB.

3) Если вы серьезно, посмотрите ompf.org - это ресурс для этого.

4) Не ходите на ompf, когда python спрашивает об оптимизации - большинство людей могут снимать от 1 миллиона до 2 миллионов лучей в секунду через внутреннюю сцену с 100 тысячами треугольников... На ядро.

Я люблю Python и трассировку лучей, но никогда не буду рассматривать их вместе. В этом случае правильная оптимизация заключается в переключении языков.

Ответ 3

С таким кодом вы можете воспользоваться мемонимированием общих подвыражений, таких как self.radius * self.radius как self.radius2 и 1 / self.radius как self.one_over_radius. Накладные расходы интерпретатора python могут доминировать в таких незначительных улучшениях.

Ответ 4

Одна незначительная оптимизация: a и b * b всегда положительны, поэтому disc < 0.0 истинно, если (c > 0 && b < min(a, c)). В этом случае вы можете избежать вычисления b * b - 4.0 * a * c. Учитывая профиль, который вы сделали, это, скорее всего, побьет 7% времени выполнения удара, я думаю.

Ответ 5

Лучше всего будет использовать таблицы поиска и предварительно рассчитанные значения как можно больше.

Как ваш ответ на мой комментарий указывает, что ваши векторы направления лучей являются единичными векторами, в критическом разделе, который вы указали, вы можете сделать хотя бы одну оптимизацию сразу с места в карьер. Любая векторная точка сама по себе является квадратом длины, поэтому сама единичная векторная точка всегда будет равна 1.

Кроме того, предварительно вычислить квадрат радиуса (в вашей сфере __init__).

Тогда у вас есть:

temp = ray.origin - self.center
a = 1 # or skip this and optimize out later
b = 2.0 * numpy.dot(temp, ray.direction)
c = numpy.dot(temp, temp) - self.radius_squared
disc = b * b - 4.0 * c

temp dot temp даст вам эквивалент sum( map( lambda component: component*component, temp ) )... я не уверен, что быстрее, хотя.