Orb Crafter est un projet solo qui m'a occupé une bonne partie de l'année 2021. Il s'agit d'un outil logiciel (Windows, Linux)
de cartographie de mondes imaginaires, à destination principale des joueurs de jeu de rôle (TTRPG), et que je souhaite à terme commercialiser. La spécificité du soft
est de mettre l'accent sur la génération automatisée/semi-automatisée et manuelle de cartes à grande échelle : des mappemondes présentées selon
diverses projections cartographiques, suivi de l'extraction en haute résolution de cartes régionales dérivées, et enfin l'export de toutes les cartes, en résolution arbitraire, et sous divers formats.
J'ai créé un site pour exposer les features cibles, ainsi que les éléments principaux de conception et de développement : www.orbcrafter.com.
En résumé, l'outil est développé sur mon propre moteur multimedia (CARGO - cf. plus bas dans cette page) écrit en C++, et s'appuie presque intégralement sur la
carte graphique (via OpenGL 4) pour toute la génération de contenu et l'édition par brush en temps réel.
Sur ces six mois de développement à temps plein, j'ai dû surmonter pas mal de difficultés techniques. Voici une liste (non-exhaustive) :
Trouver une méthode temps réel de génération automatique de mappemondes (implémentation du Wizard à disposition de l'utilisateur) comprenant des caractéristiques tectoniques : cordillères, rifts, arcs d'îles.
La solution trouvée est de partitionner la planète (en 3D) en cellules de Voronoi "warpées", lesquelles représentent des proto-plaques tectoniques, suivi de l'application d'un compute shader procédant fonctionellement par pixel de la carte (parallélisable car évaluable indépendamment) pour
évaluer le relief final : le paramètre principal étant la distance à la frontière locale de la proto-plaque, je peux ainsi construire la côte continentale (en rognant la proto-plaque), l'éventuelle cordillère ou un arc d'îles dans le cas d'une plaque océanique.
Le calcul sur GPU est très rapide, cela permet donc à l'utilisateur de manipuler les sliders de paramètres dans l'interface et de voir la modification en temps-réel.
La gestion propre de l'édition des cartes par l'utilisateur, avec notamment la possibilité d'annulation (Undo / Redo). Le design pattern
Command est la solution classique à ce problème. J'ai dû l'implémenter (pour la première fois), en ajoutant une petite optimisation consistant à distinguer opérations lourdes (wizard) et légères (brush),
ce qui permet de ne pas re-traverser l'historique complet dans le cas d'un Undo mais seulement de rejouer celui-ci depuis la dernière opération lourde (laquelle modifie intégralement l'image).
La sauvegarde des Projets de l'utilisateur, lesquels comprennent potentiellement N cartes (au moins 1 mappemonde et N-1 cartes régionales dérivées). J'ai rapidement été convaincu
que la seule façon de sauvegarder les données efficacement, et ce de manière orthogonale à la résolution de travail des cartes, étaient de les prendre dans leur forme la plus compacte,
c'est-à-dire en considérant directement l'historique des commandes d'édition comme représentation d'une carte. En pratique c'est très rapide à sérialiser (la représentation étant paramétrique) et le GPU
permet de rejouer l'historique rapidement au moment du chargement de la carte. Autre avantage certain : la possibilité d'exporter en résolution arbitraire la carte (png, jpeg, heightmap, etc.) ! Seul ombre au tableau : il faudra versionner
les diverses commandes afin de garantir l'aspect des cartes utilisateur, dans l'anticipation de l'évolution du logiciel.
L'extraction de cartes régionales en haute résolution. Ici la moitié de la solution a déjà été trouvée, avec le point précédent (ie., la représentation d'une carte par son historique). Pour le reste,
comme les mappemondes sont en fait, en interne, des mondes 3D sphériques (enfin de permettre l'édition cohérente aux longitudes qui se raccordent, soit à 180°Est et Ouest, ainsi qu'aux pôles de la planète),
il a fallu projeter sur le plan tangent afin de créer les cartes régionales, par essence planaires. En pratique et avec un peu de géométrie 3D ça se fait bien, mais je me suis heurté au classique problème de précision des flottants 32bits
(sur GPU) lorsque l'on traite des planètes à l'échelle 1:1. Ceci est apparut presque exclusivement lorsque les calculs faisaient intervenir des coordonnées sphériques (longitude, latitude) du fait des cosinus et sinus : j'ai donc rusé un peu pour éviter
cette représentation dans les shaders.
Enfin et toujours concernant les cartes régionales, une fois celles-ci extraites de la mappemonde, il a fallu générer du détail et ce de manière cohérente sur toute la planète (afin que, par exemple,
deux cartes régionales adjacentes ou se recouvrant partagent les mêmes features). Plus compliqué. J'ai fini par adopter la solution suivante : collage et mélange de DEM prégénérées, chaque DEM étant positionnée sur un sommet
d'une triangulation sphérique de Delaunay couvrant la planète (précision ~80 km). Pour éviter les répétitions les DEM (256 x 256 km) sont orientées aléatoirement dans le plan tangent à la sphère et excentrées aléatoirement. Le mélange final se fait par blend barycentrique en tout point de la carte.
Cela suppose un peu de book keeping (CPU et GPU) au niveau des structures de données, car des buffers stockent les références aux sommets de la triangulation ainsi que les intersections
(l'intersection d'un pixel de la carte avec la triangulation étant pré-calculée sur CPU, afin de maximiser les performances du shader générant la topographie finale).
PHANTOM STARS
Aux alentours de 2015 alors que finissais ma Licence à la FAC, j'ai commencé à programmer des prototypes graphiques. La première chose
intéressante à émerger a été une galaxie spirale volumique procédurale. Depuis, et parce qu'après plusieurs itérations l'allure globale
devenait plus que satisfaisante, j'ai pensé à donner un contexte et un horizon à ce prototype : d'où le projet Phantom Stars, destiné à devenir je l'espère un ambitieux jeu indé.
Le développement du moteur custom a débuté en 2019 (Cargo Engine). Beaucoup de fonctionnalités ont été ajoutées telles que la gestion mémoire, les composants et l'architecture GUI,
le logging, le profilage, la possibilité de localisation du jeu, etc.. Visuellement, et par manque de temps à consacrer au projet, Phantom Stars n'est pour l'heure toujours qu'un prototype graphique sans élément de gameplay. Voici
ce que cela donne toutefois en l'état (captures d'écran de juillet 2020) :
Je peux en dire un peu plus sur les aspects techniques et de conceptuels du jeu :
Modélisation galactique. Le jeu se déroule dans une unique galaxie, spirale, d'environ 100 000 années lumière de diamètre. Son aspect, donc le modèle, est personnalisable par le joueur, car paramétré.
Côté dev, la galaxie est représentée par un volume de texels RGBA, le triplet RGB représentant la luminosité stellaire (ponctuelle et diffuse) des champs d'étoiles, et l'opacité est simplement celle du medium interstellaire (ISM),
c'est-à-dire des grands nuages de gaz et de poussière. Un compute shader calcule chacune des tranches du volume. Une distorsion en spirale est appliquée à un domaine en croix (axes) pour former les bras spiraux. Le bulbe galactique
est ellipsoïdal. La lumière locale résulte de distributions de champs d'étoiles, donnant une lumière "diffuse" en moyenne, soit d'étoiles jeunes (Pop I) dans les bras spiraux (plutôt bleue), soit d'étoiles âgées (Pop II) vers le coeur
galactique (plutôt orangée) et au-dessus du disque (halo) - ou donnant une lumière ponctuelle pour le cas des hyper/super géantes, bleues (séquence principale) ou rouges (red giant branch) pour les étoiles sortant de la séquence principale.
Le code s'appuie sur de nombreux bruits cohérents combinés (Simplex noise), en tout plus de 200 octaves sont calculées pour un texel, avec, aussi, des fonctions de hashage pour distribuer les super-géantes.
Le volume est rendu en temps réel par raycasting, intersection avec la boîte englobante puis raymarching avec accumulation de luminosité et opacité.
Modélisation de l'espace local. L'espace local peut être calculé, dans un cube d'environ 1000 années lumière de côté. Je découpe l'espace galactique en voxels de taille 32^3 années lumère. Une série de compute shaders sont lancés :
le premier échantillonne le modèle galactique présenté ci-dessus au centre de chaque voxel (densité de l'ISM, densité des étoiles (pop I & II), présence de géantes selon leur type, etc.) ; le second génére des graines de systèmes stellaires
(essentiellement la masse du système, sa catégorie, pop I ou II, géantes...) pour chaque voxel selon des probabilités liées à la densité d'étoiles et la position galactique ; le troisième génère le détail de chaque système (multiplicité, de 1 à 7 étoiles,
et caractéristique globale de chaque étoile, donc masse et âge de laquelle je déduis d'après des lois astrophysiques connues la température, la luminosité, variabilité, etc.) et les caractéristiques agrégées du système (dans le cas d'étoiles multiples). Au passage,
je repère pour chaque voxel les systèmes les plus lumineux pour pouvoir illuminer le gas - ce dernier étant distribué d'après le sampling galactique. Au final, dans les bras spiraux un tel cube d'espace génère en moyenne 200 000 systèmes, et vers le
noyau environ 1 000 000 de systèmes sont générés - à noter que j'ai artificiellement baissé la densité réelle d'étoiles pour des raisons évidentes de performance (notamment vers le noyau). Pour le rendu du modèle : chaque système et chaque élément de l'ISM sont représentés par des billboards
semi-transparents triés par distance à la caméra, la couleur d'un système est donnée par sa température moyenne (j'utilise une table RGB réaliste de couleurs d'étoiles selon leur classification), quant à la taille du point-sprite, elle dépend de la distance
à la caméra et de la magnitude absolue du système (encore une fois, selon une loi physique).
Modélisation des sytèmes & planètes.
Contrairement aux deux étapes précédentes, ici la modélisation se fait intégralement sur CPU. A partir des données globales d'un système stellaire (donc principalement ses étoiles et sa position galactique) un algorithme calcule tout d'abord l'architecture
orbitale du coeur du système, ie. comment les étoiles (entre 1 et 7) gravitent entre elles. Là encore c'est basé sur des connaissances admises en astrophysique, notamment les orbites hiérarchiques par paires d'étoiles. Toutes les orbites sont Képlériennes (elliptiques).
Etant donné un instant t, je peux calculer d'après les orbites assignées la position de chaque étoile. Par là, on décide ensuite de la présence ou non de planètes. Un scénario simplifié d'évolution du système génère et positionne des embryons planétaires (rocheux ou glacés),
les fait grandir (notamment les géantes gazeuses, type Jupiter) et les fait migrer aléatoirement dans le protodisque, ce qui occasionne des éjections, des collisions, des changements d'orbites, etc. Au final on retient l'état présent du système après la simulation d'évolution :
un ensemble de N planètes, définies par leur orbite actuelle, leur masse et leur type (rocheux, glacé, gazeux). Pour finir, chaque planète survivante fait l'objet d'un calcul dédié chargé d'attribuer les caractéristiques détaillées du corps :
masse, densité, taille, orbite (excentricité, inclinaison, tidal locking), rayonnnement stellaire reçu (donc température sans atmosphère étant donné l'albedo), différentiation du noyau et magnétosphère, atmosphère éventuelle, géologie, cycle hydrologique, etc..
Le rendu est fait, passé une étape de précalcul sur GPU d'une cubemap représentant le terrain (en accord avec les données géologiques : présence de cratères, couleurs principales, relief), par raymarching du terrain et de l'atmosphère éventuelle (single scattering).
Au-delà de ces éléments constitutifs j'ai tenté l'ajout de features autres : les nuages (à peu près corrects en vue orbitale) et l'exploration de la surface en temps réel jusqu'au plus près du sol (ce dernier point étant largement expérimental, de plus problablement pas
requis pour le gameplay final du jeu).
Gameplay envisagé.
Dans Phantom Stars on pourra incarner le dirigeant d'une Corporation, évoluant dans un environnement dystopien. Le joueur pourra :
Explorer l'espace, par sondage distant depuis des bases avancées, puis envoie de sondes et vaisseaux afin d'établir de nouvelles routes et de récolter des infos/visualisation in situ des systèmes/planètes d'intérêt.
Exploiter l'espace : notamment les planètes présentant le bon profil de compatibilité (gravité, température, atmosphère et magnétosphère protectrice, etc.) et des ressources intéressantes (métaux, terres rares, etc.).
L'établissement de complexes, bases, etc., ainsi que les routes d'approvisionnement et de fret nécessiteront une bonne gestion logistique à la manière classique des 4X.
Dispatcher des agents dans l'espace connu, pour des missions diverses : enquête, infiltration de corporations/organisations adverses, hacking, sabotage, enlèvement, assassinat, etc.
L'investigation (enquête) et le commerce d'informations seront capitaux : pour rendre cela possible, j'ai commencé à concevoir et implémenter une structuration des données innovante, globale et transversale aux modules, qui permettra de représenter l'univers de manière dynamique et sémantique.
Je n'en dis pas plus, mon grand espoir est en fait : de prouver que cela marche, que cela permettra de proposer plus de richesse et d'intelligence au joueur, et enfin pourquoi pas finir par proposer un beau talk à GDC par exemple ! ;)
Gérer bugdets et départements, établir des Opérations avec timelines, légales ou illégales. Les opérations secrètes seront possibles loin des administrations tatillonnes, mais risquées, et concerneront notamment toutes les relations avec la pègre et les cartels...
Gérer tactiquement des affrontements spatiaux (batailles de flottes).
Découvrir le lore global, les enjeux sociétaux et prendre des décisions importantes, notamment morales...
En conclusion, les aficionados auront reconnu dans cette liste la plupart des features du vieux jeu de LucasArts "Star Wars Rebellion". A cela rien d'étonnant j'en suis un grand fan - en plus d'être un amoureux de l'univers de George Lucas - et je souhaite
depuis longtemps proposer une version très étendue, riche, et visuellement réaliste (rendu de l'Espace notamment) de ce vieux titre... Affaire à suivre !
LE MOTEUR CARGO
Cargo est donc le moteur développé par mes soins et dont je me sers, depuis 2019, pour prototyper et même développer des produits complets (cf. plus haut, Orb Crafter, Phantom Stars, etc.).
Il repose pour le fenêtrage et les entrées interface sur GLFW3 (anciennement SDL2, mais GLFW est pas mal non plus et gère un peu plus simplement l'entrée de texte - Text Input).
Côté 3D et compute, c'est OpenGL4.6 qui mène la danse (via glad). Pour le rendu de texte, j'utilise pour l'instant Freetype2 (mais j'envisage de porter tout le rendu en signed distance field via stbtt ; ceci ayant été testé avec succès
pour le projet Orb Crafter décrit plus haut).
La gestion des formats d'images est laissée à FreeImage. La libraire de maths est GLM (étant fortement habitué au code GLSL, c'est très sympa à mettre en oeuvre).
Voici une liste à peu près complète des modules principaux intégrés au moteur :
Chaînes de caractères et Localisation :
Toutes les chaînes de caractère qui devront être présentées à l'utilisateur sont des chaînes encodées en UTF8 et sont construites par un type custom cargo::String. J'ai développé un outil de Localisation
permettant de cataloguer toutes ces chaînes (l'outil est lui-même développé sur le moteur Cargo), avec une organisation en Catégories, chaque Catégorie regroupant N éléments de texte, chaque élément
étant versionné dans plusieurs langues. Il est possible d'ajouter des langues ultérieurement. Chaque langue définit également des chaînes globales (par exemple: la séparation décimale des nombres, donc '.' en anglais,
et ',' en français). Un bouton permet l'export : d'un fichier meta de localisation (langues disponibles et chaînes globales), des blobs regroupant toutes les chaînes, et d'un fichier header C++ (.h) généré automatiquement, lequel définit macros et constantes pour
au final référencer efficacement dans le code côté client chacune des chaînes localisées (celles-ci étant stockées dans un seul bloc contigu de mémoire, lequel est peuplé automatiquement par le moteur).
Rendu :
Un wrapper c++ encapsule OpenGL de manière générique. Cet effort est à mi-chemin entre une amélioration de productivité pour l'écriture de code 3D, et une abstraction de l'API graphique.
Je soupçonne qu'il sera difficile de proposer la même abstraction par-dessus Vulkan, le paradigme de programmation étant quand même éloigné de la simple machine à état qu'est OpenGL.
Néanmoins, ce wrapper est très pratique, il me permet de déclarer mes ressources GPU et d'y accéder en une ligne de code ou presque, et de mettre en oeuvre les divers shaders rapidement ce qui est très bien
pour itérer.
Mémoire :
J'ai trouvé des bouts de code sympathiques, mais pleins de bugs, sur le net pour la gestion mémoire, que j'ai donc dépoussiérés et assemblés pour en faire un ensemble utilisable permettant de s'affranchir des malloc, new et delete.
Il existe une interface Allocator, et des classes concrètes StackAllocator, PoolAllocator et FreeListAllocator, codées en bas niveau (arithmétique des pointeurs et transtypages appropriés) et permettant une gestion fine et efficace de la mémoire allouée et à libérer.
Des macros filtrent les allocations/désallocations et permettent ou non d'obtenir une trace de l'exécution du client (selon la cible de compilation). L'utilisation de templates variadiques permet de construire des objets arbitraires.
Logs :
La solution de logging est complètement custom. Des macros pratiques et variadiques permettent d'enregister des messages, comprenant optionnellement des valeurs de variables diverses (string, float, int, etc.), auprès du gestionnaire de log.
Ce dernier dispatche par défaut, de manière asynchrone, les messages auprès d'un logger qui les enregistre dans un fichier texte. Chaque ligne est formatée par son niveau de log, l'heure de dispatch du message, le fichier et la ligne source, etc.
Ce logger par défaut est en fait un processus séparé, lancé par le moteur, et ce dernier communique avec lui via un Named Pipe (Win32). Ceci permet de garder trace des messages même si le programme principal plante, le logger étant toujours actif
jusqu'à un certain timeout.
Exemple de formatage de quelques messages :
2021-08-27 22:18:55 | INFO | Engine.cpp at line 91 | [sysinfo] Total Memory MB = 16312
2021-08-27 22:18:55 | INFO | Memory.cpp at line 54 | Reserved memory by the engine = 32 MegaBytes
2021-08-27 22:18:55 | INFO | backend_glfw.cpp at line 289 | Detected monitor #0: Generic PnP Monitor (primary)
2021-08-27 22:18:56 | INFO | backend_glfw.cpp at line 290 | Monitor #0, current mode: 1920 x 1080 x 24 (16:9) (8 8 8) 60 Hz
GUI et Inputs :
L'architecture complète gérant l'interface graphique utilisable par le client a été implémentée from scratch. Le rendu des composants (widgets) se fait sur GPU, un seul shader étant mis en oeuvre
aussi bien pour les formes (rect, roundrect et ellipse) que le texte& icônes, et les images. Je propose les composants suivants : Widget (classe mère mais instanciable, représente un élément graphique générique mais supportant déjà l'interaction
souris), Button, Slider, RangeSlider, RadioButtonGroup, Canvas (points, lignes et courbes NURBS), Label, TextField, List, ComboBox, ScrollPane, TabbedPane ; ainsi que les classes auxiliaires Tooltip, Layout, Stage, Layer (organisation en couches fullscreen des composants), et Event.
Il va sans dire que le module GUI a été le plus demandeur en terme d'efforts de développement : rendu de texte, diversité des composants, dispatch des events, architecture de la scène (Stage) avec enregistrement des composants, etc..
Ressources sur Disque :
Pour la gestion des assets, j'ai développé un autre outil tiers : CargoPack. C'est un exécutable qui lit un fichier XML de ressources (images, polices, shaders, etc.), nommées et attribuées, charge chacune des ressources, effectue ou pas un traitement d'obfuscation (pour les shaders GLSL notamment),
et compacte tout ça en N fichiers binaires de tailles à peu près égales, et produit un fichier meta permettant au moteur de s'y retrouver afin de charger les assets (de manière asynchrone ou synchrone) demandées par le client lors de l'exécution de celui-ci. D'autres fonctionnalités existent, comme
la gestion de Configuration du client, sous forme initiale, côté développement, d'un fichier texte au format custom de type Key/Value, qui permet de définir des variables globales disponibles à l'exécution - le tout étant compacté par CargoPack dans les blobs de manière opaque à l'utilisateur final.
En résumé : beaucoup a été fait et c'est très, très, gratifiant à faire. Manquent encore toutefois pas mal de choses ; je pense notamment à la capacité de scripter côté client (LUA ou autre), de profiler de manière plus incisive le code client (pour l'heure une solution basée sur celle du streamer The Cherno a été posée, mais demeure
simple), de finir le moteur Audio (pour l'instant une simple surcouche objet sur RtAudio, adjointe de modules piqués ici et là de DSP (reverb, EQ, delay, compression, ...)), et d'autres choses que je n'ai pas encore envisagées mais qui se révèleront probablement nécessaires.
Jeux de Rôle
GUERRE ASTRALE (JdR)
Projet sur le long terme, "Guerre Astrale" est un jeu de rôle sur table (à l'ancienne: dés, papier, bouquins) avec un univers et des règles originales.
- Le thème est médiéval-fantastique, classique mais teinté d'éléments empruntés à Lovecraft ainsi qu'à certaines philosophies orientales comme le taoisme.
J'ai monté l'année dernière (2019) un site dédié, afin de rassembler mes nombreuses notes dans une forme présentable, pré-éditoriale :
wwww.guerre-astrale.fr
(cartes peintes avec Photoshop)
Musique
TECHNO
Je produis de la musique éléctronique (quand j'ai le temps!) et notamment de la techno sans fioritures, sous le pseudo de Yankee Charlie.
Voici quelques titres récents: