2 Punkte von GN⁺ 2024-05-03 | 1 Kommentare | Auf WhatsApp teilen
  • Aus der Aufgabe des Wheel Reinvention Jam, "bestehende Software aus einer neuen Perspektive zu betrachten", entstand ShapeUp – ein fertiger 3D-Modeller mit Browser-Demo und .obj-Export
  • Der Schlüssel dazu, innerhalb einer Woche ein 3D-Werkzeug bauen zu können, waren ray marched SDFs; im Vergleich zu einem dreiecksbasierten Renderer ließ sich damit schneller eine Szene mit Farben, weichen Schatten und Ambient Occlusion umsetzen
  • Die Implementierung blieb mit Fokus auf einer einzigen C-Datei bewusst einfach, und das Modell speichert bis zu 100 Shapes in einem statischen Array, um den Aufwand für Speicherverwaltung zu reduzieren
  • raylib half dabei, schnell ein OpenGL-Fenster zu öffnen, aber wegen der int-zentrierten API, fehlender Parameterprüfung, GLFW-Abhängigkeit und Grenzen von raygui musste direkt OpenGL verwendet oder Funktionalität neu gebaut werden
  • Das Endergebnis umfasst 2024 Zeilen C und 250 Zeilen GLSL, also insgesamt rund 2300 Zeilen, und unterstützt Datei öffnen/speichern, die Ausführung auf mehreren Plattformen sowie den .obj-Export

Wie ShapeUp zu einem 3D-Modeller wurde

  • Der Wheel Reinvention Jam war ein einwöchiges Programmier-Event, bei dem bestehende Softwaresysteme aus einem neuen Blickwinkel betrachtet werden sollten
  • Das erste Ziel war, aus Frust über langsame TypeScript-Kompilierung eine schnellere Teilmenge von TypeScript zu bauen als tsc
    • Mit dem TypeScript-Parser von esbuild oder Bun als Ausgangspunkt schien das machbar
    • Aber eine Erfolgsdemo, die nur daraus besteht, dass „ein Terminalbefehl schneller endet als ein anderer“, war visuell nicht besonders attraktiv, also ging es am Ende in Richtung 3D
  • ShapeUp wurde als 3D-Modeller zum Bearbeiten von Formen mit der Maus gebaut
    • Es gab bereits Erfahrung mit dem Schreiben klassischer SDF-Shader, aber Modellierung durch direktes Bearbeiten von Code fühlte sich nicht natürlich an
    • Das Ziel war, SDF-basierte Formbearbeitung mit der Maus zu ermöglichen

Warum SDFs ein Wochenprojekt möglich machten

  • Die Rendering-Grundlage von ShapeUp sind ray marched signed distance fields (SDFs)
  • Selbst mit Farben, weichen Schatten und Ambient Occlusion ließ sich eine SDF-Szene schneller umsetzen als ein dreiecksbasiertes Rendering-System
  • Ein Beispiel von Inigo Quilez, der mit SDFs in einer Sitzung eine Pixar-artige Figur erstellt, diente als technischer Orientierungspunkt
  • ShapeUp behandelt SDF-Modellierung als direkte Manipulation von Formen statt als Code-Editing

C-Implementierung und Datenstrukturen

  • ShapeUp ist in C geschrieben und verwendet raylib zum Erzeugen eines OpenGL-Fensters
  • Die Wahl fiel auf C wegen schneller Kompilierung, einer Syntax, die komplexes Verhalten nicht verbirgt, Vertrautheit und der Möglichkeit, sowohl nativ als auch nach WebAssembly zu kompilieren
  • Das Modell besteht aus einer Menge von Shape-Strukturen
    • Jedes Shape hat Position, Größe, Winkel, Kantenradius, Blob-Stärke, Farbe, Spiegelachse und ein Kennzeichen dafür, ob es subtrahierend wirkt
  • Die Shape-Liste wird nicht dynamisch allokiert, sondern als statisches Array verwaltet
    • MAX_SHAPE_COUNT ist 100
    • Der Zustand wird über Shape shapes[MAX_SHAPE_COUNT], shape_count und selected_shape verwaltet
    • Dieser Ansatz vermeidet fehlgeschlagene Allokationen und Speicherlecks
  • Die Begrenzung auf 100 Shapes war in der Praxis kein großes Problem
    • Bevor 100 erreicht wurden, brach die Framerate wegen fehlender Zeit für Renderer-Optimierung zuerst ein
    • Mit mehr Zeit wäre das Modell in kleine Ziegel unterteilt worden, innerhalb derer Ray Marching ausgeführt wird

