Эффективная (и хорошо объясненная) реализация квадрата для 2D - обнаружения столкновений [закрыто]

Я работал над добавлением Quadtree в программу, которую я пишу, и я не могу не заметить, что есть несколько хорошо объясненных/исполняющих учебников для реализации, которую я ищу.

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

7 ответов


1. Эффективное Деревья Квадрантов

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

enter image description here

GIF имеет чрезвычайно уменьшенную частоту кадров и значительно более низкое разрешение, чтобы соответствовать максимуму 2 МБ для этого сайта. Вот видео, Если вы хотите увидеть вещь на близкой к полной скорости: https://streamable.com/3pgmn.

и GIF со 100k, хотя мне пришлось так много возиться с ним и пришлось отключить линии quadtree (похоже, не хотелось сжимать их так сильно), а также изменить способ, которым агенты выглядели, чтобы он поместился в 2 мегабайта (я хотел бы, чтобы сделать GIF было так же просто, как кодировать quadtree):

enter image description here

моделирование с агентами 20k занимает ~3 мегабайта ОЗУ. Я также могу легко обрабатывайте 100k меньших агентов без ущерба для частоты кадров, хотя это приводит к некоторому беспорядку на экране до такой степени, что вы едва можете видеть, что происходит, как в GIF выше. Все это работает только в одном потоке на моем i7, и я трачу почти половину времени в соответствии с VTune, просто рисуя этот материал на экране (просто используя некоторые основные скалярные инструкции для построения по одному пикселю за раз в CPU).

и вот видео со 100 000 агентов хотя трудно понять, что происходит. Это своего рода большое видео, и я не мог найти достойного способа сжать его без превращения всего видео в кашу (возможно, сначала нужно загрузить или кэшировать его, чтобы увидеть его поток с разумным FPS). С агентами 100k моделирование занимает около 4,5 мегабайт ОЗУ, а использование памяти очень стабильно после запуска моделирования в течение примерно 5 секунд (останавливается вверх или вниз, так как он перестает выделять кучу). то же самое в замедленном движение.

эффективный Quadtree для обнаружения столкновения

хорошо, так что на самом деле quadtrees не моя любимая структура данных для этой цели. Я предпочитаю сеточную иерархию, такую как грубая сетка для мира, более тонкая сетка для региона и еще более тонкая сетка для субрегиона (3 фиксированных уровня плотных сеток и никаких деревьев), с оптимизацией на основе строк, так что строка, в которой нет объектов, будет освобождена и повернута в нулевой указатель, а также полностью пустые области или субрегионы, превращенные в нули. Хотя эта простая реализация quadtree, работающая в одном потоке, может обрабатывать 100k агентов на моем i7 при 60 + FPS, я реализовал сетки, которые могут обрабатывать пару миллионов агентов, отскакивающих друг от друга каждый кадр на более старом оборудовании (i3). Кроме того, мне всегда нравилось, как сетки очень легко предсказывают, сколько памяти им потребуется, поскольку они не подразделяют клетки. Но я постараюсь рассказать, как реализовать достаточно эффективное четырехугольное дерево.

обратите внимание, что я не буду вдаваться в полную теорию структуры данных. Я предполагаю, что вы уже знаете, что и заинтересованы в повышении производительности. Я также просто иду в свой личный способ решения этой проблемы, которая, кажется, превосходит большинство решений, которые я нахожу в интернете для своих случаев, но есть много достойных способов, и эти решения адаптированы к моим случаям использования (очень большие входы со всем, что перемещает каждый кадр для visual FX в кино и на телевидении). Другие люди, вероятно, оптимизируют для разных случаев использования. Когда дело доходит до структур пространственной индексации, в частности, я действительно думаю, что эффективность решения говорит вам больше о разработчике, чем о структуре данных. Также те же стратегии, я предлагаю для ускорения вещи также применяется в 3-х измерениях с octrees.

Представление Узла

Итак, прежде всего, давайте рассмотрим узел представление:

// Represents a node in the quadtree.
struct QuadNode
{
    // Points to the first child if this node is a branch or the first
    // element if this node is a leaf.
    int32_t first_child;

    // Stores the number of elements in the leaf or -1 if it this node is
    // not a leaf.
    int32_t count;
};

это в общей сложности 8 байт, и это очень важно, так как это ключевая часть скорости. На самом деле я использую меньший (6 байт на узел), но я оставлю это как упражнение для читателя.

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

где AABBs?

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

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

static QuadNodeList find_leaves(const Quadtree& tree, const QuadNodeData& root, const int rect[4])
{
    QuadNodeList leaves, to_process;
    to_process.push_back(root);
    while (to_process.size() > 0)
    {
        const QuadNodeData nd = to_process.pop_back();

        // If this node is a leaf, insert it to the list.
        if (tree.nodes[nd.index].count != -1)
            leaves.push_back(nd);
        else
        {
            // Otherwise push the children that intersect the rectangle.
            const int mx = nd.crect[0], my = nd.crect[1];
            const int hx = nd.crect[2] >> 1, hy = nd.crect[3] >> 1;
            const int fc = tree.nodes[nd.index].first_child;
            const int l = mx-hx, t = my-hx, r = mx+hx, b = my+hy;

            if (rect[1] <= my)
            {
                if (rect[0] <= mx)
                    to_process.push_back(child_data(l,t, hx, hy, fc+0, nd.depth+1));
                if (rect[2] > mx)
                    to_process.push_back(child_data(r,t, hx, hy, fc+1, nd.depth+1));
            }
            if (rect[3] > my)
            {
                if (rect[0] <= mx)
                    to_process.push_back(child_data(l,b, hx, hy, fc+2, nd.depth+1));
                if (rect[2] > mx)
                    to_process.push_back(child_data(r,b, hx, hy, fc+3, nd.depth+1));
            }
        }
    }
    return leaves;
}

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

я не знаю, почему многие люди предпочитают кэшировать AABBs: будь то удобство программирования или если это действительно быстрее в их случаях. Тем не менее, для структур данных, которые равномерно распределяются по центру, как обычные Квадри и октри, я бы предложил измерить влияние опуская AABBs и вычисляя их на лету. Возможно, вы удивитесь. Конечно, имеет смысл хранить AABBs для структур, которые не разделяются равномерно, как KD-деревья и BVHs, а также свободные четырехстворчатые деревья.

С Плавающей Точкой

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

Центрированный AABBs

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

Прилежащей Детей

это также ключ, и если мы вернемся к узлу rep:

struct QuadNode
{
    int32_t first_child;
    ...
};

нам не нужно хранить массив детей, потому что все 4 ребенка прилежащей:

first_child+0 = index to 1st child (TL)
first_child+1 = index to 2nd child (TR)
first_child+2 = index to 3nd child (BL)
first_child+3 = index to 4th child (BR)

это не только значительно уменьшает пропуски кэша при обходе, но и позволяет нам значительно сжимать наши узлы, что дополнительно уменьшает пропуски кэша, сохраняя только один 32-битный индекс (4 байта) вместо массива 4 (16 байтов).

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

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

enter image description here

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

Отложенная Очистка

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

вместо этого я делаю отложенную очистку, как это:

void Quadtree::cleanup()
{
    // Only process the root if it's not a leaf.
    SmallList<int> to_process;
    if (nodes[0].count == -1)
        to_process.push_back(0);

    while (to_process.size() > 0)
    {
        const int node_index = to_process.pop_back();
        QuadNode& node = nodes[node_index];

        // Loop through the children.
        int num_empty_leaves = 0;
        for (int j=0; j < 4; ++j)
        {
            const int child_index = node.first_child + j;
            const QuadNode& child = nodes[child_index];

            // Increment empty leaf count if the child is an empty 
            // leaf. Otherwise if the child is a branch, add it to
            // the stack to be processed in the next iteration.
            if (child.count == 0)
                ++num_empty_leaves;
            else if (child.count == -1)
                to_process.push_back(child_index);
        }

        // If all the children were empty leaves, remove them and 
        // make this node the new empty leaf.
        if (num_empty_leaves == 4)
        {
            // Push all 4 children to the free list.
            nodes[node.first_child].first_child = free_node;
            free_node = node.first_child;

            // Make this node the new empty leaf.
            node.first_child = -1;
            node.count = 0;
        }
    }
}

это вызывается в конце каждого кадра после перемещения всех агентов. Причина, по которой я делаю этот вид отложенного удаления пустых листовых узлов в нескольких итерациях и не все сразу в процессе удаления одного элемента является этот элемент A может перейти к узлу N2, сделав N1 пустой. Однако, элемент B может, в том же кадре, перейти к N1, делая его занятым снова.

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

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

давайте посмотрим на отложенную очистку визуально:

enter image description here

начиная с этого, предположим, мы удаляем некоторые элементы из дерева, оставляя нам 4 пустых листа:

enter image description here

в этот момент, если мы позвоним cleanup, он удалит 4 листа, если найдет 4 пустых дочерних листа и превратит родителя в пустой лист, например Итак:

enter image description here

предположим, мы удалим еще несколько элементов:

enter image description here

... а потом позвони cleanup еще раз:

enter image description here

если мы назовем его еще раз, мы в конечном итоге с этим:

enter image description here

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

