web-dev-qa-db-fra.com

Implémentation efficace (et bien expliquée) d'un Quadtree pour la détection de collision 2D

J'ai travaillé sur l'ajout d'un Quadtree à un programme que j'écris, et je ne peux m'empêcher de remarquer qu'il y a peu de tutoriels bien expliqués/performants pour l'implémentation que je recherche.

Plus précisément, une liste des méthodes et du pseudocode pour savoir comment les implémenter (ou simplement une description de leurs processus) qui sont couramment utilisés dans un Quadtree (récupérer, insérer, supprimer, etc.) est ce que je recherche, avec peut-être quelques conseils pour améliorer les performances. C'est pour la détection de collision, il serait donc préférable de l'expliquer avec des rectangles 2D à l'esprit, car ce sont les objets qui seront stockés.

28
Zimri Leisher

1. Quadtrees efficaces

D'accord, je vais essayer. D'abord un teaser pour montrer les résultats de ce que je proposerai impliquant 20 000 agents (juste quelque chose que j'ai concocté très rapidement pour cette question spécifique):

enter image description here

Le GIF a une fréquence d'images extrêmement réduite et une résolution nettement inférieure pour s'adapter au maximum de 2 Mo pour ce site. Voici une vidéo si vous voulez voir la chose presque à pleine vitesse: https://streamable.com/3pgmn .

Et un GIF avec 100k, même si je devais tellement le tripoter et je devais désactiver les lignes quadtree (ne semblait pas vouloir compresser autant avec elles) ainsi que changer la façon dont les agents avaient l'air de l'obtenir. tenir dans 2 mégaoctets (je souhaite faire un GIF était aussi simple que de coder un quadtree):

enter image description here

La simulation avec 20 000 agents prend environ 3 mégaoctets de RAM. Je peux également gérer facilement 100 000 agents plus petits sans sacrifier les fréquences d'images, même si cela conduit à un peu de désordre sur l'écran au point où vous pouvez à peine voir ce qui se passe comme dans le GIF ci-dessus. Tout cela fonctionne sur un seul thread sur mon i7 et je passe presque la moitié du temps selon VTune à dessiner ces trucs à l'écran (en utilisant simplement des instructions scalaires de base pour tracer les choses un pixel à la fois dans le CPU).

Et voici une vidéo avec 100 000 agents bien qu'il soit difficile de voir ce qui se passe. C'est une sorte de grosse vidéo et je n'ai trouvé aucun moyen décent de la compresser sans que la vidéo entière se transforme en bouillie (il faudra peut-être d'abord la télécharger ou la mettre en cache pour la voir en streaming à un FPS raisonnable). Avec 100 000 agents, la simulation prend environ 4,5 mégaoctets de RAM et l'utilisation de la mémoire est très stable après avoir exécuté la simulation pendant environ 5 secondes (cesse de monter ou de descendre car elle cesse d'allouer du tas). - Même chose au ralenti .

Quadtree efficace pour la détection de collision

Très bien, donc en fait les quadtrees ne sont pas ma structure de données préférée à cet effet. J'ai tendance à préférer une hiérarchie de grille, comme une grille grossière pour le monde, une grille plus fine pour une région, et une grille encore plus fine pour une sous-région (3 niveaux fixes de grilles denses, et aucun arbre impliqué), avec rangée- optimisations basées sur une ligne qui ne contient aucune entité sera désallouée et transformée en un pointeur nul, ainsi que des régions ou sous-régions complètement vides transformées en null. Bien que cette implémentation simple du quadtree fonctionnant dans un seul thread puisse gérer 100 000 agents sur mon i7 à plus de 60 FPS, j'ai mis en œuvre des grilles qui peuvent gérer quelques millions d'agents se rebondissant à chaque trame sur un matériel plus ancien (un i3). De plus, j'ai toujours aimé la façon dont les grilles facilitaient la prédiction de la quantité de mémoire dont elles auront besoin, car elles ne subdivisent pas les cellules. Mais je vais essayer de voir comment implémenter un quadtree raisonnablement efficace.

Notez que je n'entrerai pas dans la théorie complète de la structure des données. Je suppose que vous le savez déjà et que vous souhaitez améliorer les performances. Je vais également aborder ma façon personnelle de résoudre ce problème qui semble surpasser la plupart des solutions que je trouve en ligne pour mes cas, mais il existe de nombreuses façons décentes et ces solutions sont adaptées à mes cas d'utilisation (entrées très importantes avec tout ce qui bouge chaque image pour les effets visuels dans les films et la télévision). D'autres personnes optimisent probablement pour différents cas d'utilisation. En ce qui concerne les structures d'indexation spatiale en particulier, je pense vraiment que l'efficacité de la solution vous en dit plus sur l'implémenteur que sur la structure des données. Les mêmes stratégies que je proposerai pour accélérer les choses s'appliquent également en 3 dimensions avec octrees.

Représentation des nœuds

Alors tout d'abord, couvrons la représentation des nœuds:

// 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;
};

C'est un total de 8 octets, et c'est très important car c'est un élément clé de la vitesse. J'utilise en fait un plus petit (6 octets par nœud) mais je laisse cela comme un exercice au lecteur.

Vous pouvez probablement vous passer de count. J'inclus cela pour les cas pathologiques afin d'éviter de traverser linéairement les éléments et de les compter chaque fois qu'un nœud foliaire peut se diviser. Dans la plupart des cas, un nœud ne doit pas stocker autant d'éléments. Cependant, je travaille dans les effets visuels et les cas pathologiques ne sont pas nécessairement rares. Vous pouvez rencontrer des artistes créant du contenu avec une cargaison de points coïncidents, des polygones massifs qui couvrent toute la scène, etc., donc je finis par stocker un count.

Où sont les AABB?

Donc, l'une des premières choses que les gens pourraient se demander, c'est où se trouvent les boîtes englobantes (rectangles) pour les nœuds. Je ne les stocke pas. Je les calcule à la volée. Je suis un peu surpris que la plupart des gens ne le fassent pas dans le code que j'ai vu. Pour moi, ils ne sont stockés qu'avec la structure arborescente (essentiellement un seul AABB pour la racine).

Cela pourrait sembler plus coûteux de les calculer à la volée, mais réduire l'utilisation de la mémoire d'un nœud peut réduire proportionnellement les échecs de cache lorsque vous traversez l'arborescence, et ces réductions des échecs de cache ont tendance à être plus importantes que d'avoir à faire quelques décalages de bits et quelques ajouts/soustractions pendant la traversée. La traversée ressemble à ceci:

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;
}

L'omission des AABB est l'une des choses les plus inhabituelles que je fais (je continue de chercher d'autres personnes qui le font juste pour trouver un pair et échouer), mais j'ai mesuré l'avant et après et cela a considérablement réduit les temps, du moins sur très grandes entrées, pour compacter substantiellement le nœud à quatre arbres et simplement calculer les AABB à la volée pendant la traversée. L'espace et le temps ne sont pas toujours diamétralement opposés. Parfois, réduire l'espace signifie également réduire le temps compte tenu de la quantité de performances dominées par la hiérarchie de la mémoire de nos jours. J'ai même accéléré certaines opérations réelles appliquées à des entrées massives en compressant les données pour réduire l'utilisation de la mémoire et décompresser à la volée.

Je ne sais pas pourquoi beaucoup de gens choisissent de mettre en cache les AABB: que ce soit pour la commodité de la programmation ou si c'est vraiment plus rapide dans leur cas. Pourtant, pour les structures de données qui se répartissent uniformément au centre, comme les quadtre et les octrees réguliers, je suggère de mesurer l'impact de l'omission des AABB et de les calculer à la volée. Vous pourriez être assez surpris. Bien sûr, il est logique de stocker les AABB pour les structures qui ne se divisent pas également comme les arbres Kd et les BVH ainsi que les arbres quadratiques lâches.

Virgule flottante

Je n'utilise pas de virgule flottante pour les index spatiaux et c'est peut-être pour cela que je vois des performances améliorées en calculant les AABB à la volée avec des décalages à droite pour la division par 2 et ainsi de suite. Cela dit, au moins SPFP semble vraiment rapide de nos jours. Je ne sais pas car je n'ai pas mesuré la différence. J'utilise simplement des entiers de préférence même si je travaille généralement avec des entrées à virgule flottante (sommets de maillage, particules, etc.). Je viens de les convertir en coordonnées entières dans le but de partitionner et d'effectuer des requêtes spatiales. Je ne sais pas s'il y a un avantage majeur à faire de la vitesse. C'est juste une habitude et une préférence car je trouve plus facile de raisonner sur des entiers sans avoir à penser à dénormalisé FP et tout ça.

AABB centrés

Bien que je ne stocke qu'un cadre de délimitation pour la racine, il est utile d'utiliser une représentation qui stocke un centre et une demi-taille pour les nœuds tout en utilisant une représentation gauche/haut/droite/bas pour les requêtes afin de minimiser la quantité d'arithmétique impliquée.

Enfants contigus

C'est également la clé, et si nous nous référons à la représentation du nœud:

struct QuadNode
{
    int32_t first_child;
    ...
};

Nous n'avons pas besoin de stocker un tableau d'enfants car les 4 enfants sont contigus:

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)

