Hiérarchies de volumes englobants

Ce cours vise à vous introduire la notion de volume englobant et de hiérarchie pour accélérer les requêtes spatiales.

Recherche du point le plus proche

Une première requête spatiale que nous pouvons aborder est la recherche du point le plus proche parmi un ensemble de points donnés. Commençons par générer un ensemble de points

Approche Naïve

Nous pouvons maintenant proposer la recherche naïve du point le plus proche, simplement en recherchant linéairement dans le tableau de points celui qui minimise la distance à la requête.

Nous pouvons désormais tester les temps de calcul de notre algorithme sur des ensembles de points allant de 1 à 1000 points.

Comme on pouvait s'en douter, l'algorithme est linéaire en le nombre de points. Notre but par la suite sera de faire mieux. Bien sûr, si le but est de ne faire qu'une requête de plus proche voisinage, il ne sera à rien d'espérer faire mieux, car sans organisation particulière des points, ils devront nécessairement tous être testés. Lorsque le nombre de requêtes avec le même ensemble de points augmente, il devient alors envisageable de réaliser un précalcul pour organiser les points, et ainsi rendre les requêtes plus efficaces.

Boîte englobante alignée sur les axes

Pour accélérer les choses, une technique classique est d'utiliser des englobants. Un englobant dans le cadre d'une recherche de point le plus proche est un volume pour lequel il est facile de calculer la distance à un point. Ainsi, la distance de ce qui est à l'intérieur de l'englobant est nécessairement plus grande que la distance à l'englobant (vu qu'il faut rentrer dans l'englobant pour atteindre son contenu). En regroupant les points dans des englobants, on peut donc tester d'abord l'englobant, et si la distance à l'englobant est élevée, ne pas aller plus loin avec son contenu.

Ci dessous, je propose d'utiliser des boîtes englobantes alignées sur les axes (Axis Aligned Bounding Boxes abbréviées en AABB en anglais). Le fait d'être "alignée sur les axes" signifie que les côtés de la boîte sont parallèles aux axes du repère. Ci-dessous en 2D, les axes x et y. Il est donc possible de définir ces boîtes simplement à partir du minimum et du maximum de la boîte sur chaque axe.

boîte englobante alignée sur les axes

Dans le code ces deux bornes sont représentées par les points min = (xmin,ymin) et max = (xmax, ymax).

Pour contruire la boîte, les points sont ajoutés un à un, et le minimum et le maximum sur chaque axe sont conservés. Après avoir introduit tous les points, nous obtenons la boîte alignée sur les axes minimale qui englobe nos points.

La distance d'un point à une boîte est la longueur du plus court chemin permettant d'aller du point à la boîte. La méthode nearest fournie ci-dessous permet d'obtenir le point à l'intérieur de la boîte le plus proche du point fourni en paramètre. Si la requête est un point dans la boîte, le même point est retourné. Sinon le point est à la surface de la boîte. Le principe de cette fonction est que sur chaque axe (x ou y) on compare la coordonnées de la requête aux bornes de la boîte. Si la requête est plus petite que la borne minimale de la boîte, elle est en dehors de la boîte et le point le plus proche aura comme coordonnée sur cet axe la borne de la boîte. De même si la coordonnée est plus grande que la borne max. Sinon, le point le plus proche aura la même coordonnée que la requête, pour minimiser le terme de la distance correspondant à cet axe (on aura alors 0 en faisant la différence des coordonnées).

Des paquets de points

Pour accélérer les recherches de point le plus proche, une approche classique est de séparer les points en groupes, disposant chacun d'un englobant. Avant de tester les points du groupe, on teste la distance de l'englobant. S'il est trop loin, il est inutile de traiter les points à l'intérieur. Dans l'exemple ci-dessous on répartit les points en coupant l'espace en quatre.

Il suffit maintenant de chercher le point le plus proche paquet par paquet, en testant la boîte au préalable.

Testons maintenant les performances de notre nouvel algorithme de recherche de point le plus proche

On voit que comparé aux performances de l'algorithme naïf, dans le pire des cas le temps de calcul est à peu près le même que pour l'algorithme naïf, mais qu'on va jusqu'à quatre fois plus vite. On distingue nettement les quatres droites dans la complexité, selon que la requête testée a nécessité la visite d'une, deux, trois ou quatre paquets de points. La droite correspondant à deux boîtes semble être la plus fournie.

On pourrait se dire qu'en utilisant 8 boîtes au lieu de quatre on fait encore mieux.

Il faut néanmoins garder en tête que l'utilisation des boîtes n'est pas gratuite et introduit un surcoût : il faut calculer la distance aux boîtes. Au bout d'un moment, trop de boîtes tuent les performances.

On voit bien ici que pour 1000 points, au delà d'une grille de boîtes de 5 par 5, les performances commencent à se dégrader. Avec une grille 5 par 5 le nuage de 1000 points est découpé comme suit.

