Как я могу оптимизировать свой базовый физический симулятор?

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

альтернативный текст
[Ссылка на более крупную версию]

Было весело работать над этой небольшой программой, и я хочу продолжить ее, если смогу. Я знаю, что они говорят, что преждевременная оптимизация - это корень всех зол, но я начинаю сталкиваться с фактическими барьерами в производительности, и я хочу знать, может ли мне помочь кто-нибудь, имеющий опыт разработки игр / симуляторов.

Проблема:

Прямо сейчас моя программа задыхается, если вы добавляете слишком много мячей (кажется, на моей машине она не может обработать больше 800). Если вы это сделаете, симуляция перестанет быть реалистичной, и все шары будут перекрывать друг друга внизу.

Проблема в обнаружении столкновений. В самом наивном случае обнаружение столкновений представляет собой проблему O (N ^ 2). Каждый мяч проверяет каждый другой мяч. Это довольно быстро ухудшает производительность (даже после 100 мячей вы будете выполнять 10 тысяч проверок столкновений за цикл цикла).

Если вы посмотрите сюда, вы увидите скриншот, на котором я добавил несколько сотен шаров. Симулятор не успевает, и они начинают накладываться друг на друга.

альтернативный текст
[Ссылка на более крупную версию]

В настоящее время я обнаруживаю столкновения, ища перекрывающиеся шары. Если я нахожу два перекрывающихся шара, я разделяю их по минимальному расстоянию перемещения (MTD) или раздвигаю. Затем я использую простое физическое уравнение, чтобы настроить их векторы импульсов, и после столкновения они расходятся в разных направлениях.

Он отлично работает, за исключением случаев, когда мячей слишком много, минимальное расстояние перевода становится заметным. Они начинают сильно накладываться друг на друга и постоянно толкают друг друга снизу. Чем больше я увеличиваю «гравитацию», тем хуже становится. Давление на них увеличивается, и степень их сжатия / перекрытия увеличивается.

Опять же, у меня нет проблем, пока я не забью приличное количество мячей.

Текущий метод оптимизации :

Обнаружение столкновений -
развернуть и отсечь - (также известное как «Сортировка и очистка»)

Я использую сортировку вставками для своих мячей в каждом цикле цикла по оси x. Благодаря природе сортировки вставкой я могу использовать временную когерентность моего симулятора. От кадра к кадру позиции шаров меняются незначительно, так что сортировке не нужно много работать. Это приближает амортизированное время выполнения Linear Sorts к O (N) или линейно, а не к среднему времени выполнения O (N ^ 2).

Поскольку шары отсортированы, я провожу пару предварительных проверок во втором цикле перед проверкой столкновений. Теперь мне нужно только проверить шары рядом друг с другом (потому что они отсортированы по оси x), и я выхожу из второго цикла каждый раз, когда проверяю мяч против другого шара, xmin которого больше, чем xmax текущего шара. . Таким образом, он пропускает тысячи проверок.

Когда я реализовал это, это принесло огромное улучшение скорости. Тем не менее, я все же хотел бы иметь возможность обрабатывать более 600-800 мячей. Я читал о физических механизмах, которые легко обрабатывают обнаружение столкновений между 10 тысячами объектов одновременно, поэтому мне хотелось бы думать, что я смогу достичь 1-2 тысяч с небольшой работой.

После запуска профилировщика выяснилось, что обнаружение столкновений занимало около 55% моего времени, в то время как рендеринг занимал около 45%. Итак, это две мои самые дорогие затраты.


Вопрос:

Можете ли вы придумать лучшие алгоритмы или методы, которые позволили бы моему симулятору обрабатывать больше мячей?


Соответствующий код:

Весь проект:

svn checkout http://simucal-projects.googlecode.com/svn/ballbounce/trunk/

или щелкните здесь, чтобы просмотреть файлы вручную в браузере.

Разделы интересов:


Ответов (13)

Попробуй это:

Разделите прямоугольник на N * M квадратов так, чтобы квадраты были немного шире, чем радиус шара. Было бы неплохо, чтобы квадраты перекрывали края вашего прямоугольника, а не аккуратно вписывались в него.