Cela réduit non seulement de manière significative les échecs de cache lors de la traversée, mais nous permet également de réduire considérablement nos nœuds, ce qui réduit davantage les échecs de cache, en stockant un seul index 32 bits (4 octets) au lieu d'un tableau de 4 (16 octets).

Cela signifie que si vous devez transférer des éléments vers seulement quelques quadrants d'un parent lors de sa séparation, il doit toujours allouer les 4 feuilles enfant pour stocker les éléments dans seulement deux quadrants tout en ayant deux feuilles vides comme enfants. Cependant, le compromis vaut largement la peine en termes de performances, du moins dans mes cas d'utilisation, et rappelez-vous qu'un nœud ne prend que 8 octets, compte tenu de la quantité compactée.

Lors de la désallocation des enfants, nous désallouons les quatre à la fois. Je le fais en temps constant en utilisant une liste libre indexée, comme ceci:

enter image description here

Sauf que nous regroupons des morceaux de mémoire contenant 4 éléments contigus au lieu d'un à la fois. Cela fait que nous n'avons généralement pas besoin d'impliquer d'allocations de tas ou de désallocations pendant la simulation. Un groupe de 4 nœuds est marqué comme libéré de manière indivisible pour être ensuite récupéré de manière indivisible dans une division ultérieure d'un autre nœud feuille.

Nettoyage différé

Je ne mets pas à jour la structure du quadtree immédiatement lors de la suppression d'éléments. Lorsque je supprime un élément, je descends simplement dans l'arbre jusqu'au nœud enfant qu'il occupe, puis je supprime l'élément, mais je ne me donne pas la peine de faire encore plus, même si les feuilles deviennent vides.

Au lieu de cela, je fais un nettoyage différé comme ceci:

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;
        }
    }
}

Ceci est appelé à la fin de chaque image après avoir déplacé tous les agents. La raison pour laquelle je fais ce type de suppression différée de nœuds feuilles vides dans plusieurs itérations et pas tout à la fois dans le processus de suppression d'un seul élément est que l'élément A peut se déplacer vers le nœud N2, faisant N1 vide. Cependant, l'élément B peut, dans le même cadre, se déplacer vers N1, ce qui le rend à nouveau occupé.

Avec le nettoyage différé, nous pouvons gérer de tels cas sans supprimer inutilement des enfants uniquement pour les ajouter immédiatement lorsqu'un autre élément se déplace dans ce quadrant.

Déplacer des éléments dans mon cas est simple: 1) supprimer un élément, 2) le déplacer, 3) le réinsérer dans le quadtree. Après avoir déplacé tous les éléments et à la fin d'une image (pas de pas de temps, il pourrait y avoir plusieurs pas de temps par image), la fonction cleanup ci-dessus est appelée pour supprimer les enfants d'un parent qui a 4 vides quitte en tant qu'enfant, ce qui transforme efficacement ce parent en la nouvelle feuille vide qui pourrait ensuite être nettoyée dans le cadre suivant avec un appel cleanup ultérieur (ou pas si des choses y sont insérées ou si les frères et sœurs de la feuille vide sont non vide).

Regardons visuellement le nettoyage différé:

enter image description here

En partant de cela, disons que nous supprimons certains éléments de l'arbre en nous laissant avec 4 feuilles vides:

enter image description here

À ce stade, si nous appelons cleanup, il supprimera 4 feuilles s'il trouve 4 feuilles enfant vides et transformera le parent en feuille vide, comme ceci:

enter image description here

Disons que nous supprimons quelques éléments supplémentaires:

enter image description here

... puis appelez à nouveau cleanup:

enter image description here

Si nous l'appelons encore une fois, nous nous retrouvons avec ceci:

enter image description here

... à quel point la racine elle-même se transforme en feuille vide. Cependant, la méthode de nettoyage ne supprime jamais la racine (elle supprime uniquement les enfants). Encore une fois, le principal objectif de le faire différé de cette façon et en plusieurs étapes est de réduire la quantité de travail redondant potentiel qui pourrait se produire par pas de temps (ce qui peut être beaucoup) si nous faisions tout cela immédiatement chaque fois qu'un élément est supprimé de l'arbre. Il aide également à distribuer ce qui fonctionne sur plusieurs cadres pour éviter les bégaiements.

TBH, j'ai d'abord appliqué cette technique de "nettoyage différé" dans un jeu DOS que j'ai écrit en C par pure paresse! Je ne voulais pas prendre la peine de descendre dans l'arbre, de supprimer des éléments, puis de supprimer des nœuds de façon ascendante à l'époque parce que j'avais écrit à l'origine l'arbre pour favoriser la traversée de haut en bas (pas de haut en bas et de retour) et vraiment pensé que cette solution paresseuse était un compromis de productivité (sacrifier des performances optimales pour être implémenté plus rapidement). Cependant, de nombreuses années plus tard, j'ai réussi à implémenter la suppression de quadtree d'une manière qui a immédiatement commencé à supprimer les nœuds et, à ma grande surprise, je l'ai considérablement ralentie avec des taux de trame plus imprévisibles et stuttery. Le nettoyage différé, bien qu'inspiré à l'origine par la paresse de mon programmeur, était en fait (et accidentellement) une optimisation très efficace pour les scènes dynamiques.

Listes d'index à liaison unique pour les éléments

Pour les éléments, j'utilise cette représentation:

// 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;
};

J'utilise un "noeud d'élément" qui est distinct de "élément". Un élément n'est inséré qu'une seule fois dans le quadtree, quel que soit le nombre de cellules qu'il occupe. Cependant, pour chaque cellule qu'il occupe, un "nœud d'élément" est inséré qui indexe cet élément.

Le nœud d'élément est un nœud de liste d'index à liaison unique dans un tableau et utilise également la méthode de liste libre ci-dessus. Cela entraîne des erreurs de cache supplémentaires sur le stockage de tous les éléments de manière contiguë pour une feuille. Cependant, étant donné que ce quadtree est pour des données très dynamiques qui se déplacent et entrent en collision à chaque pas de temps, cela prend généralement plus de temps de traitement qu'il n'en économise pour rechercher une représentation parfaitement contiguë pour les éléments de feuille (vous devrez effectivement implémenter une variable -allocateur de mémoire de taille qui est vraiment rapide, et c'est loin d'être une chose facile à faire). J'utilise donc la liste d'index à liaison unique qui permet une approche de liste libre en temps constant pour l'allocation/la désallocation. Lorsque vous utilisez cette représentation, vous pouvez transférer des éléments de parents séparés vers de nouvelles feuilles en changeant simplement quelques entiers.

SmallList<T>

Oh, je devrais mentionner cela. Naturellement, cela aide si vous n'allouez pas juste pour stocker une pile temporaire de nœuds pour une traversée non récursive. SmallList<T> est similaire à vector<T> sauf qu'il n'impliquera pas d'allocation de segment de mémoire tant que vous n'y aurez pas inséré plus de 128 éléments. C'est similaire aux optimisations de chaînes SBO dans la bibliothèque standard C++. C'est quelque chose que j'ai implémenté et que j'utilise depuis des siècles et cela aide beaucoup à vous assurer que vous utilisez la pile autant que possible.