Art der Speichernutzung

  • ShapeUp verwendet nur an drei Stellen dynamische Speicherallokation
    • Speichern: Allokation eines Puffers für das gesamte Dokument
    • .OBJ-Export: Allokation eines Puffers für alle Vertices
    • GLSL-Shader-Erzeugung: Allokation eines Puffers für den Shader-Quelltext
  • In jedem Fall wird am Ende der Funktion genau einmal free aufgerufen
  • Man könnte auch jedes Shape per malloc anlegen und Pointer in einem dynamischen Array speichern, aber für dieses Projekt war eine solche Struktur nicht nötig
  • Ein Vorteil von C war die direkte Kontrolle über das Speicherlayout
  • Falls dynamische Arrays oder Hashmaps nötig gewesen wären, hätte man Werkzeuge wie stb_ds.h verwenden können

Wie die UI umgesetzt wurde

  • Die UI ist im Stil einer immediate mode user interface (IMGUI) implementiert
  • Vorteile von IMGUI sind einfache Fehlersuche und dass sich Elementpositionen mit einer echten Programmiersprache statt mit CSS, Constraints oder SwiftUI festlegen lassen
  • Fokussierte Elemente und Mausaktionen werden mit einem Control-Enum verfolgt
    • Zustände wie Position, Größe, Winkel, Farbe, Verschieben, Rotieren, Skalieren, Kameradrehung und Blob-Stärke werden durch Enum-Werte dargestellt
    • focused_control und mouse_action enthalten den aktuellen UI-Zustand

Wo raylib und raygui im Weg standen

  • raylib war nützlich, um schnell ein OpenGL-Fenster zu öffnen, wurde mit der Zeit aber zu einem Faktor, der das Entwicklungstempo bremste
  • Besonders unangenehm an der raylib-API war der Mangel an Typinformation
    • Selbst dort, wo Enum-Typen erwartet werden, wird int verwendet, sodass der Compiler keine Typprüfung machen kann
    • Schon aus der Funktionssignatur geht oft nicht klar hervor, was ein Parameter bedeutet
    • Bei IsGestureDetected(unsigned int gesture) sieht gesture zum Beispiel wie eine registrierte Gesten-ID aus, ist tatsächlich aber ein Gesture-Enum
    • Da sich die Dokumentation stark auf Header-Dateien stützt, musste man in die Implementierung schauen, um herauszufinden, welche int-Werte eigentlich Enums sind
  • Auch das Fehlen grundlegender Parameterprüfung verschärfte Probleme
    • LoadFileData(const char *fileName, int * dataSize) führt zu einem Segfault, wenn dataSize NULL ist
    • Im Header ist nicht erkennbar, dass dataSize ein Ausgabeparameter ist oder nicht NULL sein darf
    • Wegen fehlender Validierung waren selbst einfache Probleme schwer nachzuverfolgen, und teils konnte fehlerhaftes Verhalten still auftreten
  • Auch beim Umgang mit Abhängigkeiten gab es Abweichungen von den Erwartungen
    • Probleme in GLFW wurden von raylib nicht umgangen, und es wurden auch keine Patches eingereicht
    • Für Endnutzer ist wichtiger, dass raylib korrekt funktioniert, als wie Fenstererzeugung intern umgesetzt ist
  • Die UI-Bibliothek raygui war für das Projekt zu eingeschränkt
    • Fließkommazahlen konnten nicht angezeigt werden, sodass eigene Float-Textfelder gebaut werden mussten
    • Mouse-Event-Routing für überlappende oder abgeschnittene Elemente wurde nicht unterstützt
    • Die in UIs üblichen abgerundeten Ecken wurden nicht unterstützt
    • Eine ansprechende Gestaltung war schwer umzusetzen
  • Auch Bugs störten den Entwicklungsfluss
    • Wegen eines Fehlers in den raygui-Tools ließ sich die überstylte Standard-Schriftart nicht ersetzen
    • Zeichenfunktionen wie DrawCircle(...) teilen Vertices zwischen Dreiecken nicht, sodass bei Skalierung oder Rotation in der aktuellen Matrix durch Gleitkommafehler Pixel-Lücken entstanden
  • Entdeckte Probleme wurden eine Zeit lang gemeldet, aber meist als „wont fix“ geschlossen; danach wurden keine weiteren Meldungen mehr eingereicht
  • Der Ausweg bestand darin, OpenGL-Funktionen direkt zu verwenden oder benötigte Funktionalität von Grund auf selbst zu implementieren
  • Künftig soll statt raylib sokol verwendet werden