La recherche ci-dessus utilisant une grille est naïve, une amélioration simple consiste à parcourir les boîtes dans un ordre dépendant de la position de la requête. En effet, plus le plus proche voisin est trouvé rapidement, plus les boîtes seront éliminées rapidement par une distance trop éloignée. Une méthode classique consiste à commencer par la boîte qui contient la requête, puis de faire un parcours en largeur de la grille à partir de cette boîte tant que les boîtes ne sont pas trop loin.

Découpage hiérarchique, Bounding Volume Hierarchies (BVH)

L'approche hiérarchique consiste à mettre des boîtes dans des boîtes. Au fond, dans l'approche précédente, lorsqu'on a beaucoup de boîtes, l'algorithme dégénère en une recherche linéaire sur les boîtes. On peut alors se dire qu'on pourrait accélérer cette recherche en faisant à nouveau des paquets de boîtes dans de plus grosse boîtes. Énoncé dans l'autre sens, il s'agit de d'itérer l'approche par paquets : l'ensemble de points initial est découpé en gros paquets. Au lieu de faire une recherche linéaire sur ces gros paquets, on les divise eux-mêmes récursivement en plus petits paquets de points, dans des boîtes plus petites, qui eux-mêmes sont divisés en paquets, et ainsi de suite jusqu'à une limite sur la taille des paquets à partir de laquelle on fait une recherche linéaire. On obtient alors un arbre dont la racine correspond à l'ensemble de points, et chaque enfant d'un nœud correspond à une partie de ces points, avec la boîte qui les englobe.

Il existe de très nombreuses stratégies pour déterminer les paquets à réaliser pour augmenter l'efficacité de la recherche. Ci-dessous nous utilisons une technique simple et classique. La découpe d'un nœud est réalisée en déterminant un plan de coupe perpendiculaire à un des axes (x, y ou z). Plus simplement on détermine une coordonnées, et une valeur, et tous les points ayant cette coordonnée plus petite que la valeur sont dans un enfant, les autres sont dans l'autre enfant. Ici, pour déterminer le plan de coupe, on utilise la dimension le long de laquelle la boîte du nœud est la plus allongée.

La recherche est réalisée en réalisant un parcours en profondeur de l'arbre, mais en ne descendant que dans les nœuds qui sont suffisamment proches de la requête. De plus, à chaque nœud, la descente commence par l'enfant le plus proche de la requête.

Nous pouvons donc maintenant tester les performances de cette nouvelle approche. Notez que nous ne comptons pas ici dans le temps de calcul la création de l'arbre, en considérant que c'est un précalcul qui est amorti sur un grand nombre de recherches de plus proche voisin réalisées par la suite. Si le nombre de requêtes est faible, la création d'un tel arbre est alors absurde. En effet, chaque niveau de l'arbre correspond à un découpage de l'ensemble des points, et le calcul des boîtes englobantes de ce niveau a donc nécessité le parcours de l'ensemble des points. Si on considère que les points se répartissent équitablement à chaque découpe d'un nœud, la hauteur de l'arbre est alors le log du nombre de points, et la complexité de la création de l'arbre est donc de l'ordre de $n\log_2 n$ si $n$ est le nombre de points. Il faut donc au moins de l'ordre de $\log_2 n$ requêtes pour amortir ce coût de construction, sans quoi utiliser des recherches linéaires est plus rapide que de construire l'arbre.

On constate sur la courbe ici que le temps de calcul semble être de l'ordre du log du nombre de points.

Intersection rayon objet

Beaucoup de requêtes spatiales peuvent être accélérées avec le même principe. En gros pour que la technique de la hérarchie de volumes englobant, il est nécessaire :

  1. qu'il soit facile de calculer un englobant sur les objets
  2. que la requête soit telle qu'un test sur l'englobant permette d'éliminer son contenu
  3. qu'il soit facilement possible de réaliser le test sur l'englobant

Typiquement, pour la synthèse d'images, la brique de base de toutes les méthodes de rendu est le lancer de rayons, qui nécessite de pouvoir calculer l'intersection entre un rayon et les objets de la scène. Cette requête se prête très bien à la subdivision spatiale, car :

  1. il est facile de mettre des triangles dans des englobants
  2. il est facile de faire un test d'intersection rayon / boîte ou rayon / boule
  3. pour intersecter un objet, un rayon doit intersecter la boîte qui le contient

Ci dessous nous allons illustrer ce principe sur une scène 2D composée de disques.

On peut facilement adapter le code des boîtes englobantes alignées sur les axes pour contenir des disques plutôt que des points.

Un rayon est carractérisé par :

Il contient tous les points de la demi-droite issue de l'origine, dans la direction donnée.

Pour calculer l'intersection entre une sphère (ici en 2D un cercle mais le même code fonctionne en 3D) et un rayon, il faut commencer par exprimer mathématiquement ce que signifie pour un point être sur un rayon et être sur une sphère. Si $\mathbf{o}$ est l'origine du rayon et si $\mathbf{d}$ est sa direction, un point $\mathbf{p}$ est sur le rayon s'il peut s'écrire comme

