Comment construire des tables de hachage spatiales | XPBD

Examinons les tables de hachage spatiales et comment les construire. Celles-ci seront très pratiques lorsque nous implémenterons les autocollisions dans notre simulation de tissu XPBD.

Keywords: infographie, tutoriel, structures de données, leetcode, autocollisions

By Carmen Cincotti  

Une table de hachage spatiale est un type particulier de table de hachage, qui est un sujet dont nous avons déjà beaucoup parlé en détail.

Les tables de hachage spatiales traitent des positions dans l’espace.

Une table de hachage spatiale est un bon outil pour détecter les collisions entre objets et particules, ce qui s’avérera utile pour des simulations de tissu, des moteurs physiques de jeux vidéo, etc.

Voyons ensemble comment fonctionne une telle structure !

Une revue des tables de hachage

Avant de continuer, je vous suggère que si vous n’avez pas déjà lu les articles précédents sur l’adressage ouvert et les tables de hachage, je vous recommande vraiment d’y jeter un coup d’œil afin de pouvoir suivre plus facilement :

Qu’est-ce qu’une table de hachage spatiale ?

Une table de hachage spatiale ressemble à une table de hachage ordinaire. Cependant, la différence la plus frappante est qu’une table de hachage spatiale utilise des emplacements (2D ou 3D) dans l’espace comme clés de hachage pour entrer dans la table.

table de hachage

Voyons ensemble un exemple.

Un exemple

Pour cet exemple, il y a une particule qui s’est installée dans l’espace à la position posp=(1,3,1)\text{pos}_p = (1, 3, -1).

Particle at 1 3 -1

Pour stocker cette particule dans une table de hachage, on utilise une fonction de hachage qui prend comme entrée la position de la particule.

Hash function returns 1

La fonction de hachage nous renvoie un entier qui sert d’indice dans la table de hachage spatiale. La particule est stockée à cet indice.

Particle 1 stored in hash table

Comment hacher une position

J’utilise cette fonction qui vient de Matthias Müller :

hashCoords(xi: number, yi: number, zi: number) { const hash = (xi * 92837111) ^ (yi * 689287499) ^ (zi * 283923481); return Math.abs(hash); }

L’idée est de renvoyer un entier pseudoaléatoire. Ainsi, la fonction de hachage est moins susceptible de renvoyer des hachages qui causent des collisions de hachage.

Table de hachage spatiale dense

Une implémentation dense d’une table de hachage spatiale a été proposée par l’auteur de Ten Minute Physics, Matthias Müller lors d’une de ses vidéos.

L’implémentation dense nous offre une complexité temporelle et spatiale très efficiente.

Seuls deux tableaux sont utilisés. :

  • Tableau de particule - Il stocke les références aux particules elles-mêmes.
  • Tableau de comptage - Il stocke le nombre de particules hachées dans un seau particulier, qui sert finalement de l’indice dans le Tableau de particule.

Spatial hash map example

On verra comment ceci fonctionne dans la partie suivante !

Comment remplir une table de hachage dense

Pour clarifier cette idée, voyons d’étape en étape comment remplir une table de hachage dense.

Espace délimité

On commence avec cinq particules dans l’espace comme ceci :

Five particles

Étape 1 : Créer une grille

Imaginons que nous créons une grille superposée sur nos particules :

Overlay a grid over 5 particles

Définissons quelques propriétés de cette grille

  • Espacement (spacing) : la taille de chaque carré de la grille. Une bonne règle empirique consiste à définir l’espacement comme deux fois le rayon d’une particule, 2r2r.
  • (nx,ny)(n_x , n_y) : le nombre de carrés dans chaque direction (et nzn_z dans le cas 3D).

À ce stade, c’est notre travail de créer un tableau qui représente une telle grille. Parce que ce tableau va être désigné pour compter des particules dans chaque carré de la grille - nous l’appellerons cellCount :

Create array named cellCount

Ajouter un espace supplémentaire

Note : On ajoute un espace supplémentaire qui sera utile lorsqu’on fait une requête - ou un query - pour les particules qui se trouvent autour d’un emplacement d’intérêt. Ignorez cela pour le moment, nous y reviendrons plus tard !

Étape 2 : Remplir le tableau de comptage

Ensuite, nous parcourons toutes les particules pour remplir cellCount. Pour chaque particule qui appartient à un seau particulier, on incrémente le compteur de 11.

Incrément

Comment calculer le seau auquel appartient une particule

Nous utilisons la position d’une particule. Voici une méthode potentielle pour calculer l’indice ii que nous allons utiliser pour indexer dans cellCount :

xi=Math.floor(pxspacing)yi=Math.floor(pyspacing)i=xiny+yi x_i = \text{Math.floor}(\frac{p_x}{\text{spacing}}) \\[6pt] y_i = \text{Math.floor}(\frac{p_y}{\text{spacing}}) \\[6pt] i = x_i * n_y + y_i \\[6pt]

… et en 3D :

zi=Math.floor(pzspacing)i=(xiny+yi)nz+zi z_i = \text{Math.floor}(\frac{p_z}{\text{spacing}}) \\[6pt] i = (x_i * n_y + y_i) * n_z + z_i \\[6pt]

Avoir une particule à la position (1.5,2.3)(1.5, 2.3) et une table avec une taille de (nx,ny)=(3,3)(n_x, n_y) = (3, 3) et avec spacing égal à 11 nous donne :