Représentation arborescente

Voici la représentation du quadtree lui-même:

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;
};

Comme indiqué ci-dessus, nous stockons un seul rectangle pour la racine (root_rect). Tous les sous-rects sont calculés à la volée. Tous les nœuds sont stockés de manière contiguë dans un tableau (std::vector<QuadNode>) ainsi que les éléments et les nœuds d'élément (dans FreeList<T>).

FreeList<T>

J'utilise une structure de données FreeList qui est essentiellement un tableau (et une séquence d'accès aléatoire) qui vous permet de supprimer des éléments de n'importe où en temps constant (en laissant des trous derrière qui sont récupérés lors des insertions suivantes en temps constant). Voici une version simplifiée qui ne prend pas la peine de gérer des types de données non triviaux (n'utilise pas de nouveaux appels de placement ou de destruction manuelle):

/// 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;
}

J'en ai un qui fonctionne avec des types non triviaux et fournit des itérateurs et ainsi de suite, mais est beaucoup plus impliqué. De nos jours, j'ai tendance à travailler davantage avec des structures de style C trivialement constructibles/destructibles (en utilisant uniquement des types définis par l'utilisateur non triviaux pour des choses de haut niveau).

Profondeur d'arbre maximale

J'empêche l'arbre de se subdiviser trop en spécifiant une profondeur maximale autorisée. Pour la simulation rapide que j'ai fouettée, j'en ai utilisé 8. Pour moi, c'est crucial car encore une fois, dans les effets visuels, je rencontre beaucoup de cas pathologiques, y compris du contenu créé par des artistes avec beaucoup d'éléments coïncidents ou se chevauchant qui, sans limite maximale de profondeur d'arbre, pourraient veulent qu'il se subdivise indéfiniment.

Il y a un peu de réglage si vous voulez des performances optimales en ce qui concerne la profondeur maximale autorisée et le nombre d'éléments que vous autorisez à stocker dans une feuille avant qu'elle ne se divise en 4 enfants. J'ai tendance à trouver que les résultats optimaux sont obtenus avec quelque chose autour de 8 éléments maximum par nœud avant qu'il ne se divise, et une profondeur maximale définie de sorte que la plus petite taille de cellule soit légèrement supérieure à la taille de votre agent moyen (sinon plus d'agents simples pourraient être insérés en plusieurs feuilles).

Collision et requêtes

Il existe plusieurs façons d'effectuer la détection de collision et les requêtes. Je vois souvent les gens le faire comme ça:

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

C'est très simple, mais le problème avec cette approche est que le premier élément de la scène pourrait être dans un endroit totalement différent du monde par rapport au second. En conséquence, les chemins que nous empruntons le long du quadtree pourraient être totalement sporadiques. Nous pourrions traverser un chemin vers une feuille, puis vouloir redescendre ce même chemin pour le premier élément comme, disons, le 50 000e élément. À ce stade, les nœuds impliqués dans ce chemin peuvent avoir déjà été supprimés du cache du processeur. Je recommande donc de le faire de cette façon:

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                  
          }
     }
}

Bien que cela soit un peu plus de code et nécessite de conserver un ensemble de bits traversed ou un tableau parallèle d'une sorte pour éviter de traiter les éléments deux fois (car ils peuvent être insérés dans plusieurs feuilles), cela permet de s'assurer que nous descendons les mêmes chemins dans le quadtree tout au long de la boucle. Cela permet de garder les choses beaucoup plus compatibles avec le cache. De plus, si après avoir tenté de déplacer l'élément dans le pas de temps, il est toujours entièrement englobé dans ce nœud de feuille, nous n'avons même pas besoin de remonter à partir de la racine (nous pouvons simplement vérifier qu'une seule feuille).

Inefficacités courantes: choses à éviter

Bien qu'il existe de nombreuses façons de dépouiller le chat et de parvenir à une solution efficace, il existe un moyen courant d'obtenir une solution très inefficace. Et j'ai rencontré ma part de très inefficace quadtrees, arbres kd et octrees dans ma carrière en travaillant dans VFX. Nous parlons d'un gigaoctet d'utilisation de mémoire juste pour partitionner un maillage avec 100k triangles tout en prenant 30 secondes à construire, alors qu'une implémentation décente devrait être capable de faire les mêmes centaines de fois par seconde et de prendre quelques megs. Il y a beaucoup de gens qui les fouettent pour résoudre des problèmes qui sont des assistants théoriques mais n'ont pas prêté beaucoup d'attention à l'efficacité de la mémoire.

Donc, le non-non absolu le plus commun que je vois est de stocker un ou plusieurs conteneurs à part entière avec chaque nœud d'arbre. Par conteneur à part entière, je veux dire quelque chose qui possède et alloue et libère sa propre mémoire, comme ceci:

struct Node
{
     ...

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

Et List<Element> peut être une liste en Python, un ArrayList en Java ou C #, std::vector en C++, etc: une structure de données qui gère sa propre mémoire/ressources.

Le problème ici est que, bien que ces conteneurs soient très efficacement mis en œuvre pour stocker un grand nombre d'éléments, tous d'entre eux dans toutes les langues sont extrêmement inefficaces si vous en instanciez une cargaison uniquement pour en stocker quelques-uns éléments dans chacun. L'une des raisons est que les métadonnées du conteneur ont tendance à être assez explosives dans l'utilisation de la mémoire à un niveau aussi granulaire d'un nœud d'arbre unique. Un conteneur peut avoir besoin de stocker une taille, une capacité, un pointeur/une référence aux données qu'il alloue, etc., et tout cela à des fins généralisées, il peut donc utiliser des entiers 64 bits pour la taille et la capacité. En conséquence, les métadonnées juste pour un conteneur vide peuvent être de 24 octets, ce qui est déjà 3 fois plus grand que l'intégralité de la représentation de nœud que j'ai proposée, et c'est juste pour un conteneur vide conçu pour stocker des éléments dans des feuilles.

De plus, chaque conteneur souhaite souvent allouer du tas/GC lors de l'insertion ou nécessiter encore plus de mémoire préallouée à l'avance (auquel cas cela peut prendre 64 octets uniquement pour le conteneur lui-même). Donc, soit cela devient lent à cause de toutes les allocations (notez que les allocations GC sont vraiment rapides au départ dans certaines implémentations comme JVM, mais ce n'est que pour le cycle Eden de rafale initial) ou parce que nous subissons une cargaison de cache manque car les nœuds sont si énorme que pratiquement aucun ne rentre dans les niveaux inférieurs du cache du processeur lors de la traversée, ou les deux.

Pourtant, c'est une inclinaison très naturelle et a un sens intuitif puisque nous parlons de ces structures en utilisant théoriquement un langage comme, "Les éléments sont stockés dans les feuilles" qui suggère de stocker un conteneur d'éléments dans les nœuds de feuilles. Malheureusement, il a un coût explosif en termes d'utilisation et de traitement de la mémoire. Évitez donc cela si le désir est de créer quelque chose d'assez efficace. Faites le partage Node et pointez sur (référez-vous à) ou indexez la mémoire allouée et stockée pour l'arborescence entière, pas pour chaque nœud individuel. En réalité, les éléments ne devraient pas être stockés dans les feuilles.

Les éléments doivent être stockés dans l'arborescence et les nœuds terminaux doivent index ou pointer vers ces éléments.

Conclusion

Ouf, ce sont donc les principales choses que je fais pour atteindre ce qui, espérons-le, est considéré comme une solution performante. J'espère que ça aide. Notez que je vise cela à un niveau quelque peu avancé pour les personnes qui ont déjà implémenté des quadtrees au moins une ou deux fois. Si vous avez des questions, n'hésitez pas à tirer.

Étant donné que cette question est un peu large, je pourrais venir la modifier et continuer à la peaufiner et à l'étendre au fil du temps si elle ne se ferme pas (j'aime ce type de questions car elles nous donnent une excuse pour écrire sur nos expériences de travail dans le mais le site ne les aime pas toujours). J'espère également que certains experts pourraient intervenir avec des solutions alternatives que je peux apprendre et peut-être utiliser pour améliorer le mien.

Encore une fois, les quadtrees ne sont pas ma structure de données préférée pour des scénarios de collision extrêmement dynamiques comme celui-ci. J'ai donc probablement une ou deux choses à apprendre des gens qui privilégient les quadruples à cet effet et les ajustent et les ajustent depuis des années. Généralement, j'utilise des arbres quadruples pour les données statiques qui ne se déplacent pas dans chaque image, et pour celles-ci, j'utilise une représentation très différente de celle proposée ci-dessus.

71
Dragon Energy

2. Fondamentaux

Pour cette réponse (désolé, j'ai encore manqué de limite de caractères), je vais me concentrer davantage sur les principes fondamentaux destinés à quelqu'un de nouveau dans ces structures.

Très bien, alors disons que nous avons un tas d'éléments comme celui-ci dans l'espace:

enter image description here

Et nous voulons savoir quel élément se trouve sous le curseur de la souris, ou quels éléments se croisent/entrent en collision les uns avec les autres, ou quel est l'élément le plus proche d'un autre élément, ou quelque chose de ce genre.

Dans ce cas, si les seules données dont nous disposions étaient un ensemble de positions et de tailles/rayons d'éléments dans l'espace, nous aurions à parcourir tout pour découvrir quel élément se trouve dans une zone de recherche donnée. Pour la détection des collisions, nous devons parcourir tous les éléments puis, pour chaque élément, parcourir tous les autres éléments, ce qui en fait un algorithme de complexité quadratique explosive. Cela ne tiendra pas sur les tailles d'entrée non triviales.

Subdiviser

Que pouvons-nous faire pour résoudre ce problème? L'une des approches les plus simples consiste à subdiviser l'espace de recherche (écran, par exemple) en un nombre fixe de cellules, comme suit:

enter image description here

Supposons maintenant que vous vouliez trouver l'élément sous le curseur de votre souris à la position (cx, cy). Dans ce cas, il suffit de vérifier les éléments de la cellule sous le curseur de la souris:

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)
}

Même chose pour la détection de collision. Si nous voulons voir quels éléments se croisent (entrent en collision) avec un élément donné:

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.
             }
         }
     }
}