TBH, я первоначально применил эту технику "отложенной очистки" в игре DOS, которую я написал на C из чистого лень! Я не хотел беспокоиться о спуске вниз по дереву, удалении элементов, а затем удалении узлов снизу вверх, потому что я изначально написал дерево в пользу обхода сверху вниз (а не сверху вниз и обратно) и действительно думал, что это ленивое решение было компромиссом производительности (жертвуя оптимальной производительностью, чтобы быстрее реализоваться). Однако много лет спустя я фактически приступил к реализации удаления quadtree способами, которые сразу же начали удалять узлы и, к моему удивлению, я на самом деле значительно замедлил его с более непредсказуемыми и заикающимися частотами кадров. Отложенная очистка, несмотря на то, что изначально была вдохновлена моей ленью программиста, на самом деле (и случайно) была очень эффективной оптимизацией для динамических сцен.

Односвязные списки индексов для элементов

для элементов, я использую это представление:

// Represents an element in the quadtree.
struct QuadElt
{
    // Stores the ID for the element (can be used to
    // refer to external data).
    int id;

    // Stores the rectangle for the element.
    int x1, y1, x2, y2;
};

// Represents an element node in the quadtree.
struct QuadEltNode
{
    // Points to the next element in the leaf node. A value of -1 
    // indicates the end of the list.
    int next;

    // Stores the element index.
    int element;
};

я использую "узел элемента", который отделен от "элемент." Элемент вставляется только один раз в quadtree независимо от того, сколько ячеек он занимает. Однако для каждой ячейки, которую он занимает, вставляется "узел элемента", который индексирует этот элемент.

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

SmallList<T>

О, я должен упомянуть об этом. Естественно, это помогает, если вы не выделяете кучу только для хранения временного стека узлов для нерекурсивного обхода. SmallList<T> похож на vector<T> за исключением того, что он не будет включать распределение кучи, пока вы не вставите в него более 128 элементов. Это похоже на оптимизацию строк SBO в стандартном lib c++. Это то, что я реализовал и использовал на протяжении веков и это очень помогает убедиться, что вы используете стек, когда это возможно.

Представление Дерева

вот представление самого квадрата:

struct Quadtree
{
    // Stores all the elements in the quadtree.
    FreeList<QuadElt> elts;

    // Stores all the element nodes in the quadtree.
    FreeList<QuadEltNode> elt_nodes;

    // Stores all the nodes in the quadtree. The first node in this
    // sequence is always the root.
    std::vector<QuadNode> nodes;

    // Stores the quadtree extents.
    QuadCRect root_rect;

    // Stores the first free node in the quadtree to be reclaimed as 4
    // contiguous nodes at once. A value of -1 indicates that the free
    // list is empty, at which point we simply insert 4 nodes to the
    // back of the nodes array.
    int free_node;

    // Stores the maximum depth allowed for the quadtree.
    int max_depth;
};

как указано выше, мы храним один прямоугольник для корня (root_rect). Все суб-прямоугольникам вычисляются на лету. Все узлы хранятся в непрерывном массиве (std::vector<QuadNode>) вместе с элементами и узлами элементов (в FreeList<T>).

FreeList<T>

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

/// Provides an indexed free list with constant-time removals from anywhere
/// in the list without invalidating indices. T must be trivially constructible 
/// and destructible.
template <class T>
class FreeList
{
public:
    /// Creates a new free list.
    FreeList();

    /// Inserts an element to the free list and returns an index to it.
    int insert(const T& element);

    // Removes the nth element from the free list.
    void erase(int n);

    // Removes all elements from the free list.
    void clear();

    // Returns the range of valid indices.
    int range() const;

    // Returns the nth element.
    T& operator[](int n);

    // Returns the nth element.
    const T& operator[](int n) const;

private:
    union FreeElement
    {
        T element;
        int next;
    };
    std::vector<FreeElement> data;
    int first_free;
};

template <class T>
FreeList<T>::FreeList(): first_free(-1)
{
}

template <class T>
int FreeList<T>::insert(const T& element)
{
    if (first_free != -1)
    {
        const int index = first_free;
        first_free = data[first_free].next;
        data[index].element = element;
        return index;
    }
    else
    {
        FreeElement fe;
        fe.element = element;
        data.push_back(fe);
        return static_cast<int>(data.size() - 1);
    }
}

template <class T>
void FreeList<T>::erase(int n)
{
    data[n].next = first_free;
    first_free = n;
}

template <class T>
void FreeList<T>::clear()
{
    data.clear();
    first_free = -1;
}

template <class T>
int FreeList<T>::range() const
{
    return static_cast<int>(data.size());
}

template <class T>
T& FreeList<T>::operator[](int n)
{
    return data[n].element;
}

template <class T>
const T& FreeList<T>::operator[](int n) const
{
    return data[n].element;
}

я тот, который работает с нетривиальными типами и предоставляет итераторы и т. д., Но гораздо более вовлечен. В эти дни я больше работаю с тривиально конструктивными/разрушаемыми структурами C-стиля (ТОЛЬКО используя нетривиальные пользовательские типы для высокоуровневых вещей).

Максимальная Глубина Дерева

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

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

столкновение и запросы

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

for each element in scene:
     use quad tree to check for collision against other elements

это очень простой, но проблема с этим подходом заключается в том, что первый элемент сцены может находиться в совершенно ином месте в мире, чем второй. В результате пути, по которым мы спускаемся с квадрата, могут быть совершенно спорадическими. Мы могли бы пройти один путь к листу, а затем снова захотеть пройти по тому же пути для первого элемента, как, скажем, для 50 000-го элемента. К этому времени узлы, участвующие в этом пути, возможно, уже были вытеснены из кэша ЦП. Поэтому я рекомендую сделать это так:

traversed = {}
gather quadtree leaves
for each leaf in leaves:
{
     for each element in leaf:
     {
          if not traversed[element]:
          {
              use quad tree to check for collision against other elements
              traversed[element] = true                  
          }
     }
}

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

общие неэффективности: вещи, чтобы избежать

хотя есть много способов снять кожу с кошки и достичь эффективного решения, есть общий способ достичь очень неэффективно решение. И я встретил свою долю очень неэффективно quadtrees, KD деревья и octrees в моей карьере, работая в VFX. Мы говорим о гигабайте использования памяти только для разделения сетки со 100k треугольники, занимая 30 секунд для построения, когда достойная реализация должна быть в состоянии делать то же самое сотни раз в секунду и просто взять несколько мегабайт. Есть много людей, которые взбивают их, чтобы решить проблемы, которые являются теоретическими волшебниками, но не обращали особого внимания на эффективность памяти.

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

struct Node
{
     ...

     // Stores the elements in the node.
     List<Element> elements;
};

и List<Element> может быть список в Python,ArrayList в Java или C#, std::vector в C++ и т. д.: некоторая структура данных, которая управляет собственной памятью / ресурсами.

проблема здесь в том, что, хотя такие контейнеры очень эффективно реализованы для хранения большого количества элементов,все из них на всех языках чрезвычайно неэффективны, если вы создаете экземпляр boatload из них только для хранения нескольких элементов в каждом. Одна из причин заключается в том, что метаданные контейнера имеют тенденцию быть довольно взрывоопасными в использовании памяти на таком гранулированном уровне одного узла дерева. Контейнеру может потребоваться сохранить размер, емкость, указатель / ссылку на данные, которые он выделяет, и т. д., И все для обобщенной цели, чтобы он мог использовать 64-разрядные целые числа для размера и емкости. В результате метаданные только для пустого контейнера могут составлять 24 байта, что уже в 3 раза больше, чем полное представление узла, которое я предложил, и это как раз для пустого контейнера конструированного для того чтобы хранить элементы в листьях.

кроме того, каждый контейнер часто хочет либо heap/GC-allocate при вставке, либо требует еще более предварительно выделенной памяти (в этот момент может потребоваться 64 байта только для самого контейнера). Так что либо становится медленным из-за всех распределений (обратите внимание, что распределения GC действительно быстрые изначально в некоторых реализациях, таких как JVM, но это только для начального цикла пакета Eden), либо потому, что мы несет лодку загрузки кэша пропускает, потому что узлы настолько огромны, что едва ли любой вписывается в нижние уровни кэша ЦП при обходе, или оба.

тем не менее, это очень естественная склонность и имеет интуитивный смысл, поскольку мы говорим об этих структурах теоретически, используя язык, как,"элементы хранятся в листьях" что предполагает хранение контейнера элементов в листовых узлах. К сожалению, он имеет взрывоопасную стоимость с точки зрения использования памяти и обработка. Поэтому избегайте этого, если вы хотите создать что-то разумно эффективное. Сделайте Node поделиться и указать (см.) или индексировать память, выделенную и сохраненную для всего дерева, а не для каждого отдельного узла. На самом деле элементы не должны храниться в листьях.

элементы должны храниться в дерево и листовые узлы должны index или выберите пункт те элементы.

вывод

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

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

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


2. Основы

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

Хорошо, предположим, у нас есть куча таких элементов в пространстве:

enter image description here

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

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

разделить

Итак, что мы можем сделать с этой проблемой? Один из простых подходов-разделить пространство поиска (например, экран) на фиксированное количество ячеек, например:

enter image description here

Теперь предположим, что вы хотите найти элемент под курсором мыши в позиции (cx, cy). В этом случае, все, что нам нужно сделать, это проверить элементы в клетке под мышкой курсор:

grid_x = floor(cx / cell_size);
grid_y = floor(cy / cell_size);
for each element in cell(grid_x, grid_y):
{
     if element is under cx,cy:
         do something with element (hover highlight it, e.g)
}

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

grid_x1 = floor(element.x1 / cell_size);
grid_y1 = floor(element.y1 / cell_size);
grid_x2 = floor(element.x2 / cell_size);
grid_y2 = floor(element.y2 / cell_size);

