|
INFO607
|
L'objectif de ce TP est de vous faire mettre en oeuvre des algorithmes efficaces pour détecter les collisions entre objets dans le plan, au travers d'une application de simulation de déplacement de particules.
Un premier moteur temps-réel de visualisation, basé sur la bibliothèque GTK (http://www.gtk.org), vous est fourni. Vous vous en êtes déjà servi en cours de C, INFO505 (http://www.lama.univ-savoie.fr/~lachaud/Cours/INFO504/Tests/doc/html/tp3.html) pour le TP Tetris graphique. Vous reconnaîtrez l'utilisation des timers et des zones de dessins pour une application animée en temps-réel.
Pas mal de choses vous sont déjà fournis, histoire de se concentrer un peu plus sur les requêtes de proximité. On vous donne les fichiers suivants:
Prenez d'abord le temps de bien comprendre le code écrit. Un Makefile vous est aussi fourni.
Il s'exécute ainsi:
prompt$ make prompt$ ./main
L'exécution du programme donne quelque chose comme ceci:
Les points suivants sont à mémoriser pour réaliser le tp:
Les particules ont leurs coordonnées réelles dans une fenêtre [-1:1]x[-1:1]. Il faut donc transformer leurs coordonnées avant affichage en coordonnées pixel, ici [0:499]x[0:499], car la zone de dessin fait 500x500. Grâce à cela, si vous voulez augmenter ou diminuer la taille de la zone de dessin, vous pouvez le faire très simplement en modifiant le contexte.
Pour rendre le reste du TP moins déterministe et plus joli visuellement, on introduit de l'aléatoire dans la génération des particules. Créez donc une fonction fontaineVariable par exemple avec le prototype ci-dessous:
Par rapport à fontaine, le paramètre var donne la variabilité sur la vitesse initiale et sur la masse. Par exemple si var vaut 0.1 alors la composante x de la vitesse à une valeur aléatoire entre (1-0.1)*vx et (1+0.1)*vx, pareil pour la composante y. Utiliser dans le générateur aléatoire rand pour créer des particules avec la variabilité var.
Par exemple, si vous placez dans tic les lignes ci-dessous pour générer les particules:
Cela donne à peu près le résultat suivant:
rand() / (double) RAND_MAX vous retourne un nombre aléatoire entre 0 et 1. Faites donc une fonction rand01() qui vous retourne un nombre entre 0 et 1 ainsi:On va laisser l'utilisateur rajouter des obstacles (des boules) infranchissables pour nos particules, qui vont rebondir contre. Ces obstacles seront placés dans un premier temps dans un simple tableau de taille variable (comme les particules). Pour détecter si une particule rencontre un obstacle, on parcourera tous les obstacles et on vérifiera s'il y a eu collision. Dans la section suivante, on verra comment utiliser les arbres k-D pour limiter le nombre de tests de collision.
On va créer des obstacles simples pour les particules. Ce seront des disques de rayon donné. Créez deux fichiers obstacles.h et obstacles.c pour stocker les structures de données associées aux obstacles. Un Obstacle sera donc
Sinon, vous créerez une structure TabObstacle complétement similaire à TabParticules, afin de stocker une liste d'obstacles. (Ab)usez du copier-coller pour faire les fonctions suivantes:
N'oubliez pas de rajouter la compilation de obstacles.c dans le Makefile, ainsi que l'édition des liens de obstacles.o.
Pour ajouter des obstacles pour les particules, on va simplement demander à l'utilisateur de faire un clic gauche dans la zone de dessin. Pour capter un clic souris dans la zone de dessin il faut rajouter les lignes suivantes dans creerIHM :
La réaction appelée est la fonction mouse_clic_reaction, qui aura la forme suivante:
Il vous reste donc à:
TabObstacles TabO, et l'initialiser dans le main.mouse_clic_reaction pour qu'il ajoute dans TabO un nouvel Obstacle aux coordonnées cliquées. Attention il faut utiliser la fonction drawingAreaPoint2Point pour convertir les coordonnées pixel en coordonnées réelles. On choisira dans un premier temps un rayon réel r de 0.05, et une atténuation att de 0.6.
Vous allez maintenant modifier la fonction deplaceParticule pour qu'elle bloque la particule si elle rencontre un obstacle. Pour faire simple dans un premier temps, vous parcourez tout le tableau d'obstacles. Dès que la distance entre les nouvelles coordonnées de la particule et un obstacle est plus petite que le rayon de l'obstacle (ici 0.05), cela veut dire que la particule est dans l'obstacle. Dans ce cas-là, vous vous contenterez de mettre à zéro la vitesse de la particule. Si la particule ne collisionne avec aucun obstacle, alors vous mettez à jour sa position comme dans la fonction deplaceParticule initiale.
distance ou une fonction Point_distance dans points.c pour calculer les distancesVoilà à quoi ressemble l'application maintenant:
Vous voyez maintenant le nombre de tests "distance" réalisés par seconde par l'application. Ce nombre est directement proportionnel au nombre d'obstacles et au nombre de particules.
Pour que l'application soit plus réaliste, on va simuler un comportement physique des particules. En général, on rebondit sur un obstacle, comme les boules au billard rebondissent sur les côtés. Tout le calcul du rebond vous est donné dans la fonction suivante. Le principe est de simuler un rebond parfait, puis d'atténuer la force du rebond.
Il vous reste donc à modifier deplaceParticule pour qu'il utilise calculRebond en cas de rencontre avec un obstacle. Pour simplifier, on supposera que la particule rebondit sur le premier obstacle rencontré (les autres sont ignorés).
On voit que notre simulateur doit faire un nombre considérable de tests de collision, dès lors qu'on met un certain nombre d'obstacles. Ce serait encore plus vrai si on gérait les collisions entre particules. Comme nos obstacles sont statiques (ne se déplacent pas), on va les structurer dans un arbre k-D. Du coup on n'aura pas besoin de tester pour chaque particule ses collisions possibles avec tous les obstacles.
Les arbres k-D sont des arbres binaires avec juste un fonctionnement un peu particulier. On vous donne donc les fichiers arbre.h et arbre.c, qui vous fournissent la structure Arbre et des fonctions pour créer, modifier ou se déplacer dans les arbres.
Les arbres k-D sont décrits dans la Leçon 5 d'INFO607. Les arbres k-D qui sont des arbres binaires. Chaque point situé à un noeud de l'arbre coupe l'espace en deux régions selon un plan qui dépend de la profondeur dans l'arbre. Ainsi les noeuds de profondeur 0 coupe l'espace selon leur première coordonnée \( x \), les noeuds de profondeur 1 coupe l'espace selon leur deuxième coordonnée \( y \), les noeuds de profondeur 2 coupe l'espace selon leur troisième coordonnée \( z \) en 3D, etc, en recommançant à la première coordonnée après avoir coupé selon la dernière.
Il est facile d'équilibrer les arbres k-D. Il suffit en effet à chaque fois de choisir comme point de découpe le point situé à la médiane des points restants selon la direction de découpe. Une illustration est donnée ci-dessous.
On va donc enrichir le module arbre.h / arbre.c . Vous définirez le type Donnee comme étant un Obstacle. Il s'agira ensuite d'écrire la fonction suivante, en utilisant bien sûr les fonctions données ArbreVide, Creer0, Creer2 :
Cette fonction suit le pseudo-code de la fonction de création d'arbre k-D vue en cours:
Pour les fonctions MedianeSelonAxe et Partition, je vous conseille de les remplacer par une fonction de tri selon l'axe a, qui sera plus facile à écrire, quoique moins optimal. Pour le tri, vous pouvez faire un tri "maison" (genre bulle) ou utiliser qsort de stdlib.h (en utilisant une variable globale indiquant la dimension où vous triez).
Vous rajouterez alors le code suivant dans votre main:
kdtree dans le contexte (au début initialisé à ArbreVide).Obstacle, on va recréer tout l'arbre (ce n'est pas optimal, mais en fait on ne le fait qu'à chaque clic). Donc on appelle d'abord Detruire sur l'arbre du contexte, avant d'en recréer un nouveau avec KDT_Creer qui contient un obstacle de plus.viewerKDTree, ce qui vous permettra de déboguer votre fonction de création d'arbre.On l'appelle ainsi dans expose_evt_reaction :
Vous obtenez alors ce genre de résultat, si votre fonction de création d'arbre k-D fonctionne !
Vous êtes presque au bout de vos peines. Il s'agit maintenant juste d'être un peu plus intelligent dans la fonction deplaceParticule, en n'appelant la fonction distance que sur les obstacles suffisamment près de la particule. On va donc utiliser l'algorithme PointsDansBoule vu en-cours et de prototype :
Ensuite, dans deplaceParticule, on ne teste que les obstacles qui ont été détectés par KDT_PointsDansBoule. Une partie du code est donné ci-dessous:
On note qu'ici on a choisi le même rayon de localisation 0.05 que la taille des obstacles. En fait, il faut juste veiller à ce que le rayon d'un obstacle soit inférieur ou égal à ce rayon de localisation. Au bout du compte, votre application ressemble à ceci. On note qu'il y a beaucoup moins d'appels à la fonction distance par seconde, alors même qu'il y a plus de particules et plus d'obstacles !
Vous avez toute liberté pour aller un peu plus loin. Le plus dur est fait. On peut s'amuser à étoffer simplement l'application de beaucoup de façons:
On note que gérer les collisions entre particules elles-mêmes est plus délicat, en tous cas avec les arbres k-D. Il faut en effet recréer les arbres stockant les particules à chaque itération.
make, ça doit compiler sans erreur et sans warning (dans vos sources).