$$ \mathbf{p} = \mathbf{o} + t \mathbf{d}, $$

avec $t$ un réel positif. À chaque valeur de $t$ correspond un unique point du rayon. Plus $t$ est élevé, plus le point est loin de l'origine. Si $\mathbf{c}$ est le centre de la sphère et si $r$ est son rayon, un point est sur la sphère s'il est à une distance $r$ du centre :

$$ \lVert \mathbf{p} - \mathbf{c} \rVert = r. $$

Il est néanmoins plus facile de travailler avec le carré de la distance, pour éviter la racine carrée dans son expression :

$$ \lVert \mathbf{p} - \mathbf{c} \rVert^2 = r^2. $$

Cette expression est équivalente à la précédente car nous savons que la distance et le rayon de la sphère sont positifs. Si le rayon intersecte la sphère, le point d'interserction doit donc respecter ces deux contraintes. Nous obtenons donc le problème suivant : chercher $t \geq 0$ tel que les deux contraintes soient respectées.En combinant les deux expressions, nous obtenons l'équation à résoudre :

$$ \lVert \mathbf{o} + t \mathbf{d} - \mathbf{c} \rVert^2 = r^2. $$

La norme au carré d'un vecteur $\mathbf{v}$ peut être exprimée via le produit scalaire $\lVert \mathbf{v} \rVert^2 = \mathbf{v}.\mathbf{c}$. Nous obtenons ainsi

$$ (\mathbf{o} + t \mathbf{d} - \mathbf{c}).(\mathbf{o} + t \mathbf{d} - \mathbf{c}) = r^2 $$

et en développant

$$ \lVert \mathbf{o} - \mathbf{c} \rVert^2 - 2t(\mathbf{o} - \mathbf{c}).\mathbf{d} + t^2\lVert \mathbf{d} \rVert^2 = r^2 $$

nous obtenons ainsi une équation du second degré en $t$, que l'on peut écrire classiquement en

$$ \lVert \mathbf{d} \rVert^2 t^2 - 2(\mathbf{o} - \mathbf{c}).\mathbf{d}t + \lVert \mathbf{o} - \mathbf{c} \rVert^2 - r^2 = 0 $$

Selon le discriminant, nous obtiendrons soit aucune solution (pas d'intersection), soit une solution (rayon tageant à la sphère, cas particulier), ou deux solutions. Seules les solutions où $t$ est positif sont à conserver, car les autres sont derrière l'origine du rayon.

En synthèse d'image, on cherche souvent à résoudre le problème de la visibilité : étant donné un rayon et un ensemble d'objets, quel est le premier objet touché par le rayon. C'est par exemple la requête que l'on fait pour déterminer les objets vus par la caméras, en lançant des rayons depuis la caméra.

un lancer de rayons depuis une caméra

Pour calculer cette première intersection, un algorithme naïf consiste à calculer toutes les intersection, puis ne garder que la plus proche.

Cette approche est linéaire en le nombre de sphères à tester, comme on peut s'en douter.

De même que pour les points, on peut disposer les sphères dans une hiérarchie de volumes englobants. Il faut ici déterminer comment répartir les sphères en fonction de la position de découpe, car une sphère peut être des deux côtés de la coupe. Ici nous avons choisi de ne considérer que le centre de la sphère pour la répartition.

Pour pouvoir exploiter notre arbre pour des requêtes de lancer de rayons, il est nécessaire de pouvoir déterminer si un rayon passe dans une boîte ou non. S'il ne passe pas, il ne pourra pas non-plus rencontrer le contenu de cette boîte. L'intersection entre un rayon et une boîte consiste à calculer pour chaque axe l'intersection entre le rayon et la tranche d'espace contenue entre les deux bornes de la boîte. Pour la coordonnées $x$ et la borne inférieure $x_{min}$ de la boîte par exemple, on résoud :

$$ o_x + t d_x = x_{min} \Leftrightarrow t = \frac{x_{min} - o_x}{d_x} $$

où $o_x$ est la coordonnée $x$ de l'origine du rayon et $d_x$ celle de sa direction. On obtient alors pour chaque coordonnée un intervalle $[t_{min}, t_{max}]$ pour lequel le rayon traverse la tranche d'espace comprise entre les deux bornes de la boîte. Un rayon traverse la boîte si l'intersection entre les intervalles sur chaque axe est non vide. Il suffit donc de vérifier que le maximum des $t_{min}$ sur toutes les coordonnées est toujours plus petit que le minimum des $t_{max}$ pour toutes les coordonnées.

Nous pouvons maintenant exprimer la traversée de l'arbre pour exploiter sa structure. Ici nous avons de multiples cas à traiter.

On voit de nouveau des performances qui semblent confirmer une complexité de l'ordre de $O(\log{n})$.