for grid_y = grid_y1, grid_y2:
{
     for grid_x = grid_x1, grid_x2:
     {
         for each other_element in cell(grid_x, grid_y):
         {
             if element != other_element and collide(element, other_element):
             {
                 // The two elements intersect. Do something in response
                 // to the collision.
             }
         }
     }
}

и я рекомендую людям начать таким образом, разделив пространство/экран на фиксированное количество ячеек сетки, таких как 10x10, или 100x100, или даже 1000x1000. Некоторые люди могут подумать, что 1000x1000 будет взрывоопасным в использовании памяти, но вы можете заставить каждую ячейку требовать только 4 байта с 32-битными целыми числами, например Итак:

enter image description here

... в этот момент даже миллион клеток занимает менее 4 мегабайт.

обратная сторона сетки с фиксированным разрешением

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

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

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

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

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

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

деревья квадрантов

таким образом, quadtrees являются одним из решений этой проблемы. Вместо использования фиксированной сетки разрешения, так сказать, они адаптируют разрешение на основе некоторых критериев, разделяя/разделяя на 4 дочерние ячейки для увеличения разрешения. Например, мы можем сказать, что ячейка должна разделиться, если в данной ячейке больше 2 детей. В таком случае это:

enter image description here

В конечном итоге становится это:

enter image description here

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

enter image description here

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

реализация

Итак, как мы реализуем одну из этих штучек? Ну, это дерево в конце дня, и 4-арное дерево в этом. Итак, мы начинаем с понятия корневого узла с 4 дочерними элементами, но в настоящее время они null/nil, и корень также является листом на данный момент:

enter image description here

вставка

давайте начнем вставка некоторых элементов и снова для простоты предположим, что узел должен разделяться, когда он имеет более 2 элементов. Поэтому мы вставим элемент, и когда мы вставляем элемент, мы должны вставить его в листья (ячейки), в которых он принадлежит. В этом случае у нас есть только один, корневой узел / ячейка, и поэтому мы вставим его туда:

enter image description here

... и вставим еще:

enter image description here

... и еще другой:

enter image description here

и теперь у нас есть более 2 элементов в листовом узле. Теперь он должен расколоться. На этом этапе мы создаем 4 дочерних элемента в листовой узел( в данном случае наш корень), а затем передаем элементы из расщепляемого листа (корня) в соответствующие квадранты на основе области/ячейки, которую каждый элемент занимает в пространстве:

enter image description here

давайте вставим еще один элемент, и снова соответствующий лист, в котором он принадлежит:

enter image description here

... и еще:

enter image description here

и теперь у нас снова более 2 элементов в листе, поэтому мы должны разделить его на 4 квадранта (дети):

enter image description here

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

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

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

Удаление Элементов

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

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

вывод

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


4. Свободная Дерева Квадрантов

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

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

enter image description here

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

enter image description here

Свободный Quadtree

хорошо, так что же такое свободные quadtrees? Это в основном квадранты, узлы которых не полностью разделены по центру на четыре четных квадранта. Вместо этого их aabbs (ограничивающие прямоугольники) могут перекрываться и быть больше или даже меньше, чем то, что вы получите, если вы разделите узел идеально по центру на 4 квадранта.

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

struct LooseQuadNode
{
    // Stores the AABB of the node.
    float rect[4];

    // Stores the negative index to the first child for branches or the 
    // positive index to the element list for leaves.
    int children;
};

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

в чем смысл?

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

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

плюсы:

  • даже самый гигантский агент должен быть вставлен только в один листовой узел и займет не больше памяти, чем самый маленький. В результате он хорошо подходит для сцен с элементами, размеры которых дико варьируются от одного к другому, и это то, что я был стресс-тестированием в 250k agent demo выше.
  • использует меньше памяти на элемент, особенно огромный элементы.

плюсы:

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

Дорогие Запросы

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

enter image description here

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

что мне нравится о свободном quadtree является то, что вы можете чувствовать себя в безопасности, даже если у вас есть boatload массивных элементов в дополнение к boatload самых маленьких элементов. Массивные элементы не потребуют больше памяти, чем маленькие. В результате, если бы я писал видеоигру с массивным миром и хотел просто бросить все в один центральный пространственный индекс, чтобы ускорить все, не беспокоясь об иерархии структур данных, как я обычно делаю, тогда свободные quadtrees и свободные octrees могли бы быть идеально сбалансированная как "одна Центральная универсальная структура данных, если мы собираемся использовать только одну для всего динамического мира".

Использование Памяти

С точки зрения использования памяти, в то время как элементы занимают меньше памяти (особенно массивные), узлы занимают немного больше по сравнению с моими реализациями, где узлам даже не нужно хранить AABB. Я обнаружил в целом в различных тестовых случаях, в том числе со многими гигантскими элементами, что свободное Квадри чтобы взять немного больше памяти с ее мясистыми узлами (~33% больше, как грубая оценка). Тем не менее, он работает лучше, чем реализация quadtree в моем первоначальном ответе.

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

теория

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

давайте вставим один элемент:

enter image description here

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

enter image description here

мы расширяем AABBs узлов мы траверсируем как мы вставим (на этот раз просто корень) по AABBs элементов мы вставим. Давайте вставим другой, и предположим, что узлы должны разделяться, когда они содержат более 2 элементов:

enter image description here

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

enter image description here

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

enter image description here

здесь вы можете видеть, что вставка этого элемента не только расширила прямоугольник нижнего левого дочернего элемента, но и корень (мы расширяем все AABBs вдоль пути, который мы вставляем). Вставим еще:

enter image description here

в этом случае у нас снова есть 3 элемента в листовом узле, поэтому мы должны разделить:

enter image description here

просто так. Что теперь? о том круге в левом нижнем углу? Похоже, она пересекает 2 квадранта. Однако мы рассматриваем только одну точку элемента (например, его центр), чтобы определить квадрант, к которому он принадлежит. Таким образом, этот круг фактически вставляется только в нижний левый квадрант.

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

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

Обновление AABBs

Так что это может приведите к вопросу, когда обновляются AABBs? Если мы только расширим AABBs при вставке элементов, они будут просто расти все больше и больше. Как мы сжимаем их, когда элементы удаляются? Есть много способов решить эту проблему, но я делаю это, обновляя ограничительные рамки всей иерархии в этом методе "очистки", описанном в моем исходном ответе. Это кажется достаточно быстрым (даже не отображается как точка доступа).

по сравнению с сетями

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

вывод

Так что свободен quadtrees в двух словах, и он в основном имеет логику вставки/удаления обычного quadtree, который просто хранит точки, за исключением того, что он расширяет/обновляет AABBs по пути. Для поиска мы в конечном итоге пересекаем все дочерние узлы, прямоугольники которых пересекают нашу область поиска.

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


Грязный Трюк: Равномерные Размеры

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

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

в "humanish размер" единицы не различаются, что большая по размеру. Хоббит может быть четырех футов ростом и немного коренастый, орк может быть 6'4. Есть некоторая разница, но это не эпическая разница. Это не разница на порядок.

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

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

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

Хранение Только Одного Пункта

эта вторая часть сложная, но представьте, что у нас есть такой случай, как это:

enter image description here

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

enter image description here

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

enter image description here

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

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

Огромный Оптимизации

поэтому огромная оптимизации можно применить для разделения содержимого на 3 различные типы:

  1. динамический набор (постоянно перемещающийся и анимирующий) с общей верхней границей, такой как люди, орки, эльфы и хоббиты. Мы в основном помещаем ограничивающую коробку/сферу одинакового размера вокруг всех этих агентов. Здесь вы можете использовать плотное представление, такое как плотное квадратное дерево или плотная сетка, и оно будет хранить только одну точку для каждого элемента. Вы также можете использовать другой экземпляр этой же структуры для супер маленьких элементов, таких как феи и огоньки с Различный общий размер верхней границы.
  2. динамический набор больше, чем любая предсказуемая верхняя граница обычного случая, как драконы и энты с очень необычными размерами. Здесь вы можете использовать свободное представление, такое как свободное четырехугольник или моя "свободная/плотная двойная сетка".
  3. статический набор, где вы можете позволить себе структуры, которые занимают больше времени для создания или очень неэффективны для обновления, например, quadtree для статических данных, которые хранят все совершенно непрерывным способом. В этом случае не имеет значения, насколько неэффективна структура данных для обновления при условии, что она обеспечивает самые быстрые поисковые запросы, так как вы никогда не будете обновлять ее. Вы можете использовать это для элементов в вашем мире, таких как замки, баррикады и валуны.

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


3. Портативная Реализация C

я надеюсь, что люди не возражают против другого ответа, но я исчерпал лимит 30k. Сегодня я думал о том, что мой первый ответ был не очень языковым-агностическим. Я говорил о стратегиях распределения mem, шаблонах классов и т. д., и не все языки допускают такие вещи.

поэтому я потратил некоторое время, думая об эффективной реализации, которая почти универсально применима (исключением будут функциональные языки). Так что я закончил перенос моего quadtree на C таким образом, что все, что ему нужно, это массивы int сохранить все.

результат не очень хорош, но должен работать очень эффективно на любом языке, который позволяет хранить непрерывные массивы int. Для Python есть такие библиотеки, как ndarray на numpy. Для JS есть типизированные массивы. Для Java и C#, мы можем использовать int массивы (не Integer, те не гарантированы, что хранятся смежно и они используют очень больше mem чем старая добрая int).

C IntList

поэтому я использую один вспомогательная структура построенная на int массивы для всего quadtree, чтобы сделать его как можно проще для порта на другие языки. Он сочетает в себе стек/бесплатный список. Это все, что нам нужно для эффективной реализации всего, о чем говорилось в другом ответе.

#ifndef INT_LIST_H
#define INT_LIST_H

