- ShapeUp est né du défi de la Wheel Reinvention Jam, « revoir les logiciels existants sous un angle nouveau », et a été finalisé comme un modeleur 3D avec une démo exécutable dans le navigateur et l’export
.obj - La clé qui a permis de créer un outil 3D en une semaine a été les SDF par ray marching : par rapport à un moteur de rendu basé sur des triangles, il était possible de mettre en place rapidement une scène avec couleurs, ombres douces et ambient occlusion
- L’implémentation est restée simple, principalement autour d’un fichier C unique, et le modèle stocke jusqu’à 100 Shapes dans un tableau statique afin de réduire la charge liée à la gestion mémoire
- raylib a aidé à afficher rapidement une fenêtre OpenGL, mais son API centrée sur
int, l’absence de validation des paramètres, la dépendance à GLFW et les limites de raygui ont obligé à utiliser directement OpenGL ou à recréer certaines fonctionnalités - Le résultat final compte 2024 lignes de C et 250 lignes de GLSL, soit environ 2300 lignes au total, et prend en charge l’ouverture et l’enregistrement de fichiers, l’exécution sur plusieurs plateformes et l’export
.obj
Comment ShapeUp est devenu un modeleur 3D
- La Wheel Reinvention Jam était un événement de programmation d’une semaine consistant à réexaminer des systèmes logiciels existants sous un angle nouveau
- L’objectif initial partait d’une frustration face à la lenteur du compilateur TypeScript, avec l’idée de créer un sous-ensemble de TypeScript plus rapide que
tsc- En prenant comme point de départ le parseur TypeScript d’
esbuildou deBun, cela semblait faisable - Mais la démo de réussite se résumait à « une commande de terminal se termine plus vite qu’une autre », ce qui n’était pas très séduisant visuellement ; le projet a donc finalement bifurqué vers la 3D
- En prenant comme point de départ le parseur TypeScript d’
- ShapeUp a été conçu comme un modeleur 3D permettant d’éditer des formes à la souris
- Il y avait déjà une expérience préalable dans l’écriture de shaders SDF, mais modéliser en modifiant directement du code ne paraissait pas naturel
- L’objectif était de rendre possible l’édition de formes basée sur les SDF à la souris
Pourquoi les SDF ont rendu le projet réalisable en une semaine
- Le rendu de ShapeUp repose sur des signed distance fields (SDF) par ray marching
- Une scène SDF, même avec couleurs, ombres douces et ambient occlusion, pouvait être implémentée plus vite qu’un moteur de rendu basé sur des triangles
- Les exemples d’Inigo Quilez créant en direct des personnages façon Pixar avec des SDF ont servi de référence technique
- ShapeUp aborde la modélisation SDF en manipulant directement les formes plutôt qu’en éditant du code
Implémentation en C et structures de données
- ShapeUp est écrit en C et utilise raylib pour créer la fenêtre OpenGL
- Le choix de C s’explique par la compilation rapide, une syntaxe qui ne masque pas de comportements complexes, la familiarité, et la possibilité de compiler à la fois en natif et en WebAssembly
- Le modèle est composé d’un ensemble de structures
Shape- Chaque Shape possède une position, une taille, un angle, un rayon d’arrondi des coins, un degré de blob, une couleur, un axe de miroir et une option de soustraction
- La liste des Shapes est gérée dans un tableau statique plutôt que par allocation dynamique
MAX_SHAPE_COUNTvaut 100- L’état est géré avec
Shape shapes[MAX_SHAPE_COUNT],shape_countetselected_shape - Cette approche élimine les risques d’échec d’allocation et de fuite mémoire
- La limite de 100 Shapes n’a pas posé de gros problème en pratique
- Faute de temps pour optimiser le moteur de rendu, le framerate chutait avant d’atteindre 100 Shapes
- Avec plus de temps, le modèle aurait été découpé en petites briques, avec du ray marching effectué à l’intérieur de chaque brique
Mode d’utilisation de la mémoire
- ShapeUp n’utilise l’allocation mémoire dynamique qu’à 3 endroits
- Enregistrement : allocation d’un buffer pouvant contenir tout le document
- Export
.OBJ: allocation d’un buffer pouvant contenir tous les sommets - Génération de shader GLSL : allocation d’un buffer pour la source du shader
- Dans chaque cas, un unique
freeest effectué à la fin de la fonction - Il aurait été possible de faire un
mallocpour chaque Shape et de stocker des pointeurs dans un tableau dynamique, mais ce projet n’avait pas besoin d’une telle structure - C présentait un avantage par le contrôle direct qu’il offre sur l’agencement mémoire
- Si des tableaux dynamiques ou des hashmaps avaient été nécessaires, un outil comme
stb_ds.haurait pu être utilisé
Méthode d’implémentation de l’UI
- L’UI est implémentée selon le modèle immediate mode user interface (IMGUI)
- IMGUI a l’avantage d’être facile à déboguer et de permettre de positionner les éléments avec un vrai langage de programmation plutôt qu’avec CSS, des contraintes ou SwiftUI
- L’élément focalisé ou les actions de souris sont suivis via une enum
Control- Les états de manipulation, comme la position, la taille, l’angle, la couleur, le déplacement, la rotation, l’échelle, la rotation de la caméra ou le degré de blob, sont représentés par des valeurs d’enum
focused_controletmouse_actioncontiennent l’état courant de l’UI
Les blocages rencontrés avec raylib et raygui
- raylib a été utile pour afficher rapidement une fenêtre OpenGL, mais avec le temps, elle est devenue un facteur de ralentissement du développement
- Le point particulièrement gênant dans l’API raylib était le manque d’informations de type
- Des
intsont utilisés même là où un type enum est attendu, ce qui empêche le compilateur de faire des vérifications de type - Les signatures de fonctions ne rendent pas bien compte du sens des paramètres
- Par exemple, dans
IsGestureDetected(unsigned int gesture),gestureressemble à un ID de geste enregistré, mais il s’agit en réalité de l’enumGesture - La documentation étant centrée sur les fichiers d’en-tête, il fallait consulter l’implémentation pour savoir quel
intcorrespondait réellement à une enum
- Des
- L’absence de validation basique des paramètres a aussi aggravé les problèmes
LoadFileData(const char *fileName, int * dataSize)provoque une segfault sidataSizevautNULL- L’en-tête n’indique pas si
dataSizeest un paramètre de sortie ni s’il ne doit pas être nul - L’absence de validation a rendu difficile le diagnostic de problèmes simples et pouvait, selon les cas, entraîner des comportements anormaux silencieux
- La gestion des dépendances ne correspondait pas non plus aux attentes
- Certains problèmes de GLFW n’étaient ni contournés par raylib ni corrigés via des patchs soumis en amont
- L’utilisateur final accorde plus d’importance au bon fonctionnement des fonctionnalités de raylib qu’aux détails internes de la création de fenêtre
- La bibliothèque d’UI raygui imposait trop de contraintes pour ce projet
- Elle ne pouvait pas afficher de nombres à virgule flottante, ce qui a obligé à créer directement un champ texte pour les floats
- Elle ne gérait pas le routage des événements souris pour des éléments superposés ou coupés
- Elle ne prenait pas en charge les coins arrondis, pourtant courants dans les UI
- Il était difficile de la styliser proprement
- Des bugs ont aussi perturbé le flux de développement
- Un bug dans les outils raygui empêchait de remplacer la police par défaut, trop stylisée
- Les fonctions de dessin comme
DrawCircle(...)ne partagent pas les sommets entre triangles ; si la matrice courante contient une mise à l’échelle ou une rotation, des erreurs en virgule flottante créent donc des interstices de pixels
- Les problèmes découverts ont été signalés pendant un temps, mais la plupart ont été fermés comme “wont fix”, ce qui a fini par interrompre les signalements
- Les contournements consistaient à utiliser directement les fonctions OpenGL ou à implémenter les fonctionnalités nécessaires depuis zéro
- À l’avenir, le plan est d’utiliser sokol à la place de raylib
Les quatre choses à terminer en 6 jours
- ShapeUp devait finaliser quatre grandes parties en 6 jours
- Interface utilisateur : gizmos 3D, raccourcis clavier, barre latérale, contrôleur de jeu
- Générateur de shaders GLSL et moteur de rendu par ray marching
- Sélection à la souris basée sur le GPU
- Marching cubes pour l’export
- La difficulté tenait moins à chaque fonctionnalité prise isolément qu’au maintien des priorités
- Les problèmes délicats ou chronophages étaient évités en modifiant la conception, ou traités avec des solutions simples fonctionnant dans 90 % des cas
- Pour certaines fonctionnalités, une solution apparaissait après les avoir laissées de côté pendant une journée
- La méthode de travail consistait à maintenir en permanence un modeleur 3D fonctionnel, puis à l’améliorer progressivement autant que le temps le permettait
- L’approche est comparée non pas à une pyramide qui ne devient une pyramide qu’au moment où elle est terminée, mais à une construction qui reste une petite pyramide complète quel que soit le stade où l’on s’arrête
Résultat final
- À la fin de la semaine, ShapeUp permettait de créer des modèles 3D significatifs et de les exporter en fichiers
.obj - Il fonctionne sur plusieurs plateformes et prend aussi en charge l’ouverture et l’enregistrement de fichiers
- Le code représente 2024 lignes de C et 250 lignes de GLSL
- Le fait d’avoir pu créer un modeleur 3D relativement utile en environ 2300 lignes est un résultat notable
- Le projet lui-même est relativement simple, mais le sens du choix de ce qu’il fallait construire, les connaissances nécessaires pour le faire et la discipline pour terminer en une semaine ont joué un rôle important
1 commentaires
Commentaires Hacker News
Je suis entièrement d’accord avec l’auteur sur les limites de Raylib. Je développe actuellement un jeu de type tower defense que j’ai commencé avec Raylib, et je rencontre beaucoup des mêmes limites, ainsi que d’autres problèmes.
Par exemple, le passage en plein écran ne fonctionne pas de façon cohérente selon les plateformes, il est impossible d’énumérer les modes d’affichage, il est difficile d’activer ou désactiver des fonctionnalités de rendu pendant l’exécution, il y a des problèmes pour sauvegarder les shaders compilés, etc.
Cela dit, je suis reconnaissant du travail que Ray a consacré à cette bibliothèque et je compte continuer à le soutenir. Raylib est excellente pour créer rapidement des prototypes, mais aller au-delà n’est pas simple, sauf à accepter de fortes contraintes.
J’ai clairement appris des choses, mais à ce stade le développement est trop avancé pour remplacer tout le code lié à Raylib par quelque chose comme SDL.
Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
La stratégie actuelle consiste simplement à utiliser une fenêtre sans bordure et à faire comme si le vrai plein écran n’existait pas.
Mon plus gros problème en ce moment concerne la gestion des polices et le rendu du texte. Je pense que je vais devoir remplacer les polices TTF par des polices bitmap précalculées, mais cela risque d’être assez pénible plus tard pour la localisation.
Les deux fonctionnalités qui me manquent le plus depuis mon passage depuis Love2D sont le rendu facile de texte multicolore et la possibilité de découper puis répéter ou tuiler facilement des textures. Avec Raylib, il faut découper soi-même le texte selon le balisage de couleur, appliquer des décalages de largeur, puis appeler la fonction de dessin pour chaque fragment en tenant aussi compte des retours à la ligne.
Quand on affiche beaucoup de texte à l’écran, les FPS semblent aussi chuter fortement ; il est possible que le batching des appels de dessin pour le texte soit cassé. Il existait auparavant une fonction de dessin de textures en tuiles, mais elle a été supprimée pour une raison inconnue.
Ce qui m’a toujours agacé avec Wasm et les graphismes 3D/2D dans le navigateur, ce sont les petits problèmes fréquents comme le défilement. Voir l’exemple « Background scrolling & parallax » ici : https://www.raylib.com/examples.html
Je l’ai testé sur plusieurs appareils, et si mes yeux ne me trompent pas, ce n’est clairement pas un défilement fluide. J’ai du mal à croire qu’en 2024, le défilement fluide en 2D ne soit toujours pas un problème résolu.
« Les Shapes sont stockées dans un tableau alloué statiquement. Pas d’échec d’allocation, pas de fuite, pas de superflu. C’est charmant. La limite de 100 Shapes n’a en pratique pas été une contrainte. Comme je n’avais presque pas de temps pour optimiser le renderer, le framerate aurait probablement chuté avant même d’atteindre 100. »
C’est le meilleur exemple d’évitement de l’optimisation prématurée que j’aie vu récemment.
Article vraiment intéressant, et j’ai apprécié qu’il parle de plusieurs décisions, comme sa façon de gérer la mémoire et les problèmes rencontrés avec raylib. Je suis justement en train de revoir le C en abordant la deuxième partie de Crafting Interpreters, donc c’est agréable de me rappeler ce que C fait bien.
La démo en temps réel dans la vidéo est vraiment excellente. Sans même parler de créer l’appli, si j’avais essayé, je ne pense pas que j’aurais réussi à faire cette vidéo en une semaine.
Il y a longtemps, j’ai travaillé sur un système d’exploitation pour téléphones de bureau. Il n’y avait que 64 Ko de RAM, donc aucune gestion dynamique de la mémoire, et nous utilisions beaucoup de variables statiques afin que le compilateur place tout au moment de la compilation.
On oublie facilement que beaucoup d’applications n’ont peut-être pas du tout besoin de gestion dynamique de la mémoire. Il suffit souvent d’allouer quelques buffers de taille fixe et de gérer proprement les cas exceptionnels où ces buffers sont pleins.
Dans ce contexte, C est en réalité beaucoup plus sûr. Il n’y a pas de fuite mémoire, et la seule chose à surveiller est le dépassement de buffer. Si toutes les variables sont allouées statiquement, on peut gérer cela en utilisant
sizeofavec prudence.Cela ne veut pas dire que Rust et Go ne sont pas d’excellents choix aujourd’hui, mais le bon vieux C modeste fonctionne encore très bien et n’a pas besoin d’être d’une complexité cauchemardesque.
C’est un peu hors sujet, mais je suis content de voir pour la première fois une interface WebAssembly dont le texte ne paraît pas flou. Vraiment la première fois.
Si l’on élargit aux programmes et à certains systèmes d’exploitation, comme Windows, un problème général est apparu ces dernières années, à mesure qu’une certaine façon de rasteriser le texte est devenue une tendance commune et un réglage par défaut.
Malheureusement, les utilisateurs ne peuvent souvent pas désactiver l’anticrénelage pour obtenir un texte net, et même dans les rares cas où l’option existe, l’anticrénelage reste appliqué aux interfaces comme les menus.
J’adore ce genre de projet. J’aime encore le côté bas niveau de C. Aujourd’hui j’utilise beaucoup Rust et Elixir/Erlang, mais la simplicité et le caractère explicite de C me manquent souvent.
C’est aussi pour cela que j’utilise beaucoup Zig : c’est un langage qui conserve une grande partie de la philosophie de C tout en l’améliorant de façon très réussie.
Je suis vraiment d’accord avec son jugement sur C. En particulier le passage selon lequel « la syntaxe ne cache pas de comportements complexes. Elle est assez simple pour qu’on n’ait pas besoin de la consulter sans cesse » ; et, au-delà de ça, quand il faut chercher quelque chose sur C, c’est aussi très facile et instructif
Un langage simple et ancien a ses avantages
En allouant chaque Shape séparément avec
mallocet en stockant ces pointeurs dans un tableau dynamique, on peut clairement se compliquer la vie tout seul. Il est dit que des langages comme C# imposent ce type de structure d’allocation, mais je me demande ce qui empêcherait d’utiliser en C# un tableau fixe de structures comme l’auteur l’a fait en CJ’aimerais que quelqu’un poursuive ce projet. Avec quelques mois de peaufinage en plus, il pourrait devenir une alternative sérieuse à Blender ou FreeCAD pour certains usages, avec une courbe d’apprentissage qui semble beaucoup plus douce
Mais comme il fonctionne fondamentalement avec des SDF, l’expérience de modélisation comme les données stockées diffèrent des maillages traditionnels à base de triangles, sommets, etc.
Convertir une SDF en maillage est possible avec des méthodes comme marching cubes, mais il y a de fortes chances que ces données doivent de toute façon être nettoyées ensuite dans des applis du type Blender
Si le moteur de rendu est lui aussi basé sur des SDF, les SDF sont excellentes. Mais la plupart ne le sont pas
Désolé si tu le savais déjà