Recherche
Peut être aimerez-vous...
Sections du site
Sites Neamar
Lisez ces articles !
| Positionnement automatique d'un arbre |
|
| Programmation et tuning - Algorithmie et optimisation |
| Écrit par Neamar |
| Jeudi, 29 Avril 2010 13:23 |
|
Lors de la réalisation du jeu A-Graphe, je m'étais frotté à « la représentation graphique automatisée d'arbres mathématiques ». Ouch, dit ainsi ça fait mal. Pour ceux qui ne connaîtraient pas, un arbre est une forme particulière de graphe dans lequel on n'autorise pas les cycles : c'est-à-dire que si on descend d'élément en élément, on atteindra forcément un nœud final (les nœuds atteints de cette façon sont nommés feuille). À l'inverse, si on remonte n'importe quel élément, on finira toujours par tomber sur le même nœud (la racine de l'arbre). Un arbre au sens strict est une structure mathématique abstraite de sa représentation graphique : si l'on veut, on peut représenter tous les nœuds (le terme nœud désigne les sommets ; donc la racine, les feuilles, et les nœuds qui les relient) au même point (au détriment de la lisibilité) ou faire des imbroglios de « fils » en plaçant les éléments au hasard. Bien entendu, on a souvent besoin de présenter une information claire à l'internaute, et on ne peut se permettre de lui donner un pâté immonde (sauf si le but du jeu est de démêler ledit pâté). Le but de cet article sera d'envisager deux façons de structurer l'arbre automatiquement : d'abord, par un positionnement statique (le plus simple), ensuite par un positionnement dynamique (plus complexe, mais avec des meilleurs résultats). Allez, la partie aride est terminée, maintenant on s'amuse ! Pré-requisPour illustrer mes propos, je passerai encore une fois par le langage ActionScript3 utilisé par Flash.
La majorité du code source reste le même dans les deux cas (statique et dynamique), je ne détaillerai donc pas le code, juste les algorithmes. Représentation graphique d'un arbre par la méthode statiqueL'approche la plus intuitive est la méthode statique. Au début, l'amplitude est donc de 360°. Si on a quatre enfants, chacun des enfants d'enfants aura alors 90° de liberté. Ces 90° seront centrés autour de l'angle du parent : pour le premier enfant, on placera les enfants d'enfants entre -45° et 45° (ce qui donne bien une amplitude – centrée – de 90°), pour le second entre 90-45° et 90+45°, et ainsi de suite, le 90 correspondant ici à l'angle que fait l'enfant avec la racine. /** * Permet un placement de l'arbre par une méthode récursive. * @param Noeud Le noeud à placer. Ses enfants seront ensuite placés récursivement. * @param Rayon Le rayon d'écart avec ses enfants * @param Amplitude L'angle à utilier : 2Pi au début, il se réduit au fur et à mesure de l'enfoncement dans l'arbre. * @param Angle L'angle de départ. */ private function placement(Noeud:Node,Rayon:search?q=int%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:int.html&filter=0&num=100&btnI=lucky">int,Amplitude:search?q=Number%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Number.html&filter=0&num=100&btnI=lucky">Number=2*search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.PI,AngleCentral:search?q=Number%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Number.html&filter=0&num=100&btnI=lucky">Number=0):void { //Calculer le pas qui sépare deux enfants. Par exemple, si on est sur le noeud principal et qu'on a une amplitude de 2Pi pour quatre enfants, le pas sera de Pi/2 (un noeud à chaque point cardinal). var Step:search?q=Number%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Number.html&filter=0&num=100&btnI=lucky">Number = Amplitude / Noeud.Enfants.length; //L'angle initial : il suffit de décaler l'angle central par la moitié de l'amplitude. On ajoute aussi la moitié du pas pour équilibrer l'arbre ; sinon l'intervalle n'est pas centré et c'est moche. var Angle:search?q=Number%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Number.html&filter=0&num=100&btnI=lucky">Number = AngleCentral - Amplitude / 2 + Step / 2; for each(var Enfant:Node in Noeud.Enfants) { //placer l'enfant relativement au père Enfant.x = Noeud.x + Rayon * search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.cos(Angle); Enfant.y = Noeud.y + Rayon * search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.sin(Angle); //Puis demander le placement des enfants de l'enfant (c'est téchenique ça) placement(Enfant, Rayon * .8, Step, Angle); //Et incrémenter le pas pour le prochain noeud : Angle += Step; } } Et le premier appel, qui déclenche la cascade d'évènements : //Placer la racine, et placer ses enfants sur un cercle de 200 de rayon. placement(Racine,200); Voilà de quoi tester. Cliquez sur le cercle noir pour faire démarrer l'animation : Quelques exemples de rendus :
Représentation graphique d'un arbre par la méthode dynamiqueLa méthode statique présente l'avantage d'être rapide et efficace dans la plupart des cas. Cependant, on peut difficilement faire des animations (dans le cas de l'ajout d'un nœud en direct, on obtient des sauts peu esthétiques), et les structures obtenues sont trop « mathématiques ». Enfin, quand le nombre de nœuds augmente, on obtient un fratras incompréhensible… et pas forcément optimisé, puisque de grands vides peuvent rester si un nœud de haut niveau n'a pas d'enfants. Pour pallier à ces problèmes, on va étudier une solution plus intéressante. Mais il y a un problème : avec cette loi, les nœuds vont s'éloigner à l'infini (comme les galaxies dans l'univers). C'est vrai ; il faut donc ajouter une contrainte pour éviter ce fâcheux comportement. Et cette contrainte, c'est justement notre arbre : certains nœuds doivent garder une distance faible entre eux pour modéliser le lien. On pourrait donc se contenter d'indiquer au système « la distance entre ces deux nœuds doit être constante », mais il me parait plus intéressant d'accrocher un ressort virtuel entre chaque extrémité ; comme ça tout est représenté sous forme de forces physiques. Il ne reste plus quà y appliquer le célèbre PFD (Σ F = ma [n'oublions pas que ce sont des vecteurs]) pour avoir un positionnement correct, avec animation s'il vous plaît ! Avant d'en dire plus, le résultat (comme tout à l'heure, cliquez sur le rond noir. Soyez patient, même quand les nœuds n'apparaissent plus il y a du mouvement) : Deuxième version, avec ajout d'objets en continu (placements initial aléatoire, arrêt après 150 ronds). Développons un peu… à la création d'un nœud, on ajoute une liste de contraintes :
Puis à chaque mise à jour de l'affichage, on calcule pour chacun des nœuds la liste des forces et on applique le PFD pour en déduire l'accélération. Comme on est dans un quanta de temps très faible, on n'intègre pas l'accélération et on se contente d'ajouter la valeur obtenue à la vitesse, puis d'ajouter la vitesse à la position. C'est brutal, mais cela permet d'accélérer énormément les calculs (intégrer numériquement peut prendre du temps… ) et donne des résultats tout à fait corrects tant qu'on calcule bien la position de tout le monde en même temps. De plus, on est certain d'obtenir un système stable après un temps suffisament long : les nœuds sont alors placés sur des minimums locaux utilisant le moins d'énergie (on dit que l'ensemble minimise les contraintes, une des lubies principales de mère nature). Quelques exemples de rendus :
Quelques preuves que le codage n'est pas si complexe : /* CALCUL DE LA FORCE D'UN RESSORT*/ //Mettre à jour la force. var Distance:search?q=int%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:int.html&filter=0&num=100&btnI=lucky">int = search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.max(1,search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.sqrt(search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.pow(Bout.x - AutreBout.x, 2) + search?q=Math%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Math.html&filter=0&num=100&btnI=lucky">Math.pow(Bout.y - AutreBout.y, 2)));//penser à éviter la division par zéro //Calculer la valeur absolue (scalaire, pas un vecteur) var absForce:search?q=Number%20inurl:http://livedocs.adobe.com/flex/201/langref/%20inurl:Number.html&filter=0&num=100&btnI=lucky">Number = k * (Longueur - Distance); //Puis la projeter sur l'axe défini par les deux points AttractionBout.x = absForce * (Bout.x - AutreBout.x) / Distance; AttractionBout.y = absForce * (Bout.y - AutreBout.y) / Distance; //Calculer la force inverse (une force s'applique sur A, l'autre sur B) AttractionAutreBout.x = - AttractionBout.x; AttractionAutreBout.y = - AttractionBout.y; //Et appliquer la force aux objets pour le prochain calcul du PFD Bout.applyForce(AttractionBout); AutreBout.applyForce(AttractionAutreBout); /* APPLICATION DU PFD */ //Calcul de la somme des forces for each(var Force:Vecteur in Forces) Resultante.ajouter(Force); //application du PFD : Somme des forces = masse * Accélération //Resultante.scalarMul(1/this.Masse);//Inutile, masse à 1 //Puis mise à jour de la vitesse. À ce point là, le vecteur Resultante correspond au vecteur accélération. //Il faut noter qu'on fait les calculs à chaque itération : l'intégration du vecteur accéleration pour obtenir le vecteur vitesse se transforme alors en une simple addition. Vitesse.ajouter(Resultante); //Ajouter des frottements pour éviter d'avoir les ressorts qui oscillent à l'infini Vitesse.scalarMul(FROTTEMENTS); //Calcul de la nouvelle position selon la même logique intégration = addition. this.x += Vitesse.x; this.y += Vitesse.y; //Vidage de la liste des forces pour le prochain calcul Forces.splice(0,Forces.length); Resultante.reset(); Bref, encore un exemple de modélisation simple d'un système complexe… en voulant extraire les paramètres macroscopiques, on devait affronter des équations mathématiques et des cas particuliers de plus en plus retors pour améliorer le système initial ; avec des contraintes microscopiques (atomiques au sens étymologique) on se contente de donner des règles simples à satisfaire qui permettent d'obtenir un comportement complexe. Une sorte de jeu de la vie… qui montre l'importance de la réflexion avant le codage. Télécharger les codes. Le code statique n'est pas optimisé du tout car il incorpore aussi les ressorts et les interactions (sans les afficher bien entendu). Pour passer en mode auto, changez la variable "MODE" avant de compiler. |
| Mise à jour le Jeudi, 29 Avril 2010 19:30 |









La méthode statique m'est venu intuitivement. Cependant la méthode dynamique est plus difficile à comprendre. Pourriez-vous me dire comment vous est venu l'idée d'utiliser ces forces d'attractions et de répulsions et surtout l'application de celle-ci. En effet, vous faites repousser les noeuds les uns des autres, j'ai commencé un algo, mais où les noeuds s'attirent (deux corps s'attirent) et les arrêtes provocant une répulsion entre les noeuds, mais les résultats sont peu concluant. Pensez-vous qu'il peut être possible d'avoir un résultat semblable au votre par cette approche?
Quoi qu'il en soit, vous avez fait du beau travail!
Cordialement,
M.LABAUNE
Votre implémentation (nœuds qui s'attirent) crée un système dynamique instable : arêtes et nœuds vont constamment se battre, tandis que la solution que je décris ici permettra d'atteindre un état "d'équilibre" pour lequel l'application de contraintes supplémentaires au système décalera discrètement les points d'équilibre.
C'est un peu technique, mais ce qu'il faut retenir, c'est que des nœuds qui s'attirent et des arêtes répulsives amèneront forcément un comportement chaotique inutilisable. De plus, cela ne mènera pas forcément à un positionnement géométrique régulier... je vous déconseille donc cette approche !