#ifdef __cplusplus
    #define IL_FUNC extern "C"
#else
    #define IL_FUNC
#endif

typedef struct IntList IntList;
enum {il_fixed_cap = 128};

struct IntList
{
    // Stores a fixed-size buffer in advance to avoid requiring
    // a heap allocation until we run out of space.
    int fixed[il_fixed_cap];

    // Points to the buffer used by the list. Initially this will
    // point to 'fixed'.
    int* data;

    // Stores how many integer fields each element has.
    int num_fields;

    // Stores the number of elements in the list.
    int num;

    // Stores the capacity of the array.
    int cap;

    // Stores an index to the free element or -1 if the free list
    // is empty.
    int free_element;
};

// ---------------------------------------------------------------------------------
// List Interface
// ---------------------------------------------------------------------------------
// Creates a new list of elements which each consist of integer fields.
// 'num_fields' specifies the number of integer fields each element has.
IL_FUNC void il_create(IntList* il, int num_fields);

// Destroys the specified list.
IL_FUNC void il_destroy(IntList* il);

// Returns the number of elements in the list.
IL_FUNC int il_size(const IntList* il);

// Returns the value of the specified field for the nth element.
IL_FUNC int il_get(const IntList* il, int n, int field);

// Sets the value of the specified field for the nth element.
IL_FUNC void il_set(IntList* il, int n, int field, int val);

// Clears the specified list, making it empty.
IL_FUNC void il_clear(IntList* il);

// ---------------------------------------------------------------------------------
// Stack Interface (do not mix with free list usage; use one or the other)
// ---------------------------------------------------------------------------------
// Inserts an element to the back of the list and returns an index to it.
IL_FUNC int il_push_back(IntList* il);

// Removes the element at the back of the list.
IL_FUNC void il_pop_back(IntList* il);

// ---------------------------------------------------------------------------------
// Free List Interface (do not mix with stack usage; use one or the other)
// ---------------------------------------------------------------------------------
// Inserts an element to a vacant position in the list and returns an index to it.
IL_FUNC int il_insert(IntList* il);

// Removes the nth element in the list.
IL_FUNC void il_erase(IntList* il, int n);

#endif

#include "IntList.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void il_create(IntList* il, int num_fields)
{
    il->data = il->fixed;
    il->num = 0;
    il->cap = il_fixed_cap;
    il->num_fields = num_fields;
    il->free_element = -1;
}

void il_destroy(IntList* il)
{
    // Free the buffer only if it was heap allocated.
    if (il->data != il->fixed)
        free(il->data);
}

void il_clear(IntList* il)
{
    il->num = 0;
    il->free_element = -1;
}

int il_size(const IntList* il)
{
    return il->num;
}

int il_get(const IntList* il, int n, int field)
{
    assert(n >= 0 && n < il->num);
    return il->data[n*il->num_fields + field];
}

void il_set(IntList* il, int n, int field, int val)
{
    assert(n >= 0 && n < il->num);
    il->data[n*il->num_fields + field] = val;
}

int il_push_back(IntList* il)
{
    const int new_pos = (il->num+1) * il->num_fields;

    // If the list is full, we need to reallocate the buffer to make room
    // for the new element.
    if (new_pos > il->cap)
    {
        // Use double the size for the new capacity.
        const int new_cap = new_pos * 2;

        // If we're pointing to the fixed buffer, allocate a new array on the
        // heap and copy the fixed buffer contents to it.
        if (il->cap == il_fixed_cap)
        {
            il->data = malloc(new_cap * sizeof(*il->data));
            memcpy(il->data, il->fixed, sizeof(il->fixed));
        }
        else
        {
            // Otherwise reallocate the heap buffer to the new size.
            il->data = realloc(il->data, new_cap * sizeof(*il->data));
        }
        // Set the old capacity to the new capacity.
        il->cap = new_cap;
    }
    return il->num++;
}

void il_pop_back(IntList* il)
{
    // Just decrement the list size.
    assert(il->num > 0);
    --il->num;
}

int il_insert(IntList* il)
{
    // If there's a free index in the free list, pop that and use it.
    if (il->free_element != -1)
    {
        const int index = il->free_element;
        const int pos = index * il->num_fields;

        // Set the free index to the next free index.
        il->free_element = il->data[pos];

        // Return the free index.
        return index;
    }
    // Otherwise insert to the back of the array.
    return il_push_back(il);
}

void il_erase(IntList* il, int n)
{
    // Push the element to the free list.
    const int pos = n * il->num_fields;
    il->data[pos] = il->free_element;
    il->free_element = n;
}

Использование IntList

использование этой структуры данных для реализации не все дает самый красивый код. Вместо доступа к элементам и полям, как это:

elements[n].field = elements[n].field + 1;

... мы в конечном итоге делаем так:

il_set(&elements, n, idx_field, il_get(&elements, n, idx_field) + 1);

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

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

C Quadtree

хорошо, поэтому, используя приведенную выше структуру данных, вот quadtree в C:

#ifndef QUADTREE_H
#define QUADTREE_H

#include "IntList.h"

#ifdef __cplusplus
    #define QTREE_FUNC extern "C"
#else
    #define QTREE_FUNC
#endif

typedef struct Quadtree Quadtree;

struct Quadtree
{
    // Stores all the nodes in the quadtree. The first node in this
    // sequence is always the root.
    IntList nodes;

    // Stores all the elements in the quadtree.
    IntList elts;

    // Stores all the element nodes in the quadtree.
    IntList enodes;

    // Stores the quadtree extents.
    int root_mx, root_my, root_sx, root_sy;

    // Maximum allowed elements in a leaf before the leaf is subdivided/split unless
    // the leaf is at the maximum allowed tree depth.
    int max_elements;

    // Stores the maximum depth allowed for the quadtree.
    int max_depth;

    // Temporary buffer used for queries.
    char* temp;

    // Stores the size of the temporary buffer.
    int temp_size;
};

// Function signature used for traversing a tree node.
typedef void QtNodeFunc(Quadtree* qt, void* user_data, int node, int depth, int mx, int my, int sx, int sy);

// Creates a quadtree with the requested extents, maximum elements per leaf, and maximum tree depth.
QTREE_FUNC void qt_create(Quadtree* qt, int width, int height, int max_elements, int max_depth);

// Destroys the quadtree.
QTREE_FUNC void qt_destroy(Quadtree* qt);

// Inserts a new element to the tree.
// Returns an index to the new element.
QTREE_FUNC int qt_insert(Quadtree* qt, int id, float x1, float y1, float x2, float y2);

// Removes the specified element from the tree.
QTREE_FUNC void qt_remove(Quadtree* qt, int element);

// Cleans up the tree, removing empty leaves.
QTREE_FUNC void qt_cleanup(Quadtree* qt);

// Outputs a list of elements found in the specified rectangle.
QTREE_FUNC void qt_query(Quadtree* qt, IntList* out, float x1, float y1, float x2, float y2, int omit_element);

// Traverses all the nodes in the tree, calling 'branch' for branch nodes and 'leaf' 
// for leaf nodes.
QTREE_FUNC void qt_traverse(Quadtree* qt, void* user_data, QtNodeFunc* branch, QtNodeFunc* leaf);

#endif

#include "Quadtree.h"
#include <stdlib.h>

enum
{
    // ----------------------------------------------------------------------------------------
    // Element node fields:
    // ----------------------------------------------------------------------------------------
    enode_num = 2,

    // Points to the next element in the leaf node. A value of -1 
    // indicates the end of the list.
    enode_idx_next = 0,

    // Stores the element index.
    enode_idx_elt = 1,

    // ----------------------------------------------------------------------------------------
    // Element fields:
    // ----------------------------------------------------------------------------------------
    elt_num = 5,

    // Stores the rectangle encompassing the element.
    elt_idx_lft = 0, elt_idx_top = 1, elt_idx_rgt = 2, elt_idx_btm = 3,

    // Stores the ID of the element.
    elt_idx_id = 4,

    // ----------------------------------------------------------------------------------------
    // Node fields:
    // ----------------------------------------------------------------------------------------
    node_num = 2,

    // Points to the first child if this node is a branch or the first element
    // if this node is a leaf.
    node_idx_fc = 0,

    // Stores the number of elements in the node or -1 if it is not a leaf.
    node_idx_num = 1,

    // ----------------------------------------------------------------------------------------
    // Node data fields:
    // ----------------------------------------------------------------------------------------
    nd_num = 6,

    // Stores the extents of the node using a centered rectangle and half-size.
    nd_idx_mx = 0, nd_idx_my = 1, nd_idx_sx = 2, nd_idx_sy = 3,

    // Stores the index of the node.
    nd_idx_index = 4,

    // Stores the depth of the node.
    nd_idx_depth = 5,
};

static void node_insert(Quadtree* qt, int index, int depth, int mx, int my, int sx, int sy, int element);

static int floor_int(float val)
{
    return (int)val;
}

static int intersect(int l1, int t1, int r1, int b1,
                     int l2, int t2, int r2, int b2)
{
    return l2 <= r1 && r2 >= l1 && t2 <= b1 && b2 >= t1;
}

