оптимизация системы частиц на основе сетки

Я реализовал игру, похожую на эту в Java и в настоящее время обнаруживаю, что достигаю предельного количества частиц ~80k. Моя игровая доска представляет собой двумерный массив ссылок на объекты «Частицы», каждый из которых должен обновляться в каждом кадре. Различные типы «частиц» имеют разное поведение и могут перемещаться или изменять свое состояние в зависимости от условий окружающей среды, таких как ветер или соседние частицы.

Некоторые возможные «правила», которые могут действовать:

  • Если Частица типа лавы соседствует с Частичкой типа воды, то обе исчезают, а лава заменяется обсидианом.
  • Если частица газа соседствует с частицей лавы, огня, уголька и т. д., она воспламеняется и производит огонь и дым.
  • Если достаточное количество пылевых частиц уложить друг на друга, те, что на более низких уровнях, как бы под давлением, могут стать осадочной породой.

Я искал вокруг и не смог найти какие-либо алгоритмы или структуры данных, которые кажутся особенно подходящими для ускорения задачи. Кажется, что какая-то мемоизация может быть полезной? Будет ли здесь полезно квадродерево? Я видел их использование в чем-то похожем на игру Конвея «Жизнь» с алгоритмом Hashlife. Или дело в том, что я не смогу сделать слишком много, чтобы увеличить скорость?


person paleto-fuera-de-madrid    schedule 30.08.2017    source источник
comment
Это похоже на проблему, для решения которой отлично подходит графический процессор. Я мало разбираюсь в программировании GPU, но mikeinnes.github.io/2017/08/ 24/cudanative.html предполагает, что попасть в него может быть проще, чем вы думаете.   -  person btilly    schedule 30.08.2017
comment
Hashlife полагается на локальность вычислений, и вы мало рассказали нам о своих правилах.   -  person maaartinus    schedule 30.08.2017
comment
@maaartinus я добавил немного информации о правилах   -  person paleto-fuera-de-madrid    schedule 30.08.2017
comment
@paleto-fuera-de-madrid Думаю, hashlife совместим с первыми двумя правилами (только для локальных взаимодействий), но не с последним. Я также скептически отношусь к использованию мемоизации из-за гораздо большего количества возможностей. Если бы вы могли опубликовать весь код на CR, вы могли бы получить там некоторую помощь (напишите мне, если да). Даже незначительное улучшение может дать вам хороший фактор скорости.   -  person maaartinus    schedule 31.08.2017
comment
@maaartinus Хорошо. Я сделаю это. Как я могу бросить вам записку?   -  person paleto-fuera-de-madrid    schedule 31.08.2017
comment
Упомянув меня в комментарии так же, как и вы. ;)   -  person maaartinus    schedule 31.08.2017
comment
@maaartinus codereview.stackexchange.com/questions/174508/   -  person paleto-fuera-de-madrid    schedule 01.09.2017


Ответы (3)


Hashlife в принципе будет работать, но есть две причины, по которым вы можете не получить от него столько же, сколько от Conway Life.

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

Во-вторых, как отметил другой плакат, правила, которые включают нелокальные эффекты, будут означать, что ваши примитивы (в Conway Life 4x4) должны быть больше, поэтому вам придется отказаться от разделяй и властвуй, скажем, 8x8 или 16x16 или любой другой размер, гарантирующий, что вы сможете правильно рассчитать средняя часть в n/2 времени. Это усугубляется разнообразием штатов. В Conway Life принято предварительно вычислять все сетки 4x4 или, по крайней мере, иметь почти все соответствующие в кеше. С 2 состояниями есть только 65536 сеток 4x4 (арахис на современных платформах), но только с 3 есть 43046721. Если вам нужно иметь примитивы 8x8, они очень быстро становятся очень большими и выходят за пределы любого реалистичного хранилища.

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

Один из способов справиться с этим примитивным размером — использовать правило скалы для распространения давления. Таким образом, Камень+n (n обозначает давление) становится Камнем+(n+1) в следующем поколении, если над ним есть Камень+m, где m>=n. До некоторого порога k, где он превращается в осадочную породу.

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

Если у вас есть типы ячеек, такие как «Птица» в приведенном примере, и у вас есть скорости, которые вы не сводите к минимуму (скажем, -1,0,1 в любом направлении), вы полностью разрушите запоминание. Даже в этом случае хаотическая природа таких правил может привести к исчезающе малому количеству обращений к кэшу в этих областях.

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