Сделайте массив BitSet. Не используйте Bitset [M] [N], просто новый Bitset [M * ​​N] - небольшое умножение вам не повредит.

Обозначьте каждый шар номером. Когда вы размещаете мяч в определенном месте, установите бит в битовом наборе для этого квадрата и 8 квадратов вокруг него (чтобы упростить задачу, расширьте массив квадратов так, чтобы они выходили на один за край прямоугольника - таким образом вы не нужно зажимать.)

Бегите по площадям. Для каждой пары шаров в каждом квадрате отметьте эту пару как потенциальное столкновение. Для этого создайте битовый набор и - учитывая, что у вас H шаров, а шары A и B занимают один и тот же квадрат - установите биты A + B H и A H + B.

Перебрать потенциальные конфликты теперь легко, потому что BitSet включает метод, который говорит: «Найди мне следующий бит после установленного». Помните, что каждый бит имеет двойной счет, поэтому, когда бит Q определяется как установленный, не забудьте сбросить бит, (Q%H)*H + (Q/H) который является другим битом пары.

В качестве альтернативы: вы можете довольно легко свернуть этот массив столкновений. Конфликт между A и B - при условии, что A> B может быть отмечен установкой бита A * (A-1) / 2 + B . Это имеет то преимущество, что вам не нужно заботиться об общем количестве шаров.

На самом деле: забудьте об этом. просто используйте этот класс, который я написал в качестве упражнения:

import java.util.BitSet;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class PairSet extends BitSet implements
    Iterable<PairSet.Pair> {
  public static class Pair implements Comparable<Pair> {
    public final int a;
    public final int b;

    private Pair(int a, int b) {
      if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
          "Pair(" + a + "," + b + ")"); }
      if (a > b) {
        this.a = a;
        this.b = b;
      } else {
        this.a = b;
        this.b = a;
      }
    }

    public String toString() {
      return "Pair(" + a + "," + b + ")";
    }

    public int hashCode() {
      return a * (a - 1) / 2 + b;
    }

    public boolean equals(Object o) {
      return o instanceof Pair
          && hashCode() == ((Pair) o).hashCode();
    }

    public int compareTo(Pair o) {
      return hashCode() - o.hashCode();
    }

  }

  PairSet() {}

  PairSet(BitSet z) {
    or(z);
  }

  PairSet(Iterable<Pair> z) {
    for (Pair p : z)
      set(p);
  }

  public void set(Pair p) {
    set(p.a, p.b);
  }

  public void clear(Pair p) {
    clear(p.a, p.b);
  }

  public void set(int a, int b) {
    if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
        "add(" + a + "," + b + ")"); }
    if (a > b) {
      set(a * (a - 1) / 2 + b);
    } else {
      set(b * (b - 1) / 2 + a);
    }
  }

  public void clear(int a, int b) {
    if (a < 0 || b < 0 || a == b) { throw new IllegalArgumentException(
        "add(" + a + "," + b + ")"); }
    if (a > b) {
      clear(a * (a - 1) / 2 + b);
    } else {
      clear(b * (b - 1) / 2 + a);
    }
  }

  public Iterator<Pair> iterator() {
    return new Iterator<Pair>() {
      int at       = -1;
      int triangle = 0;
      int a        = 0;

      public boolean hasNext() {
        return nextSetBit(at + 1) != -1;
      }

      public Pair next() {
        int nextat = nextSetBit(at + 1);
        if (nextat == -1) { throw new NoSuchElementException(); }
        at = nextat;
        while (triangle <= at) {
          triangle += a++;
        }
        return new Pair(a - 1, at - (triangle - a) - 1);

      }

      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }
}

И это будет хорошо отслеживать ваши потенциальные столкновения. Псудеокод тогда

SW = width of rectangle
SH = height of rectangle
R = radius of balls + 1 // +1 is a fudge factor.

XS = number of squares across = SW/R + 4; // the +4 adds some slop
YS = number of squares hight = SH/R + 4; // the +4 adds some slop

int sx(Point2D.Float p) // the square into which you put a ball at x
   // never returns a number < 1
 := (int)((p.x-R/2)/R) + 2;

int sy(Point2D.Float p) // the square into which you put a ball at y
   // never returns a number < 1
 := (int)((p.y-R/2)/R) + 2;

Bitset[] buckets = new BitSet[XS*YS];
{for(int i: 0; i<buckets.length; i++) bukets[i] = new BitSet();}

BitSet bucket(int x, int y) {return bucket[y*XS + x]}
BitSet bucket(Point2D.Float p) {return bucket(sy(p),sx(p));}

void move(int ball, Point2D.Float from, Point2D.Float to) {
  if bucket(from) == bucket(to) return;
  int x,y;
  x = sx(from); y=sy(from);
  for(int xx==-1;xx<=1; xx++)
  for(int yy==-1;yy<=1; yy++)
  bucket(sx+xx, sy+yy).clear(ball);
  x = sx(to); y=sy(to);
  for(int xx==-1;xx<=1; xx++)
  for(int yy==-1;yy<=1; yy++)
  bucket(sx+xx, sy+yy).set(ball);
} 

PointSet findCollisions() {
    PointSet pp = new PointSet();
    for(BitSet bb: buckets) {
    int a;
    int prev_a;
    for(prev_a = -1; (a = bb.nextSetBit(prev_a+1))!=-1; prev_a=a) {
      int b;
      int prev_b;
      for(prev_b = a; (b = bb.nextSetBit(prev_b+1))!=-1; prev_b=b) {
        pp.add(a,b);
      }
    }
    return pp;
}

Я слежу за разработкой Chipmunk с самого начала, и, насколько я понимаю, ответ на оптимизацию заключается в структурах данных. (не всегда?) ...

Структура данных Бурундук использует для достижения этого является Пространственное хеширования .

Симукал. Я понимаю, что отвечаю с опозданием примерно на полтора года, но все же хочу записать свои мысли.

Недавно я написал вопрос, касающийся той же проблемы, что и перекрытие ваших мячей , в котором фактически использовался тот же алгоритм, который вы использовали. Я не видел вашего вопроса, когда отправлял его, так что плохо.

Прежде всего,

Оптимизация : я использую простую систему разделения сетки, где весь экран разделен на ячейки, ширина и высота которых равны наибольшему диаметру шара. Сетка отслеживает, в какой ячейке находится каждый шар, поэтому, когда требуется проверка на столкновение, я использую функцию nearBy (), которая заполняет список идентификаторов каждого шара, находящегося в ячейках, смежных с тем, который я проверяю. .

Метод сетки работает отлично, и я могу иметь до 2000 мячей до того, как я запаздываю. Удаление и добавление мячей в сетку - это немного неудобно, но именно так я реализовал это (сетка в значительной степени основана на списке мячей и позиции индекса каждого шара).

В будущем я хотел бы изучить другие методы разделения и оптимизации проверки коллизий.

Перекрытие : многие ответы здесь и в других местах предполагают, что вы рекурсивно исправляете коллизии в каждом кадре. Это действительно сработало для меня до некоторой степени. При достаточно хорошей оптимизации вы могли бы уйти с 2 или 3 проверками на кадр, что, кажется, предотвратило бы часть перекрывающегося беспорядка. Однако это не идеально. Я подозреваю, что повышение точности (с помощью таких причудливых слов, как интерполяция и лучшая интеграция) может помочь с дрожанием и перекрытием.

Я подумал о том, чтобы упорядочить проверки столкновений на основе приоритета (самые высокие - те, которые касаются стен, затем те, которые касаются их, - на один шаг вниз в списке приоритетов и т. Д.), И учесть это в минимальном расстоянии перевода. MyPhysicsLab говорит об обработке нескольких одновременных коллизий, но мне еще предстоит это изучить.

Если вы когда-нибудь что-нибудь узнаете, обновите! Я работаю над теми же проблемами, и симуляторы мяча кажутся довольно популярными. Такой опыт может пригодиться, если мы когда-нибудь перейдем к физике твердого тела.

Спасибо.

Как только шар полностью окружен другими шарами, перестаньте рассматривать его для обнаружения столкновения. При взгляде на ваш снимок экрана кажется, что следует рассматривать только «поверхностные» шары. Зачем проверять мяч на глубину 6 мячей, с которой ничто не может столкнуться? Это значительно снизит количество потенциальных столкновений.

Жесткие физические движки используют векторизацию плавающих объектов, что дает прирост в 16 раз на текущем оборудовании, если повезет, и намного больше на специализированном оборудовании. Larrabee, например, может обрабатывать 1024 одновременных вычисления для увеличения математической обработки в x1024 (но ему это нужно, потому что это также графический процессор)

Вы еще не смотрели код, но используете ли вы математические оптимизации, такие как быстрый квадратный корень и двоичные абсолюты, или побитовое переключение знаков? Сами по себе эти вещи не помогают, но когда вы сбрасываете большие объемы данных, эти методы перенаправляют некоторую математику на основной ЦП, освобождая FPU, что в основном дает вам большую пропускную способность.

Также генерация кода SIMD GCC немного отстой, я видел до 16-кратного увеличения при использовании VC или IntelCompiler, это означает, если вы обратили внимание, что GCC вообще не использовал никаких инструкций SIMD!

Кроме того, эти предполагаемые столкновения 10k не находятся в таком близком месте, как вы симулируете, поэтому их нельзя напрямую сравнивать.

Может проблема в том, что происходит так много взаимодействий, когда шары «скапливаются»? Если бы я делал это, я бы попробовал следующее:

  • поверните гравитацию до 0, чтобы множество столкновений не происходило одновременно
  • профилируйте свой алгоритм обнаружения столкновений - если вы используете отсортированный массив шаров и анализируете только 6 или 7 ближайших, то у вас не должно возникнуть никаких проблем ... это всего 8000 или около того проверок столкновений за цикл, предполагая 800 шаров, что не очень много

Следите за ближайшими шарами -

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

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

Вы также можете следить за векторами каждого шара, рисовать виртуальные линии и видеть, какие линии пересекаются в следующих 3-5 кадрах, и рассчитывать только те столкновения, которые могут произойти (хотя со временем произойдет немного).

Так же, как вы отсортировали по оси x, вы можете «группировать» шары по подразделениям внутри главного окна. Когда мяч находится внутри подразделения по крайней мере на один диаметр шара, ему нужно только смотреть на шары в том же подразделении. Если это ближе к границе или двух, вам нужно посмотреть на одно или два других подразделения, но вычислений должно быть меньше.

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

В зависимости от того, насколько многолюдно подразделение (количество шариков на квадратную единицу), вы можете попробовать разные алгоритмы - для редко заполненного поля требуются только векторные вычисления для прогнозирования столкновений, тогда как для плотно упакованного подразделения может потребоваться только какой-то оптимизированный гексаграфический расчет. Редкие блоки могут не нуждаться в обнаружении столкновений для многих кадров (поскольку перекрытия не будут заметны так сильно, как в густонаселенных блоках).

Плотность энергии данного ящика может быть определена - очень разреженный ящик с шарами с низкой энергией требует меньше вычислений столкновений, чем разреженный ящик с высокой энергией.

-Адам

Я думаю, что пора измерить производительность, чтобы выяснить, где на самом деле находится узкое место. Вам не нужно было проводить измерения раньше, потому что была очевидная проблема алгоритма. Сейчас еще есть возможности для улучшения алгоритма, но уверены ли вы, что это самая большая проблема? Измерьте, сколько сравнений вы делаете сейчас на мяч. Это что-то маленькое? Если так, то изменение алгоритмов может быть не лучшим следующим шагом.

Сравните, сколько времени требуется, чтобы определить положение каждого шара, и время, необходимое для их фактического извлечения. Возможно, это позднее является узким местом, и вам следует сосредоточить свои усилия на коде рендеринга, а не на физическом движке.

Самый простой способ - не тестировать столкновения объектов и объектов, а заполнить массив центральной точкой каждого шара. Затем из каждого центра сканируйте квадрат размером 4 * радиус с центром в этой точке (вы можете немного оптимизировать это, не используя квадрат, за счет усложнения кода). Если в этом квадрате есть еще один центр, только тогда вы проверяете, находятся ли они в радиусе 2 * друг от друга (и, следовательно, сталкиваются). Вы можете дополнительно оптимизировать это, уменьшив разрешение и округлив положение мяча, уменьшив количество проверок, которые вам нужно выполнить.

Другой способ - разделить пространство на сетку и хранить объекты в областях сетки. Вам нужно только проверить наличие столкновений между объектами в соседних сетках.

В этот момент это начинает звучать почти как слегка сжимаемая жидкость в гравитационном поле. Идеи вычислительной гидродинамики, использующие эйлерову точку зрения, могут помочь. Если вы создаете сетку с таким масштабом, что только один шар в каждый момент времени может занимать каждую ячейку, вы можете записать баланс массы, импульса и энергии для каждой ячейки и таким образом отслеживать движение шаров.

Ваша основная проблема - это алгоритм разрешения столкновений - вы можете ускорить прорисовку и обнаружение столкновений, но это не помешает вашим шарам сталкиваться друг с другом. Проблема, которая у вас есть, намного сложнее решить чисто, чем кажется; хотя можно сделать это правильно.

В вашем случае в конфигурации, которая терпит неудачу (большие шары), ваши объекты должны действительно скользить друг относительно друга, а не отскакивать. Скольжение - это непрерывное ограничение, которое отличается от ограничений, основанных на импульсах, которые обрабатывает ваша реализация.

Самое простое, что вы можете сделать, чтобы предотвратить коллапс, - это перепроверить все сталкивающиеся объекты, чтобы убедиться, что они не взаимопроникают после того, как вы выполните обработку столкновений, - возможно, повторяя столько раз, сколько необходимо, пока ни один из них не нарушит ограничения. Это, конечно, займет больше времени - возможно, намного дольше (и даже повышает вероятность бесконечного цикла, хотя, возможно, не в этой конкретной ситуации ...).

Есть хорошие алгоритмы, которые заставят вашу симуляцию вести себя, но они сложнее вашей импульсной системы. Как правило, они включают рассмотрение взаимно взаимодействующих тел (например, шариков в корзине) в целом и корректировку их коллективной конфигурации для удовлетворения их взаимных ограничений.

Вам следует искать работы (книги, статьи, веб-сайты) по моделированию нескольких тел . Я не могу сказать, что может быть наиболее полезным для ваших целей; На вашем месте я бы начал с посещения хорошей университетской библиотеки и просмотра всех имеющихся у них книг по этой теме. Вы должны быть готовы к серьезной математике; если такие термины, как «множители Лагранжа» вызывают у вас крапивницу, обратите внимание на 8 ^). По сути, если вы пойдете по этому пути, вы, вероятно, выучите много математики и немалое количество физики.

Я сделал что-то подобное на iPhone, и он использует акселерометр, чтобы вы могли наклонять шары, и сенсорный экран, чтобы добавлять и удалять шары. Он может обработать как минимум 30 мячей, прежде чем начнет заметно увязать.

Одна из оптимизаций, которые я сделал на раннем этапе, - это встроенная математика. Изначально у меня был отдельный "векторный" класс, и он мог обрабатывать только 10-12 мячей, прежде чем превратился в слайд-шоу. Профилирование показало, что он тратит много времени на выделение и освобождение векторов.

Кроме того, я не разделяю шары, когда они попадают, я просто отскакиваю от векторов. Кажется, что они никогда не пересекаются, и когда они это делают, совершенно очевидно, что это потому, что они все застряли вместе внизу.

Я еще не совсем готов выпустить код, мне нужно кое-что отполировать, потом выложу в магазин.

Если я что-то не пропустил, перекрытие шаров является результатом ошибки, а не неэффективного кода. Неэффективный код просто замедлит анимацию.

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