xi=Math.floor(1.51)=1yi=Math.floor(2.31)=2i=13+2=5 x_i = \text{Math.floor}(\frac{1.5}{1}) = 1 \\[6pt] y_i = \text{Math.floor}(\frac{2.3}{1}) = 2 \\[6pt] i = 1 * 3 + 2 = 5 \\[6pt]

L’indice est égal à 55.

Comme vous l’avez probablement déjà compris, cet exemple que nous venons de voir dépend de plusieurs variables, comme l’emplacement de l’origine. Utilisez cet exemple comme guide et non comme solution.

Étape 3 : Calculer la somme partielle

La prochaine étape est facile. Nous devons calculer la somme partielle sur le tableau cellCount. Nous pouvons l’écraser avec ces nouvelles valeurs.

Overwrite cellCount with partial sums

Étape 4 : Remplir le tableau de particules

Enfin, nous parcourons sur toutes les particules pour les stocker dans le tableau particleMap (la table de particules).

Create particleMap

Prenons par exemple la particule 55 :

  1. Tout d’abord, on index dans cellCount à l’indice 77, car la particule 55 est là.
  2. Ensuite, on décrément cellCount à cet indice, ce qui change l’entier stocké ici de 44 à 33.
  3. Ce nouvel entier est utilisé comme indice dans particleMap. Nous stockons la particule 55 à l’indice 33 dans particleMap.

Vous pouvez bien sûr rendre cette méthode plus performant en combinant quelques étapes afin d’éviter plusieurs boucles supplémentaires.

Espace illimité

Commençons avec cinq particules dans un espace illimité comme ceci :

Five particles

Note bien que dans cet exemple, l’espace est illimité. Le processus de la section précédente n’est donc plus suffisant !

Étape 1 : Créer une grille

Dans ce cas, la grille est illimitée :

An unbounded grid

Ce n’est donc pas une bonne idée d’essayer de représenter une telle structure avec un tableau sous-jacent de stockage d’une taille égale, car on risque de trop gaspiller trop de mémoire.

Comment éviter ce problème ?

La réponse : On crée un tableau de taille fixe auquel ajouter toutes les particules. Cette implémentation nécessitera de résoudre les collisions de hachage !

Il faut définir deux paramètres :

  • Espacement (spacing) : la taille de chaque carré de la grille. Une bonne règle empirique consiste à définir l’espacement comme deux fois le rayon d’une particule, 2r2r.
  • La taille de la table : La taille (tableSize) du tableau sous-jacent de stockage (cellCount).

Dans ce cas - on définit tableSize égale à 9 plus 1, ce qui fait 10 :

Initialize cellCount array

Étape 2 : Remplir le tableau de comptage

Ensuite, nous parcourons toutes les particules pour remplir cellCount. Cette fois, nous utilisons une fonction de hachage pour indexer dans cellCount afin d’insérer des particules.

Iterate through particles and increment cellCount elements

Pour indexer dans le tableau, nous pouvons utiliser cette formule :

i=hash(xi,yi)modtableSize i = \text{hash}(x_i, y_i) \mod \text{tableSize}

Mais qu’est-ce que xix_i et yiy_i ? Nous pourrions naïvement les définir comme les coordonnées de la particule elle-même, mais cela signifierait que les particules d’un même carré pourraient potentiellement être hachées dans différents seaux !

Par conséquent nous devons normaliser les coordonnées d’une particule à la place de la position du carré dans lequel elle réside.

Pour le faire, nous pouvons utiliser la fonction hashPos qui appelle intCoord, qui renvoie une coordonnée d’un carré dans la grille :

intCoord(coord: number) { return Math.floor(coord / spacing); } hashPos() { return hashCoords( intCoord(pos_x), intCoord(pos_y), intCoord(pos_z) ); }

Et nous avons déjà défini notre fonction de hachage, hashCoords, ci-dessus.

Nous pouvons maintenant garantir que toutes les particules dans un certain carré de la grille seront hachées au même endroit.

Comment résoudre les collisions de hachage

Comme vous pouvez le voir, les particules 22 et 33 se résolvent au même seau. Il nous suffit d’incrémenter l’entier dans cellCount à cet indice. Nous verrons pourquoi cela fonctionne dans les étapes suivantes.

Étape 3 : Calculer la somme partielle

Comme avant, nous devons calculer la somme partielle sur le tableau cellCount. Nous pouvons l’écraser avec ces nouvelles valeurs.

Partial sums

Étape 4 : Remplir le tableau de particules

Enfin, nous parcourons toutes les particules pour les stocker dans le tableau particleMap (la table de particules).

Prenons par exemple la particule 55 :

Map to particle map

  1. Tout d’abord, on index dans cellCount à l’indice 88, car la particule 55 est hachée vers cet emplacement.
  2. Ensuite, on décrément cellCount à cet indice, ce qui change l’entier stocké ici de 33 à 22.
  3. Ce nouvel entier est utilisé comme indice dans particleMap. Nous stockons donc la particule 55 à l’indice 22 dans particleMap.

Prochainement…

On verra comment effectuer des requêtes dans la table de hachage spatiale, et voir le code d’une telle structure !

Des ressources (en français et anglais)


Comments for Comment construire des tables de hachage spatiales | XPBD



Written by Carmen Cincotti, computer graphics enthusiast, language learner, and improv actor currently living in San Francisco, CA.  Follow @CarmenCincotti

Contribute

Interested in contributing to Carmen's Graphics Blog? Click here for details!