Et je recommande aux gens de commencer de cette façon en divisant l'espace/écran en un nombre fixe de cellules de grille comme 10x10, ou 100x100, ou même 1000x1000. Certaines personnes pourraient penser que 1000x1000 serait explosif dans l'utilisation de la mémoire, mais vous pouvez faire en sorte que chaque cellule ne nécessite que 4 octets avec des entiers 32 bits, comme ceci:

enter image description here

... à ce moment-là, même un million de cellules prend moins de 4 mégaoctets.

Inconvénient d'une grille à résolution fixe

La grille de résolution fixe est une structure de données fantastique pour ce problème si vous me le demandez (mon préféré pour la détection de collision), mais elle a quelques faiblesses.

Imaginez que vous ayez un jeu vidéo Lord of the Rings. Disons que beaucoup de vos unités sont de petites unités sur la carte comme les humains, les orques et les elfes. Cependant, vous avez également des unités gigantesques comme les dragons et les ents.

Ici, un problème avec la résolution fixe de la grille est que même si la taille de vos cellules peut être optimale pour stocker ces petites unités comme les humains et les elfes et les orques qui n'occupent qu'une seule cellule la plupart du temps, les énormes mecs comme les dragons et les ents pourraient vouloir occupent de nombreuses cellules, disons 400 cellules (20x20). En conséquence, nous devons insérer ces gros gars dans de nombreuses cellules et stocker beaucoup de données redondantes.

Supposons également que vous souhaitiez rechercher dans une grande zone rectangulaire de la carte des unités d'intérêt. Dans ce cas, vous devrez peut-être vérifier bien plus de cellules que ce qui est théoriquement optimal.

C'est le principal inconvénient d'une grille à résolution fixe *. Nous devrons éventuellement insérer de grandes choses et les stocker dans beaucoup plus de cellules que nous ne devrions idéalement en avoir, et pour les grandes zones de recherche, nous devrons peut-être vérifier beaucoup plus de cellules que nous ne devrions idéalement avoir à rechercher.

  • Cela dit, en mettant de côté la théorie, vous pouvez souvent travailler avec des grilles d'une manière qui respecte le cache de manière similaire au traitement d'image. Par conséquent, bien qu'il présente ces inconvénients théoriques, dans la pratique, la simplicité et la facilité d'implémentation de modèles de traversée compatibles avec le cache peuvent rendre la grille bien meilleure qu'elle n'y paraît.

Quadtrees

Donc, les arbres quadruples sont une solution à ce problème. Au lieu d'utiliser une grille de résolution fixe, pour ainsi dire, ils adaptent la résolution en fonction de certains critères, tout en se subdivisant/divisant en 4 cellules enfants pour augmenter la résolution. Par exemple, nous pourrions dire qu'une cellule devrait se diviser s'il y a plus de 2 enfants dans une cellule donnée. Dans ce cas, ceci:

enter image description here

Finit par devenir ceci:

enter image description here

Et maintenant, nous avons une jolie représentation de Nice où aucune cellule ne stocke plus de 2 éléments. En attendant, considérons ce qui se passe si nous insérons un énorme dragon:

enter image description here

Ici, contrairement à la grille de résolution fixe, le dragon peut simplement être inséré dans une cellule car la cellule qu'il occupe ne contient qu'un seul élément. De même, si nous recherchons une grande zone de la carte, nous n'aurons pas à vérifier autant de cellules à moins que de nombreux éléments occupent les cellules.

Implémentation

Alors, comment mettons-nous en œuvre l'un de ces trucs? Eh bien, c'est un arbre à la fin de la journée, et un arbre à 4 arbres à cela. Nous commençons donc avec la notion de nœud racine avec 4 enfants, mais ils sont actuellement nuls/nuls et la racine est également une feuille pour le moment:

enter image description here

Insertion

Commençons par insérer certains éléments, et pour plus de simplicité, disons qu'un nœud doit se diviser lorsqu'il a plus de 2 éléments. Nous allons donc insérer un élément, et lorsque nous insérons un élément, nous devons l'insérer dans les feuilles (cellules) auxquelles il appartient. Dans ce cas, nous n'en avons qu'un, le nœud/cellule racine, et nous allons donc l'insérer là:

enter image description here

... et insérons un autre:

enter image description here

... et encore un autre:

enter image description here

Et maintenant, nous avons plus de 2 éléments dans un nœud feuille. Il devrait maintenant se diviser. À ce stade, nous créons 4 enfants au nœud feuille (notre racine dans ce cas), puis transférons les éléments de la feuille divisée (la racine) dans les quadrants appropriés en fonction de la zone/cellule que chaque élément occupe dans l'espace:

enter image description here

Insérons un autre élément, et encore une fois dans la feuille appropriée à laquelle il appartient:

enter image description here

... et un autre:

enter image description here

Et maintenant, nous avons à nouveau plus de 2 éléments dans une feuille, nous devons donc la diviser en 4 quadrants (enfants):

enter image description here

Et c'est l'idée de base. L'une des choses que vous remarquerez peut-être est que lorsque nous insérons des éléments qui ne sont pas des points infinitésimalement petits, ils peuvent facilement chevaucher plusieurs cellules/nœuds.

Par conséquent, si nous avons de nombreux éléments qui chevauchent de nombreuses frontières entre les cellules, ils pourraient finir par vouloir subdiviser un lot entier, éventuellement à l'infini. Pour atténuer ce problème, certaines personnes choisissent de diviser l'élément. Si tout ce que vous associez à un élément est un rectangle, il est assez simple de découper des rectangles. D'autres personnes pourraient simplement mettre une limite de profondeur/récursivité sur la quantité d'arbre pouvant se diviser. J'ai tendance à préférer cette dernière solution pour les scénarios de détection de collision entre ces deux, car je trouve au moins plus facile à mettre en œuvre plus efficacement. Cependant, il existe une autre alternative: les représentations en vrac, et qui seront couvertes dans une section différente.

