Après une longue session de Starbound, j’ai eu envie de coder un générateur de cieux étoilés. Un an et demi plus tard, je décide de partager le code source et d’expliquer les étapes de conception. Enfin, j’essaye, car ma mémoire me fait souvent défaut. Le code est disponible sur mon Github.
Je l’ai codé en Python. Il s’agit de mon langage préféré pour sa simplicité. J’avais déjà testé quelques libraries graphique me permettant de partir confiant sur ce projet.
Je suis parti d’une classe, sans aucun diagramme de conception, où je viendrais appeler différentes méthodes symbolisant la progression de la construction du ciel. Pour être honnête, je ne voulais pas que ce projet me prenne trop de temps, et sachant que je suis encore un noob en OOP, on peut considérer le code que je vais présenter comme un blasphème que je n’assume qu’à moitié.
Ce n’était pas prévu, mais je me suis basé sur la génération procédurale avec une seed. Il est ainsi possible de retrouver la même image si l’on indique en entrée du programme le nombre choisi.
Nous partons d’un wallpaper 1920×1080 entièrement noir.
Je ne pense pas que je vais entrer dans les détails pour les explications suivantes, car après autant de temps passé, certaines idées ne sont pas restées. Je vais tenter de rester clair et d’indiquer les grandes lignes.
Créer des étoiles
La première étape était de créer un amas de pixels formant une étoile. Je me suis aidé de la forme d’une astroïde et de la gaussienne.
L’idée est de travailler sur une matrice carrée de pixels de taille \(n\). On y viendra dessiner notre astroïde à l’intérieur de cette grille, avec une intensité variable : Elle est forte au centre de l’étoile et s’affaiblit lorsque l’on s’en éloigne.
Je viens utiliser la Gaussienne sous la forme d’une loi normale avec comme paramètres \(\mu = 0\) et \(\sigma = \frac{n}{6}\). On peut noter que je n’utilise pas exactement la même fonction. J’omets volontairement le coefficient de l’exponentielle, car je ne m’intéresse pas tout de suite à son intensité. Je définis cette fonction deux fois pour qu’elle puisse être utilisée dans \(\mathbb{R}^2\).
Je viens dessiner l’astroïde à l’aide des courbes de Lamé de paramètre \(n=2/3\). Je multiplie la valeur obtenue par celle de la gaussienne de la case associée.
Pour revenir sur \(\mathbb{R}\), je viens faire la moyenne géométrique entre les deux intensités trouvées pour la même cellule de la matrice. On obtient un disque flouté qui se multiplie avec une astroïde pure. L’étoile est bien dessinée.
Oui bon, je n’ai pas précisé, mais j’ai fait un petit bidouillage pour changer légèrement la teinte de chaque étoile, sinon on avait que des blanches.
Fond nébuleux
C’était mon dernier ajout. Il s’agit d’un bête bruit de Perlin. On génère le bruit souhaité, on choisit une couleur, et on l’applique sur le fond noir. Pas grand-chose d’intéressant ici, à part peut⁻être que mon programme génère une matrice de la taille de l’image afin de stocker les intensités du bruit généré. Je garde en mémoire la matrice, car elle servira juste après.
Répartition des étoiles dans le ciel
En s’aidant de la matrice du bruit de Perlin, elle servira de loi de probabilité d’apparition des étoiles. Afin de casser, artistiquement parlant, le lien entre les tâches nébuleuses et les étoiles de l’image, on vient retourner le bruit de 180°. On aurait pu recréer une nouvelle matrice, mais cela aurait encore augmenté le temps d’attente lors de la génération.
Il y a également plusieurs tailles d’étoiles en fonction d’une loi de probabilité sur plusieurs étages. Les valeurs choisies sont arbitraires. Ainsi, on vient multiplier nos deux lois de probabilités entre elles, pour obtenir cette drôle de loi de probabilité finale.
Création de constellations
Dans Animal Crossing : Wild World, on pouvait dans l’observatoire de Céleste créer des constellations avec les étoiles dans le ciel. J’ai décidé de tenter d’en générer. Je me suis posé quelques règles simples.
- Les constellations doivent ne pas être trop petite ou trop grande.
- Une ligne ne peut être coupée par une autre.
- Les formes doivent être variées. On peut avoir des cycles ou des branches d’étoiles.
La description est vague. J’avoue y être allé par tâtonnements.
J’ai préalablement enregistré les positions des plus grosses étoiles générées précédemment. Avec ces données, je mets en place un algorithme qui cherchera à faire des groupes d’étoiles relativement homogène. Mon choix s’est porté sur DBSCAN. Une fois mes groupes formés, je supprime les plus petits. J’ai pris la valeur arbitraire qu’un groupe devait avoir au moins 4 étoiles pour former une belle constellation. J’ai également paramétré de façon empirique l’algorithme pour ne pas avoir de trop gros groupes.
Avec mes différents groupes, il faut maintenant dessiner les liens entre les étoiles. Plutôt que de tous les dessiner, j’ai opté sur une méthode de triangulation, notamment celle de Delaunay. Cela permet de respecter la seconde problématique, celle où tous les liens ne se traversent pas.
Enfin, on vient supprimer aléatoirement certains liens à l’aide d’un parcours entre les étoiles. On peut ainsi trouver des formes variées, tout en s’assurant que le graphe reste connexe et élégant. J’avais essayé avec d’autres algorithmes, mais esthétiquement parlant, ce DFS un peu beaucoup custom rendait le meilleur résultat.
“Dessine-moi un mouton”
Dans mon cas, c’était un peu plus grand. Je voulais remobiliser la plupart de mes connaissances en mathématiques pour résoudre ce problème artistique. L’écriture de cet article m’a permis de relire un peu de code, et surtout de tenter de comprendre ce que je voulais faire par le passé. Malgré les quelques lignes de commentaires, ça n’a pas été suffisant, et il y a encore quelques zones d’ombres que je ne saurais aujourd’hui justifier.
Je n’ai aucune envie aujourd’hui de continuer le projet. J’en garde cependant un excellent souvenir. Je ne pense pas avoir eu de grande difficulté à le réaliser, mais si je devais recommencer, je règlerais les problèmes d’optimisation en priorité. Python, c’est sympa, mais la génération est vraiment lente. Je devrais passer par du multi-processing pour gagner en temps. Je vois aussi que je ne faisais pas attention à la mémoire utilisée, ou que la phase de dessin était éparpillée un peu partout dans le programme. Aussi, l’utilisation plus avancée des libraries graphiques et mathématiques serait plus judicieux, en trouvant autre chose que PIL et exploitant davantage numpy.
Le ciel qui se trouve au-dessus de ma tête ne change pas beaucoup de forme en fonction du temps, mais trop souvent je le vois noir et sans étoile. N’étant pas souvent à la campagne, je trouvais réconfortant d’avoir un semblant de naturel dans cette œuvre artificielle.
Au final, je devais vraiment être triste ces jours-là.