person Persixty    schedule 04.09.2017
comment
Поскольку Hashlife не кажется особенно полезным в таком случае из-за большого разнообразия поведения, можете ли вы придумать какие-либо другие соответствующие алгоритмы, которые я мог бы исследовать? Или вы думаете, что мне лучше всего было бы придумать умные способы уменьшить работу в расчете на одну частицу? - person paleto-fuera-de-madrid; 04.09.2017
comment
@paleto-fuera-de-madrid Вы все еще можете что-то получить с помощью какого-то пространственного дерева. Очевидно, что оптимизация — это игнорирование мертвого пространства, и Hashlife довольно хорош в этом — игнорируйте запоминание. Вам нужна структура данных, в которой можно легко перебирать не мертвые ячейки, а также получать их ближайших соседей для взаимодействия. Альтернативой может быть пара хэш-карт (x,y)->state. Повторите «старую» карту и заполните «новую» карту, осознавая, что вы можете легко получить доступ к соседям. Или какая-то разреженная матрица со ссылками, которые прыгают в мертвое пространство. - person Persixty; 05.09.2017
comment
Я пробовал хэш-карты раньше и обнаружил, что они слишком медленные для количества частиц, с которыми я имел дело. Я также пытался использовать некоторые целые числа, чтобы отслеживать максимальные и минимальные занятые координаты, чтобы минимизировать итерацию по массиву, но это тоже казалось медленнее, когда я тестировал его. Возможно ли, чтобы старый простой массив мог быть самой быстрой структурой данных для этого? - person paleto-fuera-de-madrid; 05.09.2017

я не совсем понимаю вашу проблему, но я думаю, что cuda или OpenGL (программирование графического процессора в целом) легко справятся с вашей реферальной ссылкой: https://dan-ball.jp/en/javagame/dust/

person dk1111    schedule 30.08.2017
comment
какая дополнительная информация сделает мою проблему более понятной для вас? Кроме того, не могли бы вы дать немного больше объяснений? - person paleto-fuera-de-madrid; 30.08.2017

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

Главное, что я бы сделал для Java, — это фактически избегал моделирования каждой частицы как объекта. Это должны быть необработанные данные, такие как простые старые данные, такие как числа с плавающей запятой или целые числа. Вы хотите иметь возможность работать с гарантиями непрерывности для пространственной локальности с последовательной обработкой и не платить за заполнение и 8-байтовые накладные расходы на экземпляр класса. Отделите холодные поля от горячих полей.

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

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

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

Сетка NxN должна быть очень тонкой, учитывая количество движущихся частиц, которые вы можете иметь в каждом кадре. Поиграйте с тем, сколько ячеек вы создаете, чтобы найти что-то оптимальное для ваших входных размеров. У вас может быть даже несколько сеток. Если определенные частицы не взаимодействуют друг с другом, не помещайте их в одну сетку. Не беспокойтесь об использовании памяти в сетке. Если каждая ячейка сетки просто хранит 32-битный индекс для первой частицы в ячейке, то сетка 200x200 занимает всего 160 килобайт с 32-битным индексом next на каждую частицу.

Я сделал что-то подобное несколько лет назад на языке C, используя описанную выше технику (но не с таким количеством интересных взаимодействий частиц, как в демо-игре), которая могла обрабатывать около 10 мл частиц, прежде чем она начала опускаться ниже 30 кадров в секунду, а на старом оборудовании всего лишь 2 ядра. Он использовал C, а также SIMD и многопоточность, но я думаю, что вы можете получить очень быстрое решение на Java, обрабатывающее кучу частиц одновременно, если вы сделаете вышеописанное.

Структура данных: введите здесь описание изображения

Когда частицы перемещаются из одной ячейки в другую, все, что вы делаете, — это манипулируете парой целых чисел, чтобы переместить их из одной ячейки в другую. Ячейки не «владеют памятью» и не выделяют ее. Это просто 32-битные индексы.

Чтобы выяснить, какую ячейку занимает частица, просто выполните:

cell_x = (int)(particle_x[particle_index] / cell_size)
cell_y = (int)(particle_y[particle_index] / cell_size)
cell_index = cell_y * num_cols + cell_x

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

person Community    schedule 10.12.2017
comment
не могли бы вы рассказать о механизме проверки столкновений? Я не понимаю, как взгляд на ячейку может рассказать обо всех соседях частицы. кажется, что некоторые из соседей частицы могут находиться в соседней ячейке. - person paleto-fuera-de-madrid; 20.03.2018
comment
Если все частицы имеют одинаковый размер, то вы можете определить, какие частицы могут столкнуться с данной частицей, проверив ячейку (ячейки), которые перекрывает частица (например, все ячейки, которые пересекают окружность частицы или AABB). Если частицы имеют разные размеры, то это немного сложнее. Вы можете сделать одну из двух вещей: 1) вставить частицы, которые перекрывают несколько ячеек, в каждую ячейку. 2) Сделайте ячейки свободными и увеличьте/уменьшите их AABB, чтобы частицы поместились внутри. У меня есть довольно длинное описание второго метода здесь: stackoverflow.com/a/48384354/4842163 - person ; 21.03.2018
comment
По сути, если все частицы имеют одинаковые размеры, вы можете рассматривать их как точки для вставки и вставлять каждую частицу только в одну ячейку. Однако для обнаружения столкновений вы запрашиваете область (например, круг или AABB), чтобы определить, какие частицы могут столкнуться с данной частицей. Если частицы не имеют одинаковых размеров, то вы либо вставляете частицу во все ячейки, которые она перекрывает, либо вставляете в одну ячейку, но такую, AABB которой может увеличиваться/уменьшаться. Затем вы делаете то же самое при запросе области и проверяете все ячейки, которые перекрывает частица. - person ; 21.03.2018
comment
Влияет ли размер объектов частиц, хранящихся в списке, на производительность при таком подходе? Кроме того, есть ли у вас эталонная реализация, на которую мы могли бы взглянуть? - person paleto-fuera-de-madrid; 22.03.2018
comment
Не в абсолютном, а в относительном смысле по отношению к размеру ячейки структуры данных. Если вы используете крошечные размеры ячеек, которые составляют часть размера частицы, например, и частицы сильно различаются по размеру, тогда большие частицы в конечном итоге будут вставлены во многие ячейки, поиск области в конечном итоге потребует проверки множества ячеек, и производительность страдает таким образом. Тем не менее, это в некоторой степени относится и к деревьям квадрантов (и к пространственным хэшам), за исключением свободных вариантов. Как правило, пространственные индексы требуют определенного уровня настройки в отношении сохраняемого контента. - person ; 23.03.2018
comment
Свободные варианты, как правило, очень хорошо работают для содержимого, которое сильно различается по размеру, поскольку размер самих ячеек регулируется в зависимости от того, что в них вставлено (это означает, что вам нужно вставить элемент только в одну ячейку/узел независимо от размера). Однако у них есть недостаток, заключающийся в том, что ваши поиски теперь требуют проверки AABB ячеек, тогда как с несвободными (жесткими) вариантами они требуют только просмотра одной точки, чтобы определить, какую ячейку пройти или какой квадрант дерева (quadtree, quadtree, kd-tree и т.д.) для прохождения. - person ; 23.03.2018
comment
Тем не менее, свободные варианты (свободные деревья квадрантов, свободные сетки), вероятно, наиболее близки к хорошо сбалансированной структуре данных для обнаружения столкновений, когда вы можете просто бросить в нее все, что хотите, и она хорошо сработает. Что касается исходного кода, эта ссылка обеспечивает полную реализацию дерева квадрантов, что, как правило, является достойным началом. Свободное дерево квадрантов и свободную сетку, как правило, довольно легко реализовать впоследствии. - person ; 23.03.2018
comment
Для обнаружения столкновений, особенно с очень динамичным контентом, мои личные голоса получают свободные варианты, и я добился наибольшего успеха с ними для контента, который сильно различается по размеру. Они были бы ужасны для таких контекстов, как трассировка лучей, где вашими узкими местами преобладают поисковые запросы. Однако для обнаружения столкновений вы, как правило, распределяете горячие точки между обновлением структуры данных (с перемещением элементов в каждом отдельном кадре) и поиском в ней. В этих случаях свободный вариант делает обновление (вставку/удаление) очень, очень дешевым, даже если поиск немного дороже. - person ; 23.03.2018