Implémenter un shader 3D en temps réel sur Game Boy Color
(blog.otterstack.com)- Projet ayant implémenté un shading 3D en temps réel sur Game Boy Color, où le joueur peut manipuler la trajectoire de la lumière et faire pivoter l’objet
- Basé sur le calcul de vecteurs normalisés et du shading de Lambert (dot product), avec une simplification des opérations grâce aux coordonnées sphériques
- Pour contourner les limites du CPU SM83 dépourvu d’instruction de multiplication, utilisation d’une transformation logarithmique et de tables de correspondance afin d’effectuer les calculs en précision 8 bits
- L’usage de code auto-modifiant (self-modifying code) permet un gain de performances d’environ 10 %, avec le rendu de 15 tuiles par image
- La génération de code avec l’IA a échoué dans la plupart des cas, et l’algorithme central comme le shader ont été finalisés à la main
Vue d’ensemble du projet
- Création d’un jeu qui rend des images en temps réel sur Game Boy Color
- Le joueur contrôle une lumière en orbite et fait pivoter l’objet
- L’intégralité du code est publiée dans le dépôt GitHub (nukep/gbshader)
Processus de création 3D
- Développement du look initial avec Blender, puis poursuite du projet après un résultat visuellement satisfaisant
- Génération de normal maps à l’aide de Cryptomatte et de shaders personnalisés
- Pour le modèle de théière, rotation de la caméra afin d’exporter les normal maps en séquence PNG
- Pour l’écran du modèle Game Boy Color, rendu dans une scène séparée puis composition
Fondements mathématiques
- La normal map sert de champ de vecteurs encodant le vecteur normal de chaque pixel
- Le shading de Lambert se calcule sous la forme du produit scalaire
v = N·L - En passant aux coordonnées sphériques, on simplifie en
v = sinNθ sinLθ cos(Nφ−Lφ) + cosNθ cosLθ- Le rayon de tous les vecteurs est supposé valoir r=1 afin de réduire la quantité de calculs
Implémentation sur Game Boy
- Lθ (angle vertical de la lumière) est fixé comme constante, et seul Lφ (angle de rotation de la lumière) est contrôlé par le joueur
- La ROM stocke chaque pixel sous la forme
(Nφ, log(m), b) - Pour résoudre l’absence d’instruction de multiplication, le projet utilise une transformation logarithmique et des tables de correspondance (
log,pow)- Le bit de signe est stocké dans le bit de poids fort afin de gérer les valeurs négatives
- Toutes les valeurs scalaires sont représentées comme des fractions 8 bits dans l’intervalle -1.0 à +1.0
- Les additions sont faites dans l’espace linéaire, les multiplications dans l’espace logarithmique
- Le dénominateur 127 est utilisé afin de pouvoir représenter à la fois +1 et -1
cos_log et opérations clés
cos_logest une table combinée sous la formelog(cos x), qui remplace la multiplication par une addition logarithmique- Coût des opérations par pixel
- 1 soustraction, 1 accès à
cos_log, 1 addition, 1 accès àpow, 1 addition - Soit un total de 3 additions/soustractions et 2 consultations de table
- 1 soustraction, 1 accès à
Performances
- Traitement de 15 tuiles par image, certaines lignes vides étant calculées plus rapidement
- Environ 130 cycles par pixel, contre 3 cycles pour une ligne vide
- Environ 89 % du CPU est utilisé pour les calculs du shader, le reste pour les entrées et les E/S
Code auto-modifiant (Self-Modifying Code)
- Pour optimiser la boucle principale qui traite environ 960 pixels par image, le projet modifie directement les instructions
- Les constantes sont injectées directement dans le code pour être plus rapides qu’un chargement de variable
- Exemple :
sub a, 8est 12 cycles plus rapide quesub a, variable - Gain global d’environ 11 520 cycles (10 %)
Tentatives d’usage de l’IA
- 95 % du projet ont été écrits manuellement
- L’IA a rencontré des difficultés pour écrire de l’assembleur Game Boy (SM83)
- Usages de l’IA
- Python : lecture des couches OpenEXR
- Blender : scripts d’automatisation de scène
- SM83 : quelques snippets fonctionnels (ex. : VRAM DMA)
- Tentatives infructueuses
- Génération du code assembleur du shader avec l’IA → code inefficace et nombreuses erreurs
- Tentative de génération d’assembleur à partir de pseudocode avec le modèle Claude Sonnet 4
- Quelques parties fonctionnaient, mais c’était lent et sujet à des erreurs comme la confusion entre Z80 et SM83
- Le code final a été entièrement réécrit manuellement
Conclusion et enseignements
- L’IA est utile pour les scripts simples, mais la précision et la vérification restent indispensables
- Dans le code de traitement OpenEXR, l’IA a provoqué une erreur d’ordre des canaux (BGR vs RGB) ayant entraîné un bug pendant plusieurs semaines
- L’expérience souligne la leçon suivante : « avec l’IA, la vérification est ce qu’il y a de plus important »
- Le projet est considéré comme un exemple expérimental d’implémentation de shader repoussant les limites d’un matériel legacy
1 commentaires
Avis sur Hacker News
Ça fait plaisir de voir sur HN un article avec un vrai esprit hacker
Le résultat est vraiment impressionnant. Si je comprends bien, c’est « un shader qui donne l’illusion de la 3D, mais qui applique en réalité des effets d’éclairage à une normal map 2D pré-rendue »
Les frames sont sur ce lien GitHub
La partie traitement des triangles 3D reste simple, et les shaders d’éclairage coûteux ne s’exécutent qu’une seule fois sur l’image 2D, ce qui est efficace
Du point de vue du shader, si l’entrée est un vecteur 3D, alors c’est un shader 3D. La présence ou non d’un rasterizer 3D est une autre question
Les jeux 3D modernes utilisent aussi ce genre d’approche de différentes façons. La technique des impostors, qui consiste à utiliser des modèles pré-rendus depuis plusieurs angles, est également employée dans de vrais moteurs 3D
Sauf qu’ici, ce qui est étonnant, c’est que ça tourne sur une Game Boy Color
Bonjour, je suis l’auteur. J’ai entendu dire que le billet avait été posté ici, alors j’ai créé un compte. Merci pour le partage
Je fais aussi des essais pour simplifier encore plus avec des environment maps, visibles via ce lien partagé sur Bsky
Projet vraiment fascinant. Ça me rappelle l’époque où je faisais du code assembleur sur C64.
À l’époque aussi, il n’y avait pas d’instruction de multiplication, donc il fallait trouver des moyens créatifs de contourner les limites du matériel
C’était une tentative d’utiliser l’IA, mais au final ça a été une expérience ratée.
Comme tout le secteur ne parle que d’IA, je voulais l’essayer moi-même, et je pense qu’il est important de déclarer de façon transparente l’usage de l’IA générative.
Le cacher nuit à la confiance, alors que le dire ouvertement permet d’avoir une discussion franche, même avec des gens qui ne partagent pas le même avis
Je voulais simplement documenter le processus
Ce shader GBC montre bien une vérité fondamentale : « tous les calculs, sous contrainte, ne sont que des approximations ».
Les multiplications sont remplacées par des consultations de table et des additions, et la précision est ajustée en fonction du résultat visuel recherché
Franchement impressionnant. Le plus étonnant, c’est surtout que ça tourne sur le vrai matériel Game Boy Color.
Souvent, on voit des cartouches avec un processeur puissant qui utilisent la GBC comme simple terminal, mais là, ce n’est pas ce genre de bidouille
Honnêtement, j’aimerais bien que Nintendo ressorte la GBC ou la GBA.
S’ils vendaient ça sous forme de cartouche avec quelques jeux intégrés, je l’achèterais tout de suite
Cela dit, aujourd’hui, un appareil portable Android au même format est plus pratique.
J’ai moi aussi une collection de Game Boy, mais maintenant les émulateurs sont bien plus confortables
Même si Nintendo en refaisait une, j’ai du mal à imaginer que ce serait aussi bien
C’est exactement pour ce genre d’articles que HN existe.
Ça redonne la sensation qu’on avait autrefois en feuilletant les vieux magazines techniques
Cet auteur est un génie complètement barré, dans le bon sens du terme