Vier Dinge, die in 6 Tagen fertig werden mussten

  • ShapeUp musste in 6 Tagen im Wesentlichen vier Teile fertigstellen
    • Benutzeroberfläche: 3D-Gizmos, Tastaturkürzel, Seitenleiste, Gamecontroller
    • GLSL-Shader-Generator und Ray-Marching-Renderer
    • GPU-basierte Mausauswahl
    • Marching Cubes für den Export
  • Die Schwierigkeit lag weniger in den einzelnen Funktionen als darin, die Prioritäten beizubehalten
  • Schwierige oder zeitaufwendige Probleme wurden durch Designänderungen umgangen oder mit einer einfachen Lösung behandelt, die in 90 % der Fälle funktioniert
  • Bei manchen Funktionen kam die Lösung erst, nachdem sie einen Tag lang liegen gelassen worden waren
  • Der Arbeitsstil bestand darin, immer einen funktionsfähigen 3D-Modeller zu behalten und ihn nur so weit zu verbessern, wie die Zeit es zuließ
    • Verglichen wird das mit einem Ansatz, bei dem nicht erst am Ende eine Pyramide entsteht, sondern in jedem Zwischenstand bereits eine vollständige kleine Pyramide vorhanden ist

Endergebnis

  • Am Ende der Woche konnte ShapeUp sinnvolle 3D-Modelle erstellen und als .obj-Dateien exportieren
  • Es läuft auf mehreren Plattformen und unterstützt das Öffnen und Speichern von Dateien
  • Der Codeumfang beträgt 2024 Zeilen C und 250 Zeilen GLSL
  • Auffällig ist, dass sich mit rund 2300 Zeilen bereits ein einigermaßen nützlicher 3D-Modeller bauen ließ
  • Das Projekt selbst ist vergleichsweise einfach, aber entscheidend waren das Gespür dafür, was man bauen sollte, das Wissen, wie man es bauen kann, und die Disziplin, es in einer Woche abzuschließen