De plus, si vous avez des éléments les uns sur les autres, votre arbre peut vouloir se diviser indéfiniment même si vous stockez des points infinitésimales. Par exemple, si vous avez 25 points les uns au-dessus des autres dans l'espace (un scénario que je rencontre assez souvent dans VFX), votre arbre voudra se diviser indéfiniment sans limite de récursivité/profondeur quoi qu'il arrive. Par conséquent, pour gérer les cas pathologiques, vous pourriez avoir besoin d'une limite de profondeur même si vous découpez des éléments.

Suppression d'éléments

La suppression des éléments est traitée dans la première réponse ainsi que la suppression des nœuds pour nettoyer l'arbre et supprimer les feuilles vides. Mais fondamentalement, tout ce que nous faisons pour supprimer un élément en utilisant mon approche proposée est simplement de descendre dans l'arbre jusqu'à l'endroit où la feuille/les feuilles dans lesquelles l'élément est stocké (que vous pouvez déterminer en utilisant son rectangle, par exemple), et de le supprimer de ces feuilles.

Ensuite, pour commencer à supprimer les nœuds feuilles vides, nous utilisons une approche de nettoyage différé couverte dans la réponse d'origine.

Conclusion

Je manque de temps, mais j'essaierai de revenir sur celui-ci et de continuer à améliorer la réponse. Si les gens veulent un exercice, je suggère d'implémenter une ancienne grille à résolution fixe simple et de voir si vous pouvez le faire descendre à l'endroit où chaque cellule n'est qu'un entier 32 bits. Commencez par comprendre la grille et ses problèmes inhérents avant d'envisager le quadtree, et vous pouvez être très bien avec la grille. Il pourrait même vous fournir la solution la plus optimale en fonction de l'efficacité avec laquelle vous pouvez implémenter une grille par rapport à un quadtree.

20
Dragon Energy

5. Double-grille lâche/étanche avec des agents 500k

enter image description here

Ci-dessus est un petit GIF montrant 500 000 agents qui rebondissent les uns sur les autres à chaque étape en utilisant une nouvelle structure de données "grille lâche/serrée" que j'ai été inspiré pour créer après avoir écrit la réponse sur les quadrilatères lâches. Je vais essayer de voir comment cela fonctionne.

C'est la structure de données la plus performante à ce jour parmi toutes celles que j'ai montrées que j'ai implémentées (bien que cela puisse être juste moi), gérant un demi-million d'agents mieux que le quadtree initial traitait 100k, et mieux que le lâche quadtree a traité 250k. Il nécessite également le moins de mémoire et a l'utilisation de mémoire la plus stable parmi ces trois. Tout cela fonctionne toujours dans un seul thread, pas de code SIMD, pas de micro-optimisations sophistiquées car je demande généralement du code de production - juste une implémentation simple à partir de quelques heures de travail.

J'ai également amélioré les goulots d'étranglement du dessin sans améliorer mon code de pixellisation. C'est parce que la grille me permet de la parcourir facilement d'une manière compatible avec le cache pour le traitement d'image (le fait de dessiner les éléments dans les cellules de la grille un par un se trouve par hasard conduire à des modèles de traitement d'image très compatibles avec le cache lors de la pixellisation).

Curieusement, cela a également pris le plus court temps pour moi à implémenter (seulement 2 heures alors que j'ai passé 5 ou 6 heures sur le quadtree lâche), et cela nécessite également le moins de code (et a sans doute le code le plus simple). Bien que cela puisse être dû au fait que j'ai accumulé beaucoup d'expérience dans la mise en œuvre des grilles.

Double grille lâche/étanche

J'ai donc couvert le fonctionnement des grilles dans la section des principes fondamentaux (voir partie 2), mais il s'agit d'une "grille lâche". Chaque cellule de la grille stocke son propre cadre de délimitation qui peut se rétrécir à mesure que des éléments sont supprimés et croître à mesure que des éléments sont ajoutés. Par conséquent, chaque élément n'a besoin d'être inséré qu'une seule fois dans la grille en fonction de la cellule dans laquelle se trouve sa position centrale, comme suit:

// 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.

Les cellules stockent des éléments et des AABB comme ceci:

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;
};

Cependant, les cellules en vrac posent un problème conceptuel. Étant donné qu'ils ont ces boîtes englobantes qui peuvent devenir énormes si nous insérons un élément énorme, comment éviter de vérifier chaque cellule de la grille lorsque nous voulons savoir quelles cellules et quels éléments lâches à l'intérieur coupent un rectangle de recherche, par exemple?

Et en fait, c'est une "double grille lâche". Les cellules de grille lâches elles-mêmes sont divisées en une grille serrée. Lorsque nous effectuons la recherche analogique ci-dessus, nous examinons d'abord la grille serrée comme suit:

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
                }
            }
        }
    }
}

Les cellules serrées stockent une liste d'index liée de cellules lâches, comme ceci:

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;
};

Et voilà, c'est l'idée de base de la "double grille lâche". Lorsque nous insérons un élément, nous développons l'AABB de la cellule lâche comme nous le faisons pour un quadtree lâche. Cependant, nous insérons également la cellule lâche dans la grille serrée en fonction de son rectangle (et elle pourrait être insérée dans plusieurs cellules).

La combinaison de ces deux (grille serrée pour trouver rapidement des cellules lâches et cellules lâches pour trouver rapidement des éléments) donne une très belle structure de données où chaque élément est inséré dans une seule cellule avec des recherches, des insertions et des suppressions à temps constant.

Le seul gros inconvénient que je vois est que nous devons stocker toutes ces cellules et potentiellement encore rechercher plus de cellules que nous n'en avons besoin, mais elles sont toujours raisonnablement bon marché (20 octets par cellule dans mon cas) et il est facile de parcourir la cellules sur les recherches dans un modèle d'accès très cache.

Je recommande d'essayer cette idée de "grilles lâches". Il est sans doute beaucoup plus facile à implémenter que les quadtrees et les quadtrees lâches et, plus important encore, à optimiser, car il se prête immédiatement à une disposition de mémoire compatible avec le cache. Comme bonus super cool, si vous pouvez anticiper le nombre d'agents dans votre monde à l'avance, il est 100% parfaitement stable et immédiatement en termes d'utilisation de la mémoire, car un élément occupe toujours exactement une cellule et le nombre total de cellules est fixe (car ils ne se subdivisent pas/ne se divisent pas). En conséquence, l'utilisation de la mémoire est très stable. Cela pourrait être un énorme bonus pour certains matériels et logiciels où vous souhaitez pré-allouer toute la mémoire à l'avance et sachez que l'utilisation de la mémoire ne dépassera jamais ce point.

Il est également très facile de le faire fonctionner avec SIMD pour effectuer plusieurs requêtes cohérentes simultanément avec du code vectorisé (en plus du multithreading), car la traversée, si nous pouvons même l'appeler ainsi, est plate (c'est juste une recherche en temps constant dans un indice cellulaire qui implique une certaine arithmétique). Par conséquent, il est assez facile d'appliquer des stratégies d'optimisation similaires aux paquets de rayons qu'Intel applique à leur noyau de lancer de rayons/BVH (Embree) pour tester simultanément plusieurs rayons cohérents (dans notre cas, ce seraient des "paquets d'agent" pour la collision), sauf sans un tel code fantaisie/complexe car la "traversée" de la grille est tellement plus simple.

Sur l'utilisation et l'efficacité de la mémoire

J'ai couvert cela un peu dans la partie 1 sur les quadtrees efficaces, mais la réduction de l'utilisation de la mémoire est souvent la clé pour accélérer ces temps car nos processeurs sont si rapides une fois que vous avez entré les données dans, disons, L1 ou un registre, mais l'accès DRAM est relativement si , tellement lent. Nous avons encore si peu de mémoire rapide, même si nous avons une quantité folle de mémoire lente.

Je pense que je suis un peu chanceux de commencer à une époque où nous devions être très économes avec l'utilisation de la mémoire (mais pas autant que les gens avant moi), où même un mégaoctet de DRAM était considéré comme incroyable. Certaines des choses que j'ai apprises à l'époque et les habitudes que j'ai prises (même si je suis loin d'être un expert) s'alignent par hasard sur la performance.

Donc, un conseil général que j'offrirais sur l'efficacité en général, et pas seulement sur les indices spatiaux utilisés pour la détection des collisions, est de surveiller l'utilisation de la mémoire. S'il est explosif, il est probable que la solution ne sera pas très efficace. Bien sûr, il y a une zone grise où l'utilisation d'un peu plus de mémoire pour une structure de données peut réduire considérablement le traitement au point où il est avantageux de ne considérer que la vitesse, mais souvent de réduire la quantité de mémoire requise pour les structures de données, en particulier la "mémoire chaude" "auquel on accède de façon répétée, peut se traduire de façon assez proportionnelle par une amélioration de la vitesse. Tous les indices spatiaux les moins efficaces que j'ai rencontrés au cours de ma carrière étaient les plus explosifs dans l'utilisation de la mémoire.

Il est utile de regarder la quantité de données dont vous avez besoin pour stocker et calculer, au moins grossièrement, la quantité de mémoire dont elle devrait idéalement avoir besoin. Comparez-le ensuite à ce dont vous avez réellement besoin. Si les deux sont mondes à part, alors vous obtiendrez probablement un coup de pouce décent réduisant l'utilisation de la mémoire, car cela se traduira souvent par moins de temps à charger des morceaux de mémoire à partir des formes de mémoire plus lentes dans la hiérarchie de la mémoire.

14
Dragon Energy

4. Quadtree en vrac

Très bien, je voulais prendre un peu de temps pour implémenter et expliquer les quadruples lâches, car je les trouve très intéressants et peut-être même les plus équilibrés pour la plus grande variété de cas d'utilisation impliquant des scènes très dynamiques.

J'ai donc fini par en implémenter un hier soir et j'ai passé un peu de temps à le peaufiner, à le régler et à le profiler. Voici un teaser avec un quart de million d'agents dynamiques, tous se déplaçant et rebondissant les uns sur les autres à chaque pas:

enter image description here

Les fréquences d'images commencent à souffrir lorsque je fais un zoom arrière pour regarder tous les quart de million d'agents ainsi que tous les rectangles englobants du quadrilatère lâche, mais cela est principalement dû aux goulots d'étranglement dans mes fonctions de dessin. Ils commencent à devenir des points chauds si je fais un zoom arrière pour tout dessiner sur l'écran d'un coup et je n'ai pas pris la peine de les optimiser du tout. Voici comment cela fonctionne au niveau de base avec très peu d'agents:

enter image description here

Quadtree lâche

Très bien, alors qu'est-ce que les quadtrees en vrac? Ce sont essentiellement des arbres quadruples dont les nœuds ne sont pas parfaitement divisés au centre en quatre quadrants pairs. Au lieu de cela, leurs AABB (rectangles de délimitation) pourraient se chevaucher et être plus grands ou souvent même plus petits que ce que vous obtiendriez si vous divisez un nœud parfaitement au centre en 4 quadrants.

Donc, dans ce cas, nous devons absolument stocker les boîtes englobantes avec chaque nœud, et donc je l'ai représenté comme ceci:

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;
};

Cette fois, j'ai essayé d'utiliser la virgule flottante simple précision pour voir comment il fonctionne bien, et cela a fait un travail très décent.

À quoi ça sert?

Très bien, alors à quoi ça sert? La principale chose que vous pouvez exploiter avec un quadtree lâche est que vous pouvez traiter chaque élément que vous insérez dans le quadtree comme un seul point pour l'insertion et le retrait. Par conséquent, un élément n'est jamais inséré dans plus d'un nœud feuille dans l'arbre entier car il est traité comme un point infinitésimalement petit.

Cependant, au fur et à mesure que nous insérons ces "points d'élément" dans l'arbre, nous développons les cadres de délimitation de chaque nœud dans lequel nous insérons afin d'englober les limites de l'élément (le rectangle de l'élément, par exemple). Cela nous permet de trouver ces éléments de manière fiable lorsque nous effectuons une requête de recherche (ex: recherche de tous les éléments qui coupent une zone de rectangle ou de cercle).

Avantages:

  • Même l'agent le plus gigantesque ne doit être inséré que dans un nœud feuille et ne prendra pas plus de mémoire que le plus petit. En conséquence, il est bien adapté aux scènes avec des éléments dont les tailles varient énormément de l'une à l'autre, et c'est ce que je faisais des tests de résistance dans la démo d'agent 250k ci-dessus.
  • Utilise moins de mémoire par élément, en particulier les éléments énormes.

Inconvénients:

  • Bien que cela accélère l'insertion et le retrait, cela ralentit inévitablement les recherches dans l'arborescence. Ce qui était auparavant quelques comparaisons de base avec un point central d'un nœud pour déterminer quels quadrants descendre en virages en boucle devant vérifier chaque rectangle de chaque enfant pour déterminer ceux qui coupent une zone de recherche.
  • Utilise plus de mémoire par nœud (5 fois plus dans mon cas).

Requêtes coûteuses

Ce premier con serait assez horrible pour les éléments statiques puisque tout ce que nous faisons est de construire l'arbre et de le rechercher dans ces cas. Et j'ai trouvé avec ce quadtree lâche que, malgré quelques heures à le peaufiner et à le régler, il y a un énorme hotspot impliqué dans l'interrogation:

enter image description here

Cela dit, il s'agit en fait de mon "record personnel" d'implémentation d'un quadtree jusqu'à présent pour les scènes dynamiques (mais gardez à l'esprit que je privilégie les grilles hiérarchiques à cet effet et que je n'ai pas beaucoup d'expérience en utilisant des quadtrees pour les scènes dynamiques) malgré ce con flagrant. Et c'est parce que pour les scènes dynamiques au moins, nous devons constamment déplacer des éléments à chaque pas de temps, et donc il y a beaucoup plus à faire avec l'arbre que de simplement l'interroger. Il doit être mis à jour tout le temps, et cela fait en fait un travail assez décent.

Ce que j'aime dans le quadtree lâche, c'est que vous pouvez vous sentir en sécurité même si vous avez une cargaison d'éléments massifs en plus d'une cargaison d'éléments les plus jeunes. Les éléments massifs ne prendront pas plus de mémoire que les petits. Par conséquent, si j'écrivais un jeu vidéo avec un monde immense et que je voulais tout jeter dans un index spatial central pour accélérer tout sans se soucier d'une hiérarchie de structures de données comme je le fais habituellement, alors les quadtrees et les octrees lâches pourraient être parfaitement équilibrée comme "la structure de données universelle centrale si nous voulons en utiliser une seule pour un monde dynamique entier".

Utilisation de la mémoire

En termes d'utilisation de la mémoire, alors que les éléments prennent moins de mémoire (en particulier les plus massifs), les nœuds en prennent un peu plus par rapport à mes implémentations où les nœuds n'ont même pas besoin de stocker un AABB. J'ai trouvé globalement dans une variété de cas de test, y compris ceux avec de nombreux éléments gigantesques, que le quadtree lâche a tendance à prendre un peu plus de mémoire avec ses nœuds costauds (~ 33% de plus comme estimation approximative). Cela dit, il fonctionne mieux que l'implémentation quadtree dans ma réponse d'origine.

Du côté positif, l'utilisation de la mémoire est plus stable (ce qui a tendance à se traduire par des fréquences d'images plus stables et plus fluides). Le quadtree de ma réponse d'origine a pris environ 5+ secondes avant que l'utilisation de la mémoire ne devienne parfaitement stable. Celui-ci a tendance à devenir stable une ou deux secondes après le démarrage, et probablement parce que les éléments ne doivent jamais être insérés plus d'une fois (même les petits éléments peuvent être insérés deux fois dans mon quadtree d'origine s'ils chevauchent deux ou plusieurs nœuds aux limites). En conséquence, la structure de données découvre rapidement la quantité de mémoire requise à allouer contre tous les cas, pour ainsi dire.

Théorie

Voyons donc la théorie de base. Je recommande de commencer par implémenter un quadtree régulier d'abord et de le comprendre avant de passer à des versions lâches car elles sont un peu plus difficiles à implémenter. Lorsque nous commençons avec un arbre vide, vous pouvez l'imaginer comme ayant également un rectangle vide.

Insérons un élément:

enter image description here