void leaf_insert(Quadtree* qt, int node, int depth, int mx, int my, int sx, int sy, int element)
{
    // Insert the element node to the leaf.
    const int nd_fc = il_get(&qt->nodes, node, node_idx_fc);
    il_set(&qt->nodes, node, node_idx_fc, il_insert(&qt->enodes));
    il_set(&qt->enodes, il_get(&qt->nodes, node, node_idx_fc), enode_idx_next, nd_fc);
    il_set(&qt->enodes, il_get(&qt->nodes, node, node_idx_fc), enode_idx_elt, element);

    // If the leaf is full, split it.
    if (il_get(&qt->nodes, node, node_idx_num) == qt->max_elements && depth < qt->max_depth)
    {
        int fc = 0, j = 0;
        IntList elts = {0};
        il_create(&elts, 1);

        // Transfer elements from the leaf node to a list of elements.
        while (il_get(&qt->nodes, node, node_idx_fc) != -1)
        {
            const int index = il_get(&qt->nodes, node, node_idx_fc);
            const int next_index = il_get(&qt->enodes, index, enode_idx_next);
            const int elt = il_get(&qt->enodes, index, enode_idx_elt);

            // Pop off the element node from the leaf and remove it from the qt.
            il_set(&qt->nodes, node, node_idx_fc, next_index);
            il_erase(&qt->enodes, index);

            // Insert element to the list.
            il_set(&elts, il_push_back(&elts), 0, elt);
        }

        // Start by allocating 4 child nodes.
        fc = il_insert(&qt->nodes);
        il_insert(&qt->nodes);
        il_insert(&qt->nodes);
        il_insert(&qt->nodes);
        il_set(&qt->nodes, node, node_idx_fc, fc);

        // Initialize the new child nodes.
        for (j=0; j < 4; ++j)
        {
            il_set(&qt->nodes, fc+j, node_idx_fc, -1);
            il_set(&qt->nodes, fc+j, node_idx_num, 0);
        }

        // Transfer the elements in the former leaf node to its new children.
        il_set(&qt->nodes, node, node_idx_num, -1);
        for (j=0; j < il_size(&elts); ++j)
            node_insert(qt, node, depth, mx, my, sx, sy, il_get(&elts, j, 0));
        il_destroy(&elts);
    }
    else
    {
        // Increment the leaf element count.
        il_set(&qt->nodes, node, node_idx_num, il_get(&qt->nodes, node, node_idx_num) + 1);
    }
}

static void push_node(IntList* nodes, int nd_index, int nd_depth, int nd_mx, int nd_my, int nd_sx, int nd_sy)
{
    const int back_idx = il_push_back(nodes);
    il_set(nodes, back_idx, nd_idx_mx, nd_mx);
    il_set(nodes, back_idx, nd_idx_my, nd_my);
    il_set(nodes, back_idx, nd_idx_sx, nd_sx);
    il_set(nodes, back_idx, nd_idx_sy, nd_sy);
    il_set(nodes, back_idx, nd_idx_index, nd_index);
    il_set(nodes, back_idx, nd_idx_depth, nd_depth);
}

