Einen 3D-Modellierer in einer Woche in C erstellen
- Im Herbst des letzten Jahres habe ich am einwöchigen Programmier-Event „Wheel Reinvention Jam“ teilgenommen
- Ziel war es, bestehende Softwaresysteme aus einer neuen Perspektive neu zu betrachten
- Ich habe einen 3D-Modellierer namens „ShapeUp“ erstellt, und es hilft, das Demo-Video von ShapeUp vor der Lektüre dieses Beitrags zuerst anzusehen
- ShapeUp kann direkt im Browser ausprobiert werden
Sprachauswahl: C
- Wegen der Langsamkeit des TypeScript-Compilers habe ich am Jam teilgenommen
- Mit dem TypeScript-Parser von esbuild oder Bun schien ein Projekt möglich, das eine schnelle Teilmenge von TypeScript implementiert
- Ich dachte jedoch, dass ein reiner Geschwindigkeitsvergleich von Terminal-Befehlen kein spannendes Demo ergeben würde, also wechselte ich zu einem 3D-Projekt
- Durch Ray-Marching mit Signed Distance Fields (SDFs) schien es möglich, ein 3D-Projekt in nur einer Woche von Grund auf aufzubauen
- Eine mit SDF erstellte Szene lässt sich deutlich schneller umsetzen als mit einem vergleichbaren triangulationsbasierten Renderer
- Ich hatte bereits SDF-Shader geschrieben, jedoch nur auf sehr grundlegender Ebene, und das Modellieren durch Code-Bearbeitung fühlte sich nicht natürlich an
- Ich wollte Formen per Maus bearbeiten, und dieses Jam bot die Chance, genau das umzusetzen
- Das Projekt nannte ich ShapeUp
Vorteile von C
- C ist eine sehr einfache und low-level Sprache, daher nahm ich an, dass viel Zeit für fehlende eingebaute Datenstrukturen und das Beheben von Pointer-Bugs aufgewendet werden müsste
- Die Schlichtheit von C erwies sich jedoch als Vorteil
- Es kompiliert sehr schnell
- Die Syntax verschleiert keine komplexen Operationen
- Es ist einfach und verlangt nicht ständiges Nachschlagen in der Sprache
- Es lässt sich problemlos sowohl nativ als auch nach WebAssembly kompilieren
- Die Nachteile von C lassen sich durch Gewohnheiten vermeiden, die ich in 22 Jahren mit der Sprache entwickelt habe
- ShapeUp besteht aus einer kleinen, einzelnen C-Datei und ist dadurch sehr einfach gehalten
Datenstruktur von ShapeUp
- Ein Modell besteht aus einem Array von
Shape-Strukturen
Shapes wird in einem statisch zugewiesenen Array gespeichert
- Kein Risiko für Allokationsfehler oder Speicherlecks
- Die Begrenzung auf 100 Shapes war in der Praxis nicht wirklich einschränkend
- Mit zu wenig Zeit für die Renderer-Optimierung wäre die Framerate vermutlich schon vor Erreichen von 100 Shapes eingebrochen
- Bei mehr Zeit hätte ich das Modell in kleine Blöcke unterteilt und in jedem Block Raymarching durchgeführt
- Dynamischer Speicher wird nur an drei Stellen via
malloc angefordert
- Speichern (einen großen Puffer allokieren, der das gesamte Dokument aufnehmen kann)
- OBJ-Export (einen großen Puffer allokieren, der alle Vertices aufnehmen kann)
- GLSL-Shader-Generierung (Puffer für den Shader-Quelltext)
- In allen Fällen gibt es am Ende der Funktion genau einen einzigen
free
- Ein Beispiel dafür, dass Speicherverwaltung in C einfach sein kann
- Sprachen wie C#, JavaScript und Python erzwingen eine Struktur, bei der für jede Shape einzeln
malloc aufgerufen und der jeweilige Pointer in einem dynamischen Array gespeichert wird
- C ist angenehm, weil man das Speicherlayout kontrollieren kann
Benutzeroberfläche
- Implementiert mit einem Immediate Mode User Interface (IMGUI)
- Ich mag IMGUI-UI
- Debugging ist extrem einfach
- Zum Platzieren von Elementen wird eine echte Programmiersprache genutzt (im Gegensatz zu CSS, Constraints oder SwiftUI)
- Wie bei den meisten IMGUIs wird per Enum nachverfolgt, welches Element Fokus hat oder welche Mausaktion gerade ausgeführt wird
- Für dieses Projekt waren keine dynamischen Arrays oder Hashmaps nötig; hätten wir sie gebraucht, hätte ich auf etwas wie
stb_ds.h zurückgegriffen
Probleme mit der Raylib-Bibliothek
- Die Entscheidung, C zu nutzen, war richtig, aber
raylib bereitete Probleme
- Es gibt einige seltsame Designentscheidungen, die die Entwicklererfahrung beeinträchtigen
- Anstelle eines erwarteten
enum wird int verwendet, wodurch die Typprüfung durch den Compiler unterbunden und die Funktion nicht selbst-dokumentierend wird
- Keine Standard-Validierung von Parametern (Design-Entscheidung)
- Keine Verantwortung für Abhängigkeiten (keine Behebung von GLFW-Issues, keine Einreichung von Patches)
- Die
raygui-UI-Bibliothek ist eher ein Spielzeug
- Kann keine Gleitkommazahlen anzeigen
- Es fehlt das Routing von Mausereignissen für überlappende oder abgeschnittene Elemente
- Keine runden Ecken
- Kein ansprechendes Styling möglich
- Es gibt auch Bugs
- Ein Bug, der den Schriftartenwechsel verhindert
- Die Draw-Funktion teilt keine Scheitelpunkte zwischen benachbarten Dreiecken, wodurch Pixel-Lücken entstehen
- Jedes gefundene Problem wurde gemeldet; die meisten wurden mit „won't fix“ geschlossen, und das Erstellen von Bug-Reports kostete so viel Zeit, dass ich es aufgegeben habe
- Es war hilfreich, dass
raylib ein OpenGL-Fenster bereitstellte, aber der Komfort hatte einen hohen Preis
- Zum Glück gab es einen Ausweg: Entweder OpenGL-Funktionen direkt zu nutzen oder Funktionen von Grund auf selbst zu implementieren
- Künftig werde ich auf
sokol setzen
Entwicklungsprozess in einer Woche
- ShapeUp bestand aus vier Kernbereichen, die ich in sechs Tagen fertigstellen musste
- Benutzeroberfläche (3D-Werkzeuge, Tastenkürzel, Seitenleiste, Gamepad)
- GLSL-Shader-Generator + Ray-Marching-Renderer
- GPU-basiertes Mouse Picking
- Marching Cubes für den Export
- Keines der Teile war schwer, aber es war schwierig, die richtige Priorisierung zu finden und nicht hängen zu bleiben
- Bei kniffligen oder zeitintensiven Problemen half ein Lösungsdesign oder eine einfache Lösung, die in 90 % der Fälle funktioniert
- Manchmal reicht es, eine Funktion einen Tag liegen zu lassen, um die Lösung unbewusst zu finden
- Ich habe versucht, immer einen funktionierenden 3D-Modellierer zu behalten und ihn schrittweise zu verbessern, solange es die Zeit erlaubt
- Ich dachte dabei an den Bau einer Pyramide: Auch wenn man Schicht für Schicht vorgeht und am Ende die Spitze vielleicht nicht fertig wird, ist an jeder Unterbrechungsstelle eine komplette Pyramide vorhanden
Projektergebnis
- Nach einer Woche hatte ich ein 3D-Programm, mit dem man sinnvolle 3D-Modelle erstellen und als
.obj exportieren kann
- Es läuft plattformübergreifend und bietet Funktionen zum Öffnen/Speichern von Dateien
- Das Projekt besteht aus 2.024 Zeilen C-Code und 250 Zeilen GLSL
- Es ist etwas erstaunlich, dass rund 2.300 Zeilen ausreichen, um einen einigermaßen brauchbaren 3D-Modellierer zu bauen
- Ich wurde gebeten, eine Jam-Zusammenfassung zu erstellen und eine ShapeUp-Demo auf der Handmade Seattle Conference zu zeigen
- Die Leute schienen von ShapeUp beeindruckt zu sein, doch es wirkte nicht wie ein großer Erfolg; es ist ein vergleichsweise einfaches Projekt
- Wenn es etwas gibt, das ich besonders gut gemacht habe, dann ist es das Gefühl dafür, was man bauen sollte, das notwendige Wissen und die Disziplin, es in einer Woche umzusetzen
Meinung von GN⁺
- Ein spannendes Projekt, das die Vorteile von C in Bezug auf Einfachheit und Geschwindigkeit gut zeigt. Aufgrund der niedrigen Abstraktionsebene von C scheint es jedoch schwierig, C unverändert in kommerzielle Projekte zu übernehmen. Wahrscheinlich wäre es enormer Aufwand, alle Funktionen moderner 3D-Modellierwerkzeuge direkt in C zu implementieren
- Es ist beeindruckend, dass ein funktionierendes Programm in einer Woche erstellt wurde. Im langfristigen Blick auf Wartbarkeit und Erweiterbarkeit wäre jedoch C++ oder Rust möglicherweise die bessere Wahl
- Die SDF-Rendertechnik ist schnell und einfach, erscheint aber in Bezug auf Modellierungsfreiheit und Qualität begrenzt. Kommerzielle Modelle nutzen meist SubD oder NURBS als Oberflächenmodellierung. Für Echtzeitbereiche wie Spiele oder Demos hat SDF-Rendering aber weiterhin hohen Nutzwert
- Ein gutes Beispiel dafür, wie schwierig die Auswahl einer Open-Source-Bibliothek ist. Man sollte Dokumentation, Codequalität und Support sorgfältig prüfen und bewusst entscheiden. Eigene Implementierung kann dabei eine gute Alternative sein
- Der pragmatische Ansatz, zuerst ein funktionierendes Programm zu bauen und dann schrittweise zu verbessern, ist auch in der Praxis sehr wertvoll. Es ist wichtig, die Prioritäten klug zu setzen, indem man Kernfunktionen zuerst fertigstellt und Details später verfeinert
1 Kommentare
Hacker News Kommentar