1 Kommentare

 
GN⁺ 2024-05-03
Meinungen auf Hacker News
  • Ich stimme dem Autor in Bezug auf die Grenzen von Raylib völlig zu. Ich baue gerade ein Tower-Defense-Spiel, das ich mit Raylib begonnen habe, und stoße auf viele derselben Einschränkungen und weitere Probleme.
    Zum Beispiel funktioniert das Umschalten in den Vollbildmodus je nach Plattform nicht konsistent, Bildschirmmodi lassen sich nicht auflisten, Rendering-Funktionen sind zur Laufzeit schwer zu toggeln, es gibt Probleme beim Speichern kompilierter Shader usw.
    Trotzdem bin ich dankbar für die Arbeit, die Ray in diese Bibliothek gesteckt hat, und ich werde sie weiter unterstützen. Raylib ist hervorragend, um schnell Prototypen zu bauen, aber darüber hinaus wird es schwierig, sofern man nicht starke Einschränkungen in Kauf nimmt.
    Ich habe definitiv etwas daraus gelernt, aber inzwischen ist die Entwicklung zu weit fortgeschritten, um den gesamten Raylib-bezogenen Code durch etwas wie SDL zu ersetzen.

    • Das Detail, dass Raylib nicht von Raytracing kommt, sondern vom Vornamen des Machers, Ray, ist interessant.
      Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
    • Raylib ist leicht für den Einstieg, aber sobald das Projekt etwas komplexer wird, beißt es zurück. SDL braucht dagegen anfangs mehr Setup-Zeit, skaliert aber mit wachsendem Projekt sehr gut. Auch die Codequalität ist außergewöhnlich gut.
    • Raylib hat viele Probleme, die wohl auch künftig nicht behoben werden, aber das Vollbild-Problem allein Raylib anzulasten, ist schwierig. Vollbild unter Windows ist seit Jahrzehnten praktisch unbenutzbar kaputt, und bei anderen Plattformen dürfte es ähnlich sein.
      Die heutige Strategie ist einfach, einen randlosen Fenstermodus zu verwenden und so zu tun, als gäbe es echtes Vollbild nicht.
    • Fühlt sich ähnlich an. Ich habe vor etwa zwei Monaten ein Projekt gestartet und Raylib gewählt; die Grundlagen laufen wirklich einfach, aber je länger man es nutzt, desto mehr kleine, zufällige Unannehmlichkeiten tauchen auf. Inzwischen habe ich zu viel in das Projekt investiert, um die Raylib-Nutzung noch rückgängig zu machen.
      Mein größtes Problem im Moment sind Font-Handling und Text-Rendering. Wahrscheinlich muss ich statt TTF-Fonts auf vorab gebackene Bitmap-Fonts wechseln, was bei späterer Lokalisierung ziemlich schmerzhaft werden dürfte.
      Die zwei Funktionen, die ich nach dem Wechsel von Love2D am meisten vermisse, sind das einfache Rendern von Text mit mehreren Farben und das einfache Zuschneiden und Wiederholen bzw. Kacheln von Texturen. In Raylib muss man Text anhand von Farb-Markup selbst in Stücke zerlegen, Breiten-Offsets anwenden und dann unter Berücksichtigung von Zeilenumbrüchen für jedes Stück die Zeichenfunktion aufrufen.
      Wenn viel Text auf dem Bildschirm gezeichnet wird, scheint auch die FPS deutlich zu fallen; möglicherweise wird das Batching der Draw Calls für Text unterbrochen. Früher gab es eine Funktion zum Zeichnen gekachelter Texturen, die aus irgendeinem Grund entfernt wurde.
    • Das hat mich neugierig gemacht, mir raylib anzusehen. Es gibt niedliche Beispiele, die als WebAssembly laufen: https://www.raylib.com/examples.html
      Was mich bei Wasm und 3D/2D-Grafik im Browser immer stört, sind kleine Probleme wie Scrollen, die man häufig sieht. Siehe hier das Beispiel „Background scrolling & parallax“: https://www.raylib.com/examples.html
      Ich habe es auf mehreren Geräten getestet, und wenn meine Augen mich nicht täuschen, ist das definitiv kein smooth scrolling. Es wirkt absurd, dass flüssiges 2D-Scrolling im Jahr 2024 immer noch kein gelöstes Problem ist.
  • „Shapes werden in einem statisch allozierten Array gespeichert. Keine fehlgeschlagenen Allokationen, keine Leaks, kein Ballast. Liebenswert. Das Limit von 100 Shapes war in der Praxis keine Einschränkung. Da kaum Zeit für Renderer-Optimierung blieb, wäre die Framerate wohl schon gefallen, bevor ich 100 erreicht hätte.“
    Eines der besten Beispiele für das Vermeiden vorschneller Optimierung, die ich in letzter Zeit gesehen habe.

    • Ich würde eher sagen: genau das Gegenteil. Es wurde vorschnelle Abstraktion und Generalisierung vermieden.
    • Ein Paradebeispiel für den Unterschied zwischen Menschen, die Dinge tatsächlich bauen, und Menschen, die nur herumsitzen und darüber streiten, wie man sie bauen sollte.
  • Ein wirklich interessanter Artikel, und ich mochte, dass er über verschiedene Entscheidungen spricht, etwa zur Speicherverwaltung und zu den Problemen, auf die er mit raylib gestoßen ist. Ich bin gerade bei Teil 2 von Crafting Interpreters angekommen und frische C wieder auf; daher war es schön, erneut daran erinnert zu werden, worin C gut ist.

  • Die Live-Demo im Video ist wirklich gut. Ganz abgesehen davon, die App zu bauen: Wenn ich es versucht hätte, hätte ich vermutlich nicht einmal dieses Video innerhalb einer Woche hinbekommen.

    • Das Video zu machen hat länger gedauert als die App. Ich weiß nicht, wie YouTuber das so konstant schaffen.
  • Vor langer Zeit habe ich einmal an einem Betriebssystem für ein Tischtelefon gearbeitet. Es hatte nur 64K RAM, daher gab es überhaupt keine dynamische Speicherverwaltung; wir nutzten viele statische Variablen, sodass der Compiler alles zur Compile-Zeit platzieren konnte.
    Man vergisst leicht, dass viele Anwendungen möglicherweise gar keine dynamische Speicherverwaltung brauchen. Oft reicht es aus, ein paar Buffer fester Größe zuzuweisen und die Ausnahmefälle sauber zu behandeln, wenn diese Buffer voll sind.
    In diesem Kontext ist C tatsächlich viel sicherer. Es gibt keine Speicherlecks, und worum man sich sorgen muss, sind nur Buffer Overflows. Wenn alle Variablen statisch alloziert sind, kann man das durch vorsichtige Verwendung von sizeof handhaben.
    Das heißt nicht, dass Rust und Go heutzutage keine hervorragenden Optionen wären, aber das schlichte alte C funktioniert immer noch gut und muss nicht albtraumhaft komplex sein.

  • Etwas off-topic, aber ich freue mich, zum ersten Mal ein WebAssembly-Interface zu sehen, bei dem der Text nicht verschwommen wirkt. Wirklich zum ersten Mal.
    Wenn man Programme und einige Betriebssysteme, zum Beispiel auch Windows, mit einbezieht, ist in den letzten Jahren ein allgemeines Problem entstanden, weil eine bestimmte Art der Text-Rasterisierung zum gemeinsamen Trend und zur Standardeinstellung wurde.
    Leider können Nutzer Antialiasing oft nicht abschalten, um scharfen Text zu bekommen, und selbst wenn es selten eine Option gibt, bleibt Antialiasing bei Oberflächen wie Menüs weiterhin aktiv.

    • Dieser scharfe Text ist nicht besonders beeindruckend. Die Schrift hat keine Kurven und kein Smoothing. Bei jeder Auflösung wird sie scharf und blockig-kantig aussehen.
    • Ich frage mich, welche Verbindung es zwischen WebAssembly und Rasterisierung gibt. Das klingt wirklich interessant.
  • Ich mag solche Projekte wirklich. Ich mag immer noch die Low-Level-Natur von C. Heute nutze ich viel Rust und Elixir/Erlang, aber die Einfachheit und Explizitheit von C vermisse ich oft.
    Deshalb nutze ich auch viel Zig; es ist eine Sprache, die viel von Cs Philosophie bewahrt und sie zugleich sehr ordentlich verbessert.

    • Erlang ist ebenfalls eine ziemlich einfache Sprache.
  • Ich stimme seiner Einschätzung zu C wirklich zu. Besonders dem Teil, dass „die Syntax kein komplexes Verhalten verbirgt. Sie ist einfach genug, dass man nicht ständig nachschlagen muss“; darüber hinaus ist es auch sehr leicht und lehrreich, wenn man doch einmal etwas zu C nachschlagen muss.
    Einfache, alte Sprachen haben eben ihre Vorteile.

  • Wenn man jede Shape separat mit malloc alloziert und diese Pointer in einem dynamischen Array speichert, kann man es sich definitiv selbst schwerer machen. Es gibt die Aussage, dass Sprachen wie C# eine solche Allokationsstruktur erzwingen würden; ich frage mich aber, was einen in C# daran hindert, wie der Autor in C ein festes Array von Structs zu verwenden.

    • Nichts hindert einen daran. Auch in C# ist es nicht ungewöhnlich, auf diese Weise Struct-Arrays zu verwenden.
  • Ich hoffe, jemand führt dieses Projekt weiter. Mit ein paar Monaten zusätzlichem Feinschliff könnte es für bestimmte Einsatzzwecke eine ernsthafte Alternative zu Blender oder FreeCAD werden, und die Lernkurve wirkt deutlich flacher.

    • Man sollte sich auch MagicaCSG ansehen, das eine ausgereiftere Version ist und weiterhin kostenlos: https://ephtracy.github.io/index.html?page=magicacsg#ss-caro...
    • Korrektur: Ah, dieses Programm unterstützt bereits den Mesh-Export per Marching Cubes. Siehe das YouTube-Video auf der Website. Das wusste ich nicht.
      Da es grundsätzlich mit SDF arbeitet, unterscheiden sich Modellierungserfahrung und gespeicherte Daten allerdings von traditionellen Meshes mit Dreiecken, Vertices usw.
      SDFs lassen sich mit Verfahren wie Marching Cubes in Meshes umwandeln, aber solche Daten muss man später in Apps wie Blender wahrscheinlich ohnehin bereinigen.
      Wenn auch der Renderer SDF-basiert ist, sind SDFs großartig. Die meisten sind es aber nicht.
      Sorry, falls das schon bekannt war.
    • Wenn man SDFs mag, ist Womp ein ziemlich guter Einstieg. Tinkercad ist als CAD für Einsteiger ebenfalls recht ordentlich.
    • Dune3D und Salome-Platform sollte man sich ebenfalls ansehen.
    • Eine Alternative zu Blender kann es sein, aber leider wohl keine Alternative für CAD-Arbeiten.