static void find_leaves(IntList* out, const Quadtree* qt, int node, int depth, 
                        int mx, int my, int sx, int sy, 
                        int lft, int top, int rgt, int btm)
{
    IntList to_process = {0};
    il_create(&to_process, nd_num);
    push_node(&to_process, node, depth, mx, my, sx, sy);

    while (il_size(&to_process) > 0)
    {
        const int back_idx = il_size(&to_process) - 1;
        const int nd_mx = il_get(&to_process, back_idx, nd_idx_mx);
        const int nd_my = il_get(&to_process, back_idx, nd_idx_my);
        const int nd_sx = il_get(&to_process, back_idx, nd_idx_sx);
        const int nd_sy = il_get(&to_process, back_idx, nd_idx_sy);
        const int nd_index = il_get(&to_process, back_idx, nd_idx_index);
        const int nd_depth = il_get(&to_process, back_idx, nd_idx_depth);
        il_pop_back(&to_process);

        // If this node is a leaf, insert it to the list.
        if (il_get(&qt->nodes, nd_index, node_idx_num) != -1)
            push_node(out, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
        else
        {
            // Otherwise push the children that intersect the rectangle.
            const int fc = il_get(&qt->nodes, nd_index, node_idx_fc);
            const int hx = nd_sx >> 1, hy = nd_sy >> 1;
            const int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;

            if (top <= nd_my)
            {
                if (lft <= nd_mx)
                    push_node(&to_process, fc+0, nd_depth+1, l,t,hx,hy);
                if (rgt > nd_mx)
                    push_node(&to_process, fc+1, nd_depth+1, r,t,hx,hy);
            }
            if (btm > nd_my)
            {
                if (lft <= nd_mx)
                    push_node(&to_process, fc+2, nd_depth+1, l,b,hx,hy);
                if (rgt > nd_mx)
                    push_node(&to_process, fc+3, nd_depth+1, r,b,hx,hy);
            }
        }
    }
    il_destroy(&to_process);
}

static void node_insert(Quadtree* qt, int index, int depth, int mx, int my, int sx, int sy, int element)
{
    // Find the leaves and insert the element to all the leaves found.
    int j = 0;
    IntList leaves = {0};

    const int lft = il_get(&qt->elts, element, elt_idx_lft);
    const int top = il_get(&qt->elts, element, elt_idx_top);
    const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
    const int btm = il_get(&qt->elts, element, elt_idx_btm);

    il_create(&leaves, nd_num);
    find_leaves(&leaves, qt, index, depth, mx, my, sx, sy, lft, top, rgt, btm);
    for (j=0; j < il_size(&leaves); ++j)
    {
        const int nd_mx = il_get(&leaves, j, nd_idx_mx);
        const int nd_my = il_get(&leaves, j, nd_idx_my);
        const int nd_sx = il_get(&leaves, j, nd_idx_sx);
        const int nd_sy = il_get(&leaves, j, nd_idx_sy);
        const int nd_index = il_get(&leaves, j, nd_idx_index);
        const int nd_depth = il_get(&leaves, j, nd_idx_depth);
        leaf_insert(qt, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy, element);
    }
    il_destroy(&leaves);
}

void qt_create(Quadtree* qt, int width, int height, int max_elements, int max_depth)
{
    qt->max_elements = max_elements;
    qt->max_depth = max_depth;
    qt->temp = 0;
    qt->temp_size = 0;
    il_create(&qt->nodes, node_num);
    il_create(&qt->elts, elt_num);
    il_create(&qt->enodes, enode_num);

    // Insert the root node to the qt.
    il_insert(&qt->nodes);
    il_set(&qt->nodes, 0, node_idx_fc, -1);
    il_set(&qt->nodes, 0, node_idx_num, 0);

    // Set the extents of the root node.
    qt->root_mx = width >> 1;
    qt->root_my = height >> 1;
    qt->root_sx = qt->root_mx;
    qt->root_sy = qt->root_my;
}

void qt_destroy(Quadtree* qt)
{
    il_destroy(&qt->nodes);
    il_destroy(&qt->elts);
    il_destroy(&qt->enodes);
    free(qt->temp);
}

int qt_insert(Quadtree* qt, int id, float x1, float y1, float x2, float y2)
{
    // Insert a new element.
    const int new_element = il_insert(&qt->elts);

    // Set the fields of the new element.
    il_set(&qt->elts, new_element, elt_idx_lft, floor_int(x1));
    il_set(&qt->elts, new_element, elt_idx_top, floor_int(y1));
    il_set(&qt->elts, new_element, elt_idx_rgt, floor_int(x2));
    il_set(&qt->elts, new_element, elt_idx_btm, floor_int(y2));
    il_set(&qt->elts, new_element, elt_idx_id, id);

    // Insert the element to the appropriate leaf node(s).
    node_insert(qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, new_element);
    return new_element;
}

void qt_remove(Quadtree* qt, int element)
{
    // Find the leaves.
    int j = 0;
    IntList leaves = {0};

    const int lft = il_get(&qt->elts, element, elt_idx_lft);
    const int top = il_get(&qt->elts, element, elt_idx_top);
    const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
    const int btm = il_get(&qt->elts, element, elt_idx_btm);

    il_create(&leaves, nd_num);
    find_leaves(&leaves, qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, lft, top, rgt, btm);

    // For each leaf node, remove the element node.
    for (j=0; j < il_size(&leaves); ++j)
    {
        const int nd_index = il_get(&leaves, j, nd_idx_index);

        // Walk the list until we find the element node.
        int node_index = il_get(&qt->nodes, nd_index, node_idx_fc);
        int prev_index = -1;
        while (node_index != -1 && il_get(&qt->enodes, node_index, enode_idx_elt) != element)
        {
            prev_index = node_index;
            node_index = il_get(&qt->enodes, node_index, enode_idx_next);
        }

        if (node_index != -1)
        {
            // Remove the element node.
            const int next_index = il_get(&qt->enodes, node_index, enode_idx_next);
            if (prev_index == -1)
                il_set(&qt->nodes, nd_index, node_idx_fc, next_index);
            else
                il_set(&qt->enodes, prev_index, enode_idx_next, next_index);
            il_erase(&qt->enodes, node_index);

            // Decrement the leaf element count.
            il_set(&qt->nodes, nd_index, node_idx_num, il_get(&qt->nodes, nd_index, node_idx_num)-1);
        }
    }
    il_destroy(&leaves);

    // Remove the element.
    il_erase(&qt->elts, element);
}

void qt_query(Quadtree* qt, IntList* out, float x1, float y1, float x2, float y2, int omit_element)
{
    // Find the leaves that intersect the specified query rectangle.
    int j = 0;
    IntList leaves = {0};
    const int elt_cap = il_size(&qt->elts);

    const int qlft = floor_int(x1);
    const int qtop = floor_int(y1);
    const int qrgt = floor_int(x2);
    const int qbtm = floor_int(y2);

    if (qt->temp_size < elt_cap)
    {
        qt->temp_size = elt_cap;
        qt->temp = realloc(qt->temp, qt->temp_size * sizeof(*qt->temp));
        memset(qt->temp, 0, qt->temp_size * sizeof(*qt->temp));
    }

    // For each leaf node, look for elements that intersect.
    il_create(&leaves, nd_num);
    find_leaves(&leaves, qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, qlft, qtop, qrgt, qbtm);

    il_clear(out);
    for (j=0; j < il_size(&leaves); ++j)
    {
        const int nd_index = il_get(&leaves, j, nd_idx_index);

        // Walk the list and add elements that intersect.
        int elt_node_index = il_get(&qt->nodes, nd_index, node_idx_fc);
        while (elt_node_index != -1)
        {
            const int element = il_get(&qt->enodes, elt_node_index, enode_idx_elt);
            const int lft = il_get(&qt->elts, element, elt_idx_lft);
            const int top = il_get(&qt->elts, element, elt_idx_top);
            const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
            const int btm = il_get(&qt->elts, element, elt_idx_btm);
            if (!qt->temp[element] && element != omit_element && intersect(qlft,qtop,qrgt,qbtm, lft,top,rgt,btm))
            {
                il_set(out, il_push_back(out), 0, element);
                qt->temp[element] = 1;
            }
            elt_node_index = il_get(&qt->enodes, elt_node_index, enode_idx_next);
        }
    }
    il_destroy(&leaves);

    // Unmark the elements that were inserted.
    for (j=0; j < il_size(out); ++j)
        qt->temp[il_get(out, j, 0)] = 0;
}

void qt_cleanup(Quadtree* qt)
{
    IntList to_process = {0};
    il_create(&to_process, 1);

    // Only process the root if it's not a leaf.
    if (il_get(&qt->nodes, 0, node_idx_num) == -1)
    {
        // Push the root index to the stack.
        il_set(&to_process, il_push_back(&to_process), 0, 0);
    }

    while (il_size(&to_process) > 0)
    {
        // Pop a node from the stack.
        const int node = il_get(&to_process, il_size(&to_process)-1, 0);
        const int fc = il_get(&qt->nodes, node, node_idx_fc);
        int num_empty_leaves = 0;
        int j = 0;
        il_pop_back(&to_process);

        // Loop through the children.
        for (j=0; j < 4; ++j)
        {
            const int child = fc + j;

            // Increment empty leaf count if the child is an empty 
            // leaf. Otherwise if the child is a branch, add it to
            // the stack to be processed in the next iteration.
            if (il_get(&qt->nodes, child, node_idx_num) == 0)
                ++num_empty_leaves;
            else if (il_get(&qt->nodes, child, node_idx_num) == -1)
            {
                // Push the child index to the stack.
                il_set(&to_process, il_push_back(&to_process), 0, child);
            }
        }

        // If all the children were empty leaves, remove them and 
        // make this node the new empty leaf.
        if (num_empty_leaves == 4)
        {
            // Remove all 4 children in reverse order so that they 
            // can be reclaimed on subsequent insertions in proper
            // order.
            il_erase(&qt->nodes, fc + 3);
            il_erase(&qt->nodes, fc + 2);
            il_erase(&qt->nodes, fc + 1);
            il_erase(&qt->nodes, fc + 0);

            // Make this node the new empty leaf.
            il_set(&qt->nodes, node, node_idx_fc, -1);
            il_set(&qt->nodes, node, node_idx_num, 0);
        }
    }
    il_destroy(&to_process);
}

void qt_traverse(Quadtree* qt, void* user_data, QtNodeFunc* branch, QtNodeFunc* leaf)
{
    IntList to_process = {0};
    il_create(&to_process, nd_num);
    push_node(&to_process, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy);

    while (il_size(&to_process) > 0)
    {
        const int back_idx = il_size(&to_process) - 1;
        const int nd_mx = il_get(&to_process, back_idx, nd_idx_mx);
        const int nd_my = il_get(&to_process, back_idx, nd_idx_my);
        const int nd_sx = il_get(&to_process, back_idx, nd_idx_sx);
        const int nd_sy = il_get(&to_process, back_idx, nd_idx_sy);
        const int nd_index = il_get(&to_process, back_idx, nd_idx_index);
        const int nd_depth = il_get(&to_process, back_idx, nd_idx_depth);
        const int fc = il_get(&qt->nodes, nd_index, node_idx_fc);
        il_pop_back(&to_process);

        if (il_get(&qt->nodes, nd_index, node_idx_num) == -1)
        {
            // Push the children of the branch to the stack.
            const int hx = nd_sx >> 1, hy = nd_sy >> 1;
            const int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
            push_node(&to_process, fc+0, nd_depth+1, l,t, hx,hy);
            push_node(&to_process, fc+1, nd_depth+1, r,t, hx,hy);
            push_node(&to_process, fc+2, nd_depth+1, l,b, hx,hy);
            push_node(&to_process, fc+3, nd_depth+1, r,b, hx,hy);
            if (branch)
                branch(qt, user_data, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
        }
        else if (leaf)
            leaf(qt, user_data, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
    }
    il_destroy(&to_process);
}

Временное Заключение

это не такой уж отличный ответ, но я постараюсь вернуться и продолжить его редактирование. Однако, приведенный выше код должен быть очень эффективен практически на любом языке, который позволяет непрерывных массивов простых чисел. До тех пор, пока у нас есть эта гарантия смежности, мы можем придумать очень удобный для кэша quadtree, который использует очень маленький след mem.

пожалуйста см. первоначальный ответ для деталей на общем подходе.


5. Свободная / плотная двойная сетка с агентами 500k

enter image description here

выше немного GIF, показывающий 500,000 агентов, отскакивающих друг от друга каждый раз, когда шаг, используя новую структуру данных "loose/tight grid", которую я был вдохновлен создать после написания ответа о loose quadtrees. Я попробую разобраться, как это работает.

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

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

как ни странно, мне также потребовалось самое короткое время для реализации (всего 2 часа, в то время как я провел 5 или 6 часов на свободном quadtree), а также требует наименьшего количества кода (и, возможно, имеет самый простой код). Хотя это может быть просто потому, что я накопил так много опыта реализации сеток.

Свободная / Плотная Двойная Сетка

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

// Ideally use multiplication here with inv_cell_w or inv_cell_h.
int cell_x = clamp(floor(elt_x / cell_w), 0, num_cols-1);
int cell_y = clamp(floor(ely_y / cell_h), 0, num_rows-1);
int cell_idx = cell_y*num_rows + cell_x;
// Insert element to cell at 'cell_idx' and expand the loose cell's AABB.

ячейки хранят элементы и AABBs, как это:

struct LGridLooseCell
{
    // Stores the index to the first element using an indexed SLL.
    int head;

    // Stores the extents of the grid cell relative to the upper-left corner
    // of the grid which expands and shrinks with the elements inserted and 
    // removed.
    float l, t, r, b;
};

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

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

tx1 = clamp(floor(search_x1 / cell_w), 0, num_cols-1);
tx2 = clamp(floor(search_x2 / cell_w), 0, num_cols-1);
ty1 = clamp(floor(search_y1 / cell_h), 0, num_rows-1);
ty2 = clamp(floor(search_y2 / cell_h), 0, num_rows-1);

for ty = ty1, ty2:
{
    trow = ty * num_cols
    for tx = tx1, tx2:
    {
        tight_cell = tight_cells[trow + tx];
        for each loose_cell in tight_cell:
        {
            if loose_cell intersects search area:
            {
                for each element in loose_cell:
                {
                    if element intersects search area:
                        add element to query results
                }
            }
        }
    }
}

жесткие ячейки хранят односвязный список индексов свободных ячеек, например:

struct LGridLooseCellNode
{
    // Points to the next loose cell node in the tight cell.
    int next;

    // Stores an index to the loose cell.
    int cell_idx;
};

struct LGridTightCell
{
    // Stores the index to the first loose cell node in the tight cell using 
    // an indexed SLL.
    int head;
};

и вуаля, это основная идея "свободной двойной сетки". Когда мы вставляем элемент, мы расширяем AABB свободной ячейки так же, как мы делаем для свободного квадрата. Однако мы также вставляем свободную ячейку в плотная сетка на основе ее прямоугольника (и ее можно вставить в несколько ячеек).

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

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

Я рекомендую попробовать эту идею "свободных сеток". Возможно, это намного проще реализовать, чем quadtrees и loose quadtrees и, что более важно, оптимизировать, поскольку он сразу же поддается удобной для кэша компоновке памяти. В качестве супер крутого бонуса, если вы можете заранее предвидеть количество агентов в своем мире, это является 100% абсолютно стабильным и сразу же с точки зрения использования памяти, так как элемент всегда занимает ровно одну ячейку, а общее количество ячеек фиксировано (поскольку они не подразделяются/разделяются). В результате использование памяти очень стабильно. Это может быть огромным бонусом для определенного оборудования и программного обеспечения, где вы хотите заранее выделить всю память и знать, что использование памяти никогда не поднимется выше этой точки.

также очень легко заставить его работать с SIMD, чтобы сделать несколько когерентных запросы одновременно с векторизованным кодом (в дополнение к многопоточности), так как обход, если мы можем его даже назвать, плоский (это просто постоянный поиск в индексе ячейки, который включает некоторую арифметику). В результате довольно легко применять стратегии оптимизации, подобные пакетам ray, которые Intel применяет к их ядру raytracing / BVH (Embree) для тестирования нескольких когерентных лучей одновременно( в нашем случае это были бы "пакеты агентов" для столкновения), за исключением таких причудливый / сложный код, так как сетка "обход" намного проще.

об использовании и эффективности памяти

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

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

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

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


4. Java IntList

Я надеюсь, что люди не возражают, что я публикую третий ответ, но я снова исчерпал лимит символов. Я закончил перенос кода C во втором ответе на Java. Порт Java может быть проще ссылаться на людей, переносящих на объектно-ориентированные языки.

class IntList
{
    private int data[] = new int[128];
    private int num_fields = 0;
    private int num = 0;
    private int cap = 128;
    private int free_element = -1;

    // Creates a new list of elements which each consist of integer fields.
    // 'start_num_fields' specifies the number of integer fields each element has.
    public IntList(int start_num_fields)
    {
        num_fields = start_num_fields;
    }

    // Returns the number of elements in the list.
    int size()
    {
        return num;
    }

    // Returns the value of the specified field for the nth element.
    int get(int n, int field)
    {
        assert n >= 0 && n < num && field >= 0 && field < num_fields;
        return data[n*num_fields + field];
    }

    // Sets the value of the specified field for the nth element.
    void set(int n, int field, int val)
    {
        assert n >= 0 && n < num && field >= 0 && field < num_fields;
        data[n*num_fields + field] = val;
    }

    // Clears the list, making it empty.
    void clear()
    {
        num = 0;
        free_element = -1;
    }

    // Inserts an element to the back of the list and returns an index to it.
    int pushBack()
    {
        final int new_pos = (num+1) * num_fields;

        // If the list is full, we need to reallocate the buffer to make room
        // for the new element.
        if (new_pos > cap)
        {
            // Use double the size for the new capacity.
            final int new_cap = new_pos * 2;

            // Allocate new array and copy former contents.
            int new_array[] = new int[new_cap];
            System.arraycopy(data, 0, new_array, 0, cap);
            data = new_array;

            // Set the old capacity to the new capacity.
            cap = new_cap;
        }
        return num++;
    }

    // Removes the element at the back of the list.
    void popBack()
    {
        // Just decrement the list size.
        assert num > 0;
        --num;
    }

    // Inserts an element to a vacant position in the list and returns an index to it.
    int insert()
    {
        // If there's a free index in the free list, pop that and use it.
        if (free_element != -1)
        {
            final int index = free_element;
            final int pos = index * num_fields;

            // Set the free index to the next free index.
            free_element = data[pos];

            // Return the free index.
            return index;
        }
        // Otherwise insert to the back of the array.
        return pushBack();
    }

    // Removes the nth element in the list.
    void erase(int n)
    {
        // Push the element to the free list.
        final int pos = n * num_fields;
        data[pos] = free_element;
        free_element = n;
    }
}

Java Quadtree

и вот quadtree в Java (извините, если это не очень идиоматично; я не писал Java около десяти лет или около того и забыл многие вещи):

interface IQtVisitor
{
    // Called when traversing a branch node.
    // (mx, my) indicate the center of the node's AABB.
    // (sx, sy) indicate the half-size of the node's AABB.
    void branch(Quadtree qt, int node, int depth, int mx, int my, int sx, int sy);

    // Called when traversing a leaf node.
    // (mx, my) indicate the center of the node's AABB.
    // (sx, sy) indicate the half-size of the node's AABB.
    void leaf(Quadtree qt, int node, int depth, int mx, int my, int sx, int sy);
}

class Quadtree
{
    // Creates a quadtree with the requested extents, maximum elements per leaf, and maximum tree depth.
    Quadtree(int width, int height, int start_max_elements, int start_max_depth)
    {
        max_elements = start_max_elements;
        max_depth = start_max_depth;

        // Insert the root node to the qt.
        nodes.insert();
        nodes.set(0, node_idx_fc, -1);
        nodes.set(0, node_idx_num, 0);

        // Set the extents of the root node.
        root_mx = width / 2;
        root_my = height / 2;
        root_sx = root_mx;
        root_sy = root_my;
    }

    // Outputs a list of elements found in the specified rectangle.
    public int insert(int id, float x1, float y1, float x2, float y2)
    {
        // Insert a new element.
        final int new_element = elts.insert();

        // Set the fields of the new element.
        elts.set(new_element, elt_idx_lft, floor_int(x1));
        elts.set(new_element, elt_idx_top, floor_int(y1));
        elts.set(new_element, elt_idx_rgt, floor_int(x2));
        elts.set(new_element, elt_idx_btm, floor_int(y2));
        elts.set(new_element, elt_idx_id, id);

        // Insert the element to the appropriate leaf node(s).
        node_insert(0, 0, root_mx, root_my, root_sx, root_sy, new_element);
        return new_element;
    }

    // Removes the specified element from the tree.
    public void remove(int element)
    {
        // Find the leaves.
        final int lft = elts.get(element, elt_idx_lft);
        final int top = elts.get(element, elt_idx_top);
        final int rgt = elts.get(element, elt_idx_rgt);
        final int btm = elts.get(element, elt_idx_btm);
        IntList leaves = find_leaves(0, 0, root_mx, root_my, root_sx, root_sy, lft, top, rgt, btm);

        // For each leaf node, remove the element node.
        for (int j=0; j < leaves.size(); ++j)
        {
            final int nd_index = leaves.get(j, nd_idx_index);

            // Walk the list until we find the element node.
            int node_index = nodes.get(nd_index, node_idx_fc);
            int prev_index = -1;
            while (node_index != -1 && enodes.get(node_index, enode_idx_elt) != element)
            {
                prev_index = node_index;
                node_index = enodes.get(node_index, enode_idx_next);
            }

            if (node_index != -1)
            {
                // Remove the element node.
                final int next_index = enodes.get(node_index, enode_idx_next);
                if (prev_index == -1)
                    nodes.set(nd_index, node_idx_fc, next_index);
                else
                    enodes.set(prev_index, enode_idx_next, next_index);
                enodes.erase(node_index);

                // Decrement the leaf element count.
                nodes.set(nd_index, node_idx_num, nodes.get(nd_index, node_idx_num)-1);
            }
        }

        // Remove the element.
        elts.erase(element);
    }

    // Cleans up the tree, removing empty leaves.
    public void cleanup()
    {
        IntList to_process = new IntList(1);

        // Only process the root if it's not a leaf.
        if (nodes.get(0, node_idx_num) == -1)
        {
            // Push the root index to the stack.
            to_process.set(to_process.pushBack(), 0, 0);
        }

        while (to_process.size() > 0)
        {
            // Pop a node from the stack.
            final int node = to_process.get(to_process.size()-1, 0);
            final int fc = nodes.get(node, node_idx_fc);
            int num_empty_leaves = 0;
            to_process.popBack();

            // Loop through the children.
            for (int j=0; j < 4; ++j)
            {
                final int child = fc + j;

                // Increment empty leaf count if the child is an empty 
                // leaf. Otherwise if the child is a branch, add it to
                // the stack to be processed in the next iteration.
                if (nodes.get(child, node_idx_num) == 0)
                    ++num_empty_leaves;
                else if (nodes.get(child, node_idx_num) == -1)
                {
                    // Push the child index to the stack.
                    to_process.set(to_process.pushBack(), 0, child);
                }
            }

            // If all the children were empty leaves, remove them and 
            // make this node the new empty leaf.
            if (num_empty_leaves == 4)
            {
                // Remove all 4 children in reverse order so that they 
                // can be reclaimed on subsequent insertions in proper
                // order.
                nodes.erase(fc + 3);
                nodes.erase(fc + 2);
                nodes.erase(fc + 1);
                nodes.erase(fc + 0);

                // Make this node the new empty leaf.
                nodes.set(node, node_idx_fc, -1);
                nodes.set(node, node_idx_num, 0);
            }
        }
    }

    // Returns a list of elements found in the specified rectangle.
    public IntList query(float x1, float y1, float x2, float y2)
    {
        return query(x1, y1, x2, y2, -1);
    }

    // Returns a list of elements found in the specified rectangle excluding the
    // specified element to omit.
    public IntList query(float x1, float y1, float x2, float y2, int omit_element)
    {
        IntList out = new IntList(1);

        // Find the leaves that intersect the specified query rectangle.
        final int qlft = floor_int(x1);
        final int qtop = floor_int(y1);
        final int qrgt = floor_int(x2);
        final int qbtm = floor_int(y2);
        IntList leaves = find_leaves(0, 0, root_mx, root_my, root_sx, root_sy, qlft, qtop, qrgt, qbtm);

        if (temp_size < elts.size())
        {
            temp_size = elts.size();
            temp = new boolean[temp_size];;
        }

        // For each leaf node, look for elements that intersect.
        for (int j=0; j < leaves.size(); ++j)
        {
            final int nd_index = leaves.get(j, nd_idx_index);

            // Walk the list and add elements that intersect.
            int elt_node_index = nodes.get(nd_index, node_idx_fc);
            while (elt_node_index != -1)
            {
                final int element = enodes.get(elt_node_index, enode_idx_elt);
                final int lft = elts.get(element, elt_idx_lft);
                final int top = elts.get(element, elt_idx_top);
                final int rgt = elts.get(element, elt_idx_rgt);
                final int btm = elts.get(element, elt_idx_btm);
                if (!temp[element] && element != omit_element && intersect(qlft,qtop,qrgt,qbtm, lft,top,rgt,btm))
                {
                    out.set(out.pushBack(), 0, element);
                    temp[element] = true;
                }
                elt_node_index = enodes.get(elt_node_index, enode_idx_next);
            }
        }

        // Unmark the elements that were inserted.
        for (int j=0; j < out.size(); ++j)
            temp[out.get(j, 0)] = false;
        return out;
    }

    // Traverses all the nodes in the tree, calling 'branch' for branch nodes and 'leaf' 
    // for leaf nodes.
    public void traverse(IQtVisitor visitor)
    {
        IntList to_process = new IntList(nd_num);
        pushNode(to_process, 0, 0, root_mx, root_my, root_sx, root_sy);

        while (to_process.size() > 0)
        {
            final int back_idx = to_process.size() - 1;
            final int nd_mx = to_process.get(back_idx, nd_idx_mx);
            final int nd_my = to_process.get(back_idx, nd_idx_my);
            final int nd_sx = to_process.get(back_idx, nd_idx_sx);
            final int nd_sy = to_process.get(back_idx, nd_idx_sy);
            final int nd_index = to_process.get(back_idx, nd_idx_index);
            final int nd_depth = to_process.get(back_idx, nd_idx_depth);
            final int fc = nodes.get(nd_index, node_idx_fc);
            to_process.popBack();

            if (nodes.get(nd_index, node_idx_num) == -1)
            {
                // Push the children of the branch to the stack.
                final int hx = nd_sx >> 1, hy = nd_sy >> 1;
                final int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
                pushNode(to_process, fc+0, nd_depth+1, l,t, hx,hy);
                pushNode(to_process, fc+1, nd_depth+1, r,t, hx,hy);
                pushNode(to_process, fc+2, nd_depth+1, l,b, hx,hy);
                pushNode(to_process, fc+3, nd_depth+1, r,b, hx,hy);
                visitor.branch(this, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
            }
            else
                visitor.leaf(this, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
        }
    }

    private static int floor_int(float val)
    {
        return (int)val;
    }

    private static boolean intersect(int l1, int t1, int r1, int b1,
                                     int l2, int t2, int r2, int b2)
    {
        return l2 <= r1 && r2 >= l1 && t2 <= b1 && b2 >= t1;
    }

    private static void pushNode(IntList nodes, int nd_index, int nd_depth, int nd_mx, int nd_my, int nd_sx, int nd_sy)
    {
        final int back_idx = nodes.pushBack();
        nodes.set(back_idx, nd_idx_mx, nd_mx);
        nodes.set(back_idx, nd_idx_my, nd_my);
        nodes.set(back_idx, nd_idx_sx, nd_sx);
        nodes.set(back_idx, nd_idx_sy, nd_sy);
        nodes.set(back_idx, nd_idx_index, nd_index);
        nodes.set(back_idx, nd_idx_depth, nd_depth);
    }

    private IntList find_leaves(int node, int depth, 
                                int mx, int my, int sx, int sy, 
                                int lft, int top, int rgt, int btm)
    {
        IntList leaves = new IntList(nd_num);
        IntList to_process = new IntList(nd_num);
        pushNode(to_process, node, depth, mx, my, sx, sy);

        while (to_process.size() > 0)
        {
            final int back_idx = to_process.size() - 1;
            final int nd_mx = to_process.get(back_idx, nd_idx_mx);
            final int nd_my = to_process.get(back_idx, nd_idx_my);
            final int nd_sx = to_process.get(back_idx, nd_idx_sx);
            final int nd_sy = to_process.get(back_idx, nd_idx_sy);
            final int nd_index = to_process.get(back_idx, nd_idx_index);
            final int nd_depth = to_process.get(back_idx, nd_idx_depth);
            to_process.popBack();

            // If this node is a leaf, insert it to the list.
            if (nodes.get(nd_index, node_idx_num) != -1)
                pushNode(leaves, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
            else
            {
                // Otherwise push the children that intersect the rectangle.
                final int fc = nodes.get(nd_index, node_idx_fc);
                final int hx = nd_sx / 2, hy = nd_sy / 2;
                final int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;

                if (top <= nd_my)
                {
                    if (lft <= nd_mx)
                        pushNode(to_process, fc+0, nd_depth+1, l,t,hx,hy);
                    if (rgt > nd_mx)
                        pushNode(to_process, fc+1, nd_depth+1, r,t,hx,hy);
                }
                if (btm > nd_my)
                {
                    if (lft <= nd_mx)
                        pushNode(to_process, fc+2, nd_depth+1, l,b,hx,hy);
                    if (rgt > nd_mx)
                        pushNode(to_process, fc+3, nd_depth+1, r,b,hx,hy);
                }
            }
        }
        return leaves;
    }

    private void node_insert(int index, int depth, int mx, int my, int sx, int sy, int element)
    {
        // Find the leaves and insert the element to all the leaves found.
        final int lft = elts.get(element, elt_idx_lft);
        final int top = elts.get(element, elt_idx_top);
        final int rgt = elts.get(element, elt_idx_rgt);
        final int btm = elts.get(element, elt_idx_btm);
        IntList leaves = find_leaves(index, depth, mx, my, sx, sy, lft, top, rgt, btm);

        for (int j=0; j < leaves.size(); ++j)
        {
            final int nd_mx = leaves.get(j, nd_idx_mx);
            final int nd_my = leaves.get(j, nd_idx_my);
            final int nd_sx = leaves.get(j, nd_idx_sx);
            final int nd_sy = leaves.get(j, nd_idx_sy);
            final int nd_index = leaves.get(j, nd_idx_index);
            final int nd_depth = leaves.get(j, nd_idx_depth);
            leaf_insert(nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy, element);
        }
    }

    private void leaf_insert(int node, int depth, int mx, int my, int sx, int sy, int element)
    {
        // Insert the element node to the leaf.
        final int nd_fc = nodes.get(node, node_idx_fc);
        nodes.set(node, node_idx_fc, enodes.insert());
        enodes.set(nodes.get(node, node_idx_fc), enode_idx_next, nd_fc);
        enodes.set(nodes.get(node, node_idx_fc), enode_idx_elt, element);

        // If the leaf is full, split it.
        if (nodes.get(node, node_idx_num) == max_elements && depth < max_depth)
        {
            // Transfer elements from the leaf node to a list of elements.
            IntList elts = new IntList(1);
            while (nodes.get(node, node_idx_fc) != -1)
            {
                final int index = nodes.get(node, node_idx_fc);
                final int next_index = enodes.get(index, enode_idx_next);
                final int elt = enodes.get(index, enode_idx_elt);

                // Pop off the element node from the leaf and remove it from the qt.
                nodes.set(node, node_idx_fc, next_index);
                enodes.erase(index);

                // Insert element to the list.
                elts.set(elts.pushBack(), 0, elt);
            }

            // Start by allocating 4 child nodes.
            final int fc = nodes.insert();
            nodes.insert();
            nodes.insert();
            nodes.insert();
            nodes.set(node, node_idx_fc, fc);

            // Initialize the new child nodes.
            for (int j=0; j < 4; ++j)
            {
                nodes.set(fc+j, node_idx_fc, -1);
                nodes.set(fc+j, node_idx_num, 0);
            }

            // Transfer the elements in the former leaf node to its new children.
            nodes.set(node, node_idx_num, -1);
            for (int j=0; j < elts.size(); ++j)
                node_insert(node, depth, mx, my, sx, sy, elts.get(j, 0));
        }
        else
        {
            // Increment the leaf element count.
            nodes.set(node, node_idx_num, nodes.get(node, node_idx_num) + 1);
        }
    }


    // ----------------------------------------------------------------------------------------
    // Element node fields:
    // ----------------------------------------------------------------------------------------
    // Points to the next element in the leaf node. A value of -1 
    // indicates the end of the list.
    static final int enode_idx_next = 0;

    // Stores the element index.
    static final int enode_idx_elt = 1;

    // Stores all the element nodes in the quadtree.
    private IntList enodes = new IntList(2);

    // ----------------------------------------------------------------------------------------
    // Element fields:
    // ----------------------------------------------------------------------------------------
    // Stores the rectangle encompassing the element.
    static final int elt_idx_lft = 0, elt_idx_top = 1, elt_idx_rgt = 2, elt_idx_btm = 3;

    // Stores the ID of the element.
    static final int elt_idx_id = 4;

    // Stores all the elements in the quadtree.
    private IntList elts = new IntList(5);

    // ----------------------------------------------------------------------------------------
    // Node fields:
    // ----------------------------------------------------------------------------------------
    // Points to the first child if this node is a branch or the first element
    // if this node is a leaf.
    static final int node_idx_fc = 0;

    // Stores the number of elements in the node or -1 if it is not a leaf.
    static final int node_idx_num = 1;

    // Stores all the nodes in the quadtree. The first node in this
    // sequence is always the root.
    private IntList nodes = new IntList(2);

    // ----------------------------------------------------------------------------------------
    // Node data fields:
    // ----------------------------------------------------------------------------------------
    static final int nd_num = 6;

    // Stores the extents of the node using a centered rectangle and half-size.
    static final int nd_idx_mx = 0, nd_idx_my = 1, nd_idx_sx = 2, nd_idx_sy = 3;

    // Stores the index of the node.
    static final int nd_idx_index = 4;

    // Stores the depth of the node.
    static final int nd_idx_depth = 5;

    // ----------------------------------------------------------------------------------------
    // Data Members
    // ----------------------------------------------------------------------------------------
    // Temporary buffer used for queries.
    private boolean temp[];

    // Stores the size of the temporary buffer.
    private int temp_size = 0;

    // Stores the quadtree extents.
    private int root_mx, root_my, root_sx, root_sy;

    // Maximum allowed elements in a leaf before the leaf is subdivided/split unless
    // the leaf is at the maximum allowed tree depth.
    private int max_elements;

    // Stores the maximum depth allowed for the quadtree.
    private int max_depth;
}

Временное Заключение

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

пожалуйста см. первоначальный ответ для деталей на общем подходе.