- Ein Projekt, das Echtzeit-3D-Shading auf dem Game Boy Color umsetzt, bei dem der Spieler die Bahn des Lichts steuern und ein Objekt rotieren lassen kann
- Basierend auf Berechnungen mit normalisierten Vektoren und Lambert-Shading (Skalarprodukt) werden die Operationen durch die Verwendung von Kugelkoordinaten vereinfacht
- Um die Beschränkungen der SM83-CPU ohne Multiplikationsbefehl zu überwinden, werden Logarithmentransformationen und Lookup-Tabellen genutzt, um Berechnungen mit 8-Bit-Präzision durchzuführen
- Durch den Einsatz von selbstmodifizierendem Code (self-modifying code) wurde eine Leistungssteigerung von etwa 10 % erreicht und es werden 15 Tiles pro Frame gerendert
- Die KI-gestützte Codegenerierung scheiterte größtenteils; der Kernalgorithmus und der Shader wurden als von Hand geschriebener Code fertiggestellt
Projektüberblick
- Entwicklung eines Spiels, das Bilder in Echtzeit auf dem Game Boy Color rendert
- Der Spieler steuert ein kreisendes Licht und rotiert dabei das Objekt
- Der vollständige Code ist im GitHub-Repository (nukep/gbshader) öffentlich verfügbar
3D-Erstellungsprozess
- Mit Blender wurde zunächst das Lookdev erstellt; weil das Ergebnis visuell überzeugte, wurde das Projekt weiterverfolgt
- Mit Cryptomatte und einem benutzerdefinierten Shader wurden Normal Maps erzeugt
- Beim Teekannenmodell wurde die Kamera rotiert, um die Normal Maps als PNG-Sequenz auszugeben
- Der Bildschirmbereich des Game-Boy-Color-Modells wurde in einer separaten Szene gerendert und anschließend komponiert
Mathematische Grundlage
- Eine Normal Map dient als Vektorfeld, das den Normalenvektor jedes Pixels codiert
- Lambert-Shading wird als Skalarprodukt in der Form
v = N·L berechnet
- Durch Umwandlung in Kugelkoordinaten wird dies zu
v = sinNθ sinLθ cos(Nφ−Lφ) + cosNθ cosLθ vereinfacht
- Für alle Vektoren wird
r=1 angenommen, um den Rechenaufwand zu verringern
Implementierung auf dem Game Boy
- Lθ (vertikaler Lichtwinkel) wird als Konstante fixiert, und nur Lφ (Rotationswinkel des Lichts) wird vom Spieler gesteuert
- Im ROM wird jedes Pixel in der Form
(Nφ, log(m), b) gespeichert
- Um das Fehlen eines Multiplikationsbefehls zu kompensieren, werden Logarithmentransformationen und Lookup-Tabellen (
log, pow) verwendet
- Das Vorzeichenbit wird im höchstwertigen Bit gespeichert, um negative Berechnungen zu unterstützen
- Alle Skalarwerte werden als 8-Bit-Brüche im Bereich -1.0 bis +1.0 dargestellt
- Additionen erfolgen im linearen Raum, Multiplikationen im logarithmischen Raum
- Als Nenner wird 127 verwendet, damit sowohl ±1 darstellbar sind
cos_log und die Kernoperationen
cos_log ist ein kombiniertes Lookup in der Form log(cos x) und ersetzt Multiplikation durch logarithmische Addition
- Rechenaufwand pro Pixel
- 1 Subtraktion, 1
cos_log-Lookup, 1 Addition, 1 pow-Lookup, 1 Addition
- Insgesamt 3 Additionen/Subtraktionen und 2 Lookups
Leistung
- Es werden 15 Tiles pro Frame verarbeitet; einige leere Zeilen lassen sich schneller berechnen
- Etwa 130 Zyklen pro Pixel, leere Zeilen benötigen 3 Zyklen
- Rund 89 % der CPU werden für die Shader-Berechnungen verwendet, der Rest für Eingaben und I/O
Selbstmodifizierender Code (Self-Modifying Code)
- Um die Kernschleife zu optimieren, die pro Frame etwa 960 Pixel verarbeitet, werden die Befehle selbst modifiziert
- Konstanten werden direkt in den Code eingebettet, um schneller zu sein als das Laden von Variablen
- Beispiel:
sub a, 8 ist 12 Zyklen schneller als sub a, variable
- Insgesamt werden etwa 11.520 Zyklen (10 %) eingespart
Versuche mit KI
- 95 % des gesamten Projekts wurden von Hand geschrieben
- KI hatte Schwierigkeiten beim Schreiben von Game-Boy-Assembler (SM83)
- Einsatzbereiche der KI
- Python: Lesen von OpenEXR-Layern
- Blender: Automatisierungsskripte für Szenen
- SM83: einige Funktions-Snippets (z. B. VRAM-DMA)
- Fehlgeschlagene Versuche
- Versuch, mit KI Shader-Assemblercode zu erzeugen → ineffizient und voller Fehler
- Mit dem Modell Claude Sonnet 4 wurde versucht, aus Pseudocode Assembler zu erzeugen
- Teilweise funktionierte das, war aber langsam und enthielt Fehler wie die Verwechslung von Z80 und SM83
- Der finale Code wurde manuell vollständig neu geschrieben
Fazit und Erkenntnisse
- KI ist für einfache Skripte nützlich, aber Korrektheit und Verifikation sind unverzichtbar
- Im OpenEXR-Verarbeitungscode verursachte KI einen Fehler bei der Kanalreihenfolge (BGR vs RGB), was wochenlange Bugs auslöste
- Als Lehre aus der Erfahrung wird betont: „Bei der Nutzung von KI ist Verifikation am wichtigsten.“
- Das Projekt gilt als experimentelles Beispiel für eine Shader-Implementierung, die die Grenzen von Legacy-Hardware überwindet
1 Kommentare
Hacker-News-Kommentare
Es ist schön, auf HN mal wieder einen Beitrag mit echtem Hacker-Spirit zu sehen.
Das Ergebnis ist wirklich großartig. So wie ich es verstehe, ist das ein „Shader, der wie 3D aussieht, in Wirklichkeit aber mit vorgerenderten 2D-Normal-Maps Lichteffekte erzeugt“.
Die Frames sind hier auf GitHub zu finden.
Die 3D-Dreiecksverarbeitung bleibt einfach, und teure Licht-Shader werden nur einmal auf dem 2D-Bild ausgeführt, was effizient ist.
Wenn für den Shader die Eingabe ein 3D-Vektor ist, dann ist es ein 3D-Shader. Ob es einen 3D-Rasterizer gibt, ist eine andere Frage.
Moderne 3D-Spiele nutzen solche Ansätze ebenfalls auf verschiedene Weise. Auch die Imposter-Technik, bei der Modelle aus mehreren Blickwinkeln vorgerendert werden, wird in vollwertigen 3D-Engines eingesetzt.
Der erstaunliche Teil ist nur, dass es dieses Mal auf dem Game Boy Color läuft.
Hallo, ich bin der Autor. Ich habe gehört, dass der Beitrag hier gepostet wurde, und deshalb einen Account erstellt. Danke fürs Teilen.
Ich experimentiere auch mit einer weiteren Vereinfachung mithilfe von Environment Maps; zu sehen ist das in diesem auf Bsky geteilten Link.
Wirklich ein faszinierendes Projekt. Es erinnert mich an meine Zeit mit C64-Assemblerprogrammierung.
Damals gab es auch keinen Multiplikationsbefehl, also musste man kreative Wege finden, um die Hardware-Beschränkungen zu umgehen.
Es war ein Versuch, AI zu verwenden, aber letztlich ein gescheitertes Experiment.
Da die Branche so viel über AI redet, wollte ich es selbst ausprobieren, und ich halte es für wichtig, die Nutzung generativer AI transparent offenzulegen.
Wenn man es verbirgt, schadet das dem Vertrauen, und wenn man es offenlegt, kann man auch mit Menschen mit anderen Ansichten offen darüber sprechen.
Ich wollte diesen Prozess einfach nur dokumentieren.
Dieser GBC-Shader zeigt die Wahrheit, dass bei solchen Beschränkungen alle Berechnungen Annäherungen sind.
Multiplikation wird durch Tabellen-Lookups und Addition ersetzt, und die Genauigkeit wird auf das sichtbare Ergebnis abgestimmt.
Wirklich beeindruckend. Besonders erstaunlich ist, dass das auf echter Game-Boy-Color-Hardware läuft.
Oft steckt man einen leistungsfähigen Prozessor in ein Cartridge und nutzt den GBC nur als simples Terminal, aber das hier ist kein solcher Hack.
Ehrlich gesagt würde ich mir wünschen, dass Nintendo den GBC oder GBA neu auflegt.
Wenn sie ihn in Form eines Cartridges mit ein paar vorinstallierten Spielen verkaufen würden, wäre ich sofort dabei.
Allerdings sind heute Android-Handhelds im gleichen Formfaktor praktischer.
Ich habe selbst eine Game-Boy-Sammlung, aber inzwischen sind Emulatoren deutlich bequemer.
Selbst wenn Nintendo etwas Neues bauen würde, wäre es vermutlich nicht so gut.
Solche Beiträge sind genau der Grund, warum es HN gibt.
Sie bringen mir wieder das Gefühl zurück, das ich früher beim Durchblättern alter Technikmagazine hatte.
Dieser Autor ist im besten Sinne ein verrücktes Genie.