Puisque nous n'avons pour l'instant qu'un nœud racine qui est également une feuille, nous l'insérons simplement dans celui-ci. En faisant cela, le rectangle précédemment vide du nœud racine englobe maintenant l'élément que nous avons inséré (montré en pointillés rouges). Insérons un autre:

enter image description here

Nous développons les AABB des nœuds que nous traversons lorsque nous insérons (cette fois juste la racine) par les AABB des éléments que nous insérons. Insérons un autre, et disons que les nœuds doivent se séparer lorsqu'ils contiennent plus de 2 éléments:

enter image description here

Dans ce cas, nous avons plus de 2 éléments dans un nœud feuille (notre racine), nous devons donc le diviser en 4 quadrants. C'est à peu près la même chose que de diviser un quadtree ponctuel normal, sauf que nous élargissons à nouveau les cadres de délimitation lorsque nous transférons des enfants. Nous commençons par considérer la position centrale du nœud à diviser:

enter image description here

Nous avons maintenant 4 enfants sur notre nœud racine et chacun stocke également sa boîte englobante également ajustée (indiquée en vert). Insérons un autre élément:

enter image description here

Ici, vous pouvez voir que l'insertion de cet élément a non seulement développé le rectangle de l'enfant inférieur gauche, mais également la racine (nous développons tous les AABB le long du chemin que nous insérons). Insérons un autre:

enter image description here

Dans ce cas, nous avons à nouveau 3 éléments dans un nœud feuille, nous devons donc diviser:

enter image description here

Comme ça. Et qu'en est-il de ce cercle en bas à gauche? Il semble recouper 2 quadrants. Cependant, nous ne considérons qu'un seul point de l'élément (ex: son centre) pour déterminer le quadrant auquel il appartient. Donc, ce cercle n'est en fait inséré que dans le quadrant inférieur gauche.

Cependant, le cadre de délimitation du quadrant inférieur gauche est étendu pour englober ses étendues (affichées en cyan, et j'espère que cela ne vous dérange pas, mais j'ai changé la couleur BG car il devenait difficile de voir les couleurs), et donc les AABB de les nœuds au niveau 2 (représentés en cyan) se déversent dans les quadrants les uns des autres.

Le fait que chaque quadrant stocke son propre rectangle qui est toujours garanti pour englober ses éléments est ce qui nous permet d'insérer un élément dans un seul nœud feuille même si sa zone intersecte plusieurs nœuds. Au lieu de cela, nous développons la boîte englobante du nœud feuille au lieu d'insérer l'élément sur plusieurs nœuds.

Mise à jour des AABB

Cela pourrait donc conduire à la question: quand les AABB sont-ils mis à jour? Si nous développons uniquement les AABB lors de l'insertion d'éléments, ils auront tendance à grossir de plus en plus. Comment les rétrécir lorsque les éléments sont supprimés? Il existe de nombreuses façons de résoudre ce problème, mais je le fais en mettant à jour les boîtes englobantes de toute la hiérarchie dans cette méthode de "nettoyage" décrite dans ma réponse d'origine. Cela semble être assez rapide (ne s'affiche même pas comme un hotspot).

Comparaison avec les grilles

Je n'arrive toujours pas à implémenter cela aussi efficacement pour la détection de collision que mes implémentations de grille hiérarchique, mais encore une fois, cela pourrait être plus à propos de moi que de la structure des données. La principale difficulté que je rencontre avec les arborescences est de contrôler facilement où tout est en mémoire et comment y accéder. Avec la grille, vous pouvez vous assurer que toutes les colonnes d'une ligne sont contiguës et disposées séquentiellement, par exemple, et assurez-vous d'y accéder de manière séquentielle avec les éléments stockés de manière contiguë dans cette ligne. Avec un arbre, l'accès à la mémoire a tendance à être un peu sporadique juste par nature et a également tendance à se dégrader rapidement car les arbres veulent transférer des éléments beaucoup plus souvent lorsque les nœuds sont divisés en plusieurs enfants. Cela dit, si je voulais utiliser un index spatial qui était un arbre, je suis vraiment en train de creuser ces variantes lâches jusqu'à présent, et des idées surgissent dans ma tête pour implémenter une "grille lâche".

Conclusion

C'est donc des quadtrees lâches en un mot, et il a essentiellement la logique d'insertion/suppression d'un quadtree normal qui stocke juste des points, sauf qu'il développe/met à jour les AABB en cours de route. Pour la recherche, nous finissons par parcourir tous les nœuds enfants dont les rectangles coupent notre zone de recherche.

J'espère que les gens ne me dérangeront pas de poster autant de réponses longues. Je reçois vraiment un coup de pied de les écrire et cela a été un exercice utile pour moi de revoir les quadtrees pour essayer d'écrire toutes ces réponses. J'envisage également un livre sur ces sujets à un moment donné (même s'il sera en japonais) et j'écris quelques réponses ici, tout en hâte et en anglais, m'aide à tout mettre en place dans ma tête. Maintenant, j'ai juste besoin que quelqu'un me demande une explication sur la façon d'écrire des octrees ou des grilles efficaces à des fins de détection de collision pour me donner une excuse pour faire de même sur ces sujets.

14
Dragon Energy

Dirty Trick: Tailles uniformes

Pour cette réponse, je couvrirai une astuce sournoise qui peut permettre à votre simulation d'exécuter un ordre de grandeur plus rapidement si les données sont appropriées (ce qui sera souvent le cas dans de nombreux jeux vidéo, par exemple). Il peut vous faire passer de dizaines de milliers à centaines de milliers d'agents, ou de centaines de milliers d'agents à des millions d'agents. Je ne l'ai pas appliqué dans aucune des démonstrations montrées dans mes réponses jusqu'à présent car c'est un peu une triche, mais je l'ai utilisé dans la production et cela peut faire un monde de différence. Et curieusement, je ne le vois pas souvent discuté. En fait, je ne l'ai jamais vu discuté, ce qui est bizarre.

Revenons donc à l'exemple du Seigneur des Anneaux. Nous avons beaucoup d'unités "de taille humaine" comme les humains, les elfes, les nains, les orques et les hobbits, et nous avons également quelques unités ginormeuses comme les dragons et les ents.

Les unités "à taille humaine" ne varient pas beaucoup en taille. Un hobbit peut mesurer quatre pieds de haut et un peu trapu, un orque peut mesurer 6 pi 4 po. Il y a une différence mais ce n'est pas une différence épique. Ce n'est pas un ordre de grandeur.

Alors, que se passe-t-il si nous plaçons une sphère/boîte englobante autour d'un hobbit qui est la taille de la sphère/boîte englobante d'un orc juste pour le plaisir des requêtes d'intersection grossière (avant de creuser pour vérifier une collision plus vraie à un niveau granulaire/fin )? Il y a un peu d'espace négatif gaspillé mais quelque chose de vraiment intéressant se produit.

Si nous pouvons prévoir une telle limite supérieure sur des unités de cas commun, nous pouvons les stocker dans une structure de données qui suppose que toutes choses ont une taille uniforme supérieure. Deux choses vraiment intéressantes se produisent dans ce cas:

  1. Nous n'avons pas besoin de stocker une taille avec chaque élément. La structure de données peut supposer que tous les éléments qui y sont insérés ont la même taille uniforme (uniquement pour les requêtes d'intersection grossière). Cela peut presque réduire de moitié l'utilisation de la mémoire pour les éléments dans de nombreux scénarios et cela accélère naturellement la traversée lorsque nous avons moins de mémoire/données à accéder par élément.
  2. Nous pouvons stocker des éléments dans juste un cellule/nœud, même pour les représentations serrées qui n'ont pas d'AABB de taille variable stockées dans des cellules/nœuds.

Stockage d'un seul point

Cette deuxième partie est délicate mais imaginez que nous ayons un cas comme celui-ci:

enter image description here

Eh bien, si nous regardons le cercle vert et recherchons son rayon, nous finirions par manquer le point central du cercle bleu s'il n'est stocké que comme un seul point dans notre index spatial. Mais que faire si nous recherchons une zone deux fois le rayon de nos cercles?

enter image description here

Dans ce cas, nous trouverions l'intersection même si le cercle bleu n'est stocké que comme un seul point dans notre index spatial (le point central en orange). Juste pour montrer visuellement que cela fonctionne:

enter image description here

Dans ce cas, les cercles ne se croisent pas et nous pouvons voir que le point central est en dehors même du rayon de recherche doublé élargi. Donc, tant que nous recherchons deux fois le rayon dans un index spatial qui suppose que tous les éléments ont une taille de limite supérieure uniforme, nous sommes garantis de les trouver dans une requête grossière si nous recherchons une zone deux fois le rayon de limite supérieure (ou deux fois la demi-taille rectangulaire pour les AABB).

Maintenant, cela peut sembler un gaspillage, car cela vérifierait plus de cellules/nœuds que nécessaire dans nos requêtes de recherche, mais c'est uniquement parce que j'ai dessiné le diagramme à des fins d'illustration. Si vous utilisez cette stratégie, vous l'utiliseriez pour des éléments dont la taille est généralement une fraction de la taille d'un nœud/cellule feuille unique.

Optimisation énorme

Ainsi, une énorme optimisation que vous pouvez appliquer consiste à séparer votre contenu en 3 types distincts:

  1. Un ensemble dynamique (se déplaçant constamment et s'animant) avec une limite supérieure commune comme les humains, les orcs, les elfes et les hobbits. Nous mettons essentiellement la boîte/sphère englobante de même taille autour de tous ces agents. Ici, vous pourriez utiliser une représentation serrée comme un quadtree serré ou une grille serrée et elle ne stockerait qu'un seul point pour chaque élément. Vous pouvez également utiliser une autre instance de cette même structure pour des éléments super minuscules comme des fées et des mèches avec une taille supérieure supérieure de casse commune.
  2. Un ensemble dynamique plus grand que n'importe quelle borne supérieure prévisible comme les dragons et les ents avec des tailles très inhabituelles. Ici, vous pouvez utiliser une représentation lâche comme un quadtree lâche ou ma "double grille lâche/serrée".
  3. Un ensemble statique où vous pouvez vous permettre des structures qui prennent plus de temps à construire ou qui sont très inefficaces à mettre à jour, comme un arbre quadruple pour les données statiques qui stocke tout de manière parfaitement contiguë. Dans ce cas, peu importe l'inefficacité de la mise à jour de la structure de données à condition qu'elle fournisse les requêtes de recherche les plus rapides, car vous ne la mettrez jamais à jour. Vous pouvez l'utiliser pour des éléments de votre monde comme des châteaux, des barricades et des rochers.

Cette idée de séparer les éléments de casse commune avec des étendues de limite supérieure uniformes (sphères ou boîtes englobantes) peut être une stratégie d'optimisation extrêmement utile si vous pouvez l'appliquer. C'est aussi un sujet dont je ne vois pas parler. Je vois souvent des développeurs parler de séparer le contenu dynamique et statique, mais vous pouvez obtenir autant d'amélioration sinon plus en regroupant davantage les éléments dynamiques de taille similaire dans le cas commun et en les traitant comme s'ils avaient des tailles supérieures uniformes pour votre des tests de collision grossière qui ont pour effet de les stocker comme un point infinitésimal qui n'est inséré que dans un nœud feuille dans votre structure de données étanche.

Sur les avantages de la "tricherie"

Cette solution n'est donc pas particulièrement intelligente ou intéressante, mais l'état d'esprit qui la sous-tend est quelque chose qui, selon moi, mérite d'être mentionné, du moins pour ceux qui sont comme moi. J'ai perdu une bonne partie de ma carrière à chercher les solutions "uber": les structures de données et les algorithmes à taille unique qui peuvent parfaitement gérer n'importe quel cas d'utilisation dans l'espoir de pouvoir prendre un peu plus de temps à l'avance pour l'obtenir à droite, puis réutilisez-le comme un fou dans le futur et dans des cas d'utilisation disparates, sans parler de travailler avec de nombreux collègues qui ont cherché la même chose.

Et dans les scénarios où les performances ne peuvent pas être trop compromises en faveur de la productivité, rechercher avec zèle de telles solutions ne peut conduire ni aux performances ni à la productivité. Donc, parfois, il est bon de s'arrêter et d'examiner la nature des exigences particulières en matière de données pour un logiciel et de voir si nous pouvons "tricher" et créer des solutions "sur mesure", plus étroitement applicables à ces exigences particulières, comme dans cet exemple. Parfois, c'est le moyen le plus utile pour obtenir une bonne combinaison de performances et de productivité dans les cas où l'un ne peut pas être trop compromis en faveur de l'autre.

12
Dragon Energy

3. Implémentation C portable

J'espère que les gens ne se soucient pas d'une autre réponse mais j'ai manqué de la limite de 30k. Je pensais aujourd'hui à la façon dont ma première réponse n'était pas très indépendante de la langue. Je parlais de stratégies d'allocation de mem, de modèles de classe, etc., et toutes les langues ne permettent pas de telles choses.

J'ai donc passé un peu de temps à réfléchir à une implémentation efficace qui est presque universellement applicable (une exception serait les langages fonctionnels). J'ai donc fini par porter mon quadtree en C d'une manière telle qu'il ne lui faut que des tableaux de int pour tout stocker.

Le résultat n'est pas joli mais devrait fonctionner très efficacement sur n'importe quelle langue qui vous permet de stocker des tableaux contigus de int. Pour Python il y a des bibliothèques comme ndarray dans numpy. Pour JS il y a tableaux typés . Pour Java et C #, nous pouvons utiliser int tableaux (pas Integer, ceux-ci ne sont pas garantis d'être stockés de manière contiguë et ils utilisent beaucoup plus de mem que les anciens int ).

C IntList

J'utilise donc un structure auxiliaire construite sur des tableaux int pour l'ensemble du quadtree afin de faciliter le portage vers d'autres langages. Il combine une pile/liste libre. C'est tout ce dont nous avons besoin pour mettre en œuvre tout ce dont nous avons parlé dans l'autre réponse de manière efficace.

#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;
}

Utilisation d'IntList

Utiliser cette structure de données pour implémenter tout ne donne pas le plus joli code. Au lieu d'accéder à des éléments et des champs comme celui-ci:

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

... on finit par faire comme ça:

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

... ce qui est dégoûtant, je sais, mais le but de ce code est d'être aussi efficace et portable que possible, pas aussi facile à entretenir que possible. L'espoir est que les gens peuvent simplement utiliser ce quadtree pour leurs projets sans le changer ni le maintenir.

Oh et n'hésitez pas à utiliser ce code que je poste comme vous le souhaitez, même pour des projets commerciaux. J'aimerais vraiment que les gens me fassent savoir s'ils le trouvent utile, mais faites comme vous le souhaitez.

C Quadtree

Très bien, donc en utilisant la structure de données ci-dessus, voici le quadtree en 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);
}

Conclusion temporaire

Ce n'est pas une si bonne réponse mais j'essaierai de revenir et de continuer à l'éditer. Cependant, le code ci-dessus devrait être très efficace sur à peu près n'importe quel langage qui autorise les tableaux contigus d'anciens entiers simples. Tant que nous avons cette garantie de contiguïté, nous pouvons proposer un quadtree très convivial pour le cache qui utilise une très petite empreinte mem.

Veuillez vous référer à la réponse originale pour plus de détails sur l'approche globale.

11
Dragon Energy

4. Java IntList

J'espère que les gens ne me dérangeront pas de poster une troisième réponse mais j'ai à nouveau manqué de limite de caractères. J'ai fini de porter le code C dans la deuxième réponse à Java. Le port Java pourrait être plus facile à consulter pour les personnes effectuant le portage vers des langages orientés objet.

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

Et voici le quadtree en Java (désolé si ce n'est pas très idiomatique; je n'ai pas écrit Java depuis une dizaine d'années environ et j'ai oublié beaucoup de choses) ):

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;
}

Conclusion temporaire

Encore une fois désolé, c'est un peu une réponse de vidage de code. Je reviendrai et le modifierai et essayerai d'expliquer de plus en plus de choses.

Veuillez vous référer à la réponse originale pour plus de détails sur l'approche globale.

7
Dragon Energy