Halfspace

Halfspace dient als showcase-applicatie voor de Fidget-kernel, die wordt gebruikt voor rasterisatie en meshing. Binnen de GUI worden afbeeldingen in (bijna) real-time gerasteriseerd; modellen kunnen worden geëxporteerd als afbeeldingen of als driehoekige meshes (triangle meshes).

Aangezien het 2026 is, wil ik vooraf opmerken dat dit niet "vibe-coded" is. Ik werk er sinds april 2025 aan en schrijf de code met mijn menselijke brein, om uiteenlopende redenen (en Fidget dateert al uit 2022).

De rest van dit verslag gaat uit van enige kennis van impliciete oppervlakken (implicit surfaces).

Waarom Halfspace?

Ik heb veel tijd besteed aan het schrijven van impliciete kernels, iets minder tijd aan het bouwen van GUI's voor die kernels, en nog minder tijd aan het daadwerkelijk modelleren met die tools. In de praktijk heb ik in mijn dagelijks leven niet veel behoefte aan solid modeling, dus het meeste wat ik maak is een demo of een voorbeeld van hoe een specifieke kernel gebruikt moet worden.

Toch is me een bepaalde spanning opgevallen bij het werken met impliciete oppervlakken. Werken met low-level impliciete oppervlakken is een beetje als het schrijven van assembly: het is low-level, krachtig en irritant. Als je alleen x-, y- en z-variabelen krijgt – en het jouw verantwoordelijkheid is om deze te combineren tot de vormen van je dromen – dan kan dat een pijnlijke ervaring zijn.

Wanneer mensen geconfronteerd worden met de pijn van het schrijven van assembly, bouwen ze meestal abstracties daarop: high-level talen en bibliotheken die compileren naar een low-level representatie. Het equivalent hiervan is een standaardbibliotheek van vormen en transformaties: sfeer, box, translate, scale, etc.

Er is ook een minder gebruikelijke aanpak voor de pijn van assembly: assembly zelf minder pijnlijk maken om te schrijven. Mijn favoriete project in die richting is Mu van Kartik Agaram, dat emulatie, tracing en time-travel debugging combineert rond een subset van de x86-assemblytaal.

Halfspace kiest beide paden. Het bevat een (kleine maar groeiende) standaardbibliotheek, maar maakt het ook gemakkelijk om modellen incrementeel op te bouwen: een complex model kan worden opgesplitst in kleinere stukken, die afzonderlijk kunnen worden geparametriseerd en gevisualiseerd.

Solid Modeling

Ten eerste betekent "solid modeling" dat we ons richten op objecten met een definitieve binnen- en buitenkant; je moet in staat zijn om elk punt in de ruimte te kiezen en te bepalen of het zich binnen of buiten het model bevindt.

Dit lijkt vanzelfsprekend, maar er is veel modellering waarbij deze eigenschap niet relevant is. Als je naar een willekeurige video game model viewer kijkt, zie je genoeg oneindig dunne getextureerde muren, opgebouwd uit een enkele waaier van driehoeken.

Omdat mijn achtergrond ligt in CAD/CAM-software (met een nadruk op 3D-printen), wil ik modellen die fysiek gerealiseerd kunnen worden.

Er zijn verschillende manieren om solid modeling uit te voeren. In de meeste CAD-software is een boundary-representation (B-rep) geometrie-kernel verantwoordelijk voor het aan elkaar stikken van een reeks individuele oppervlakken tot een solide lichaam. Dit is een enorm moeilijk probleem – bijvoorbeeld: de doorsnijding van twee NURBS-oppervlakken heeft mogelijk geen gesloten-vorm oplossing!

Sinds mijn masterthesis werk ik aan geometrie-kernels gebaseerd op impliciete oppervlakken. Deze hebben het voordeel dat ze concepteel geschreven en volledig begrepen kunnen worden door één persoon of een klein team, waardoor ze goed passen bij fabricagesoftware op persoonlijke schaal.

Dit wordt voortgezet in Halfspace: het is een GUI rondom de Fidget-geometrie-kernel. Modellen kunnen worden ontworpen met een combinatie van vooraf gedefinieerde primitieven en handgeschreven scripts, en kunnen worden geëxporteerd als afbeeldingen of driehoekige meshes.

Een IDE voor afstandsvelden

We zouden de Fidget-kernel puur op de constructive solid geometry (CSG) laag kunnen gebruiken, door vormen (sferen, kubussen, cilinders, etc.) te bouwen en deze te combineren met logische operaties (unie, intersectie, differentie). Halfspace maakt echter de keuze om de onderliggende afstandsvelden (distance fields) op de voorgrond te plaatsen.

Een voorbeeld van waarom dit belangrijk is: beschouw twee afstandsvelden voor een zaagtandgolf, die op elk punt in de ruimte dezelfde tekens hebben, maar verschillende waarden.

In een visualisatie wordt het teken (dat binnen versus buiten definieert) getoond door kleur (blauw versus oranje), waarbij de grens van de vorm in het wit wordt getoond (overeenkomend met een waarde van 0). De veldwaarden worden getoond door vage lijnen die met regelmatige intervallen zijn geplaatst (zoals op een topografische kaart).

Ondanks identieke tekens overal, is het eerste veld zeer slecht gedisciplineerd. Kijk naar de verticale rand van de zaagtand: er is een overgang van binnen (blauw) naar buiten (oranje) zonder een passage door nul.

Dit is een $C^0$ of "jump" discontinuïteit, en dat is slecht nieuws! Fidget gebruikt automatische differentiatie om normalen te berekenen, waardoor de normalen over deze grens niet in de juiste richting wijzen. In 3D, waar we normalen gebruiken voor shading, produceert dit incorrecte shading in de shingles van een blokhut.

Door afstandsvelden centraal te stellen, is het eenvoudig om dit soort problemen te diagnosticeren. Sterker nog, het suggereert een verdere verbetering van het zaagtandveld: we kunnen de gradiënt zo aanpassen dat deze overal 1 is, in plaats van dat deze opgehoopt is op de diagonalen.

Uniforme gradiënten zorgen ervoor dat diverse algoritmen beter functioneren; Fidget vereist dit niet voor de correctheid, maar het kan bijvoorbeeld de kwaliteit van de mesh verbeteren.

Experimenteel en cross-platform

Op dit moment zou het een zeer gedurfde beslissing zijn om Halfspace in een bedrijfskritische capaciteit te gebruiken. In de Fidget-documentatie staat een van de projectdoelen:

"Het vinden van de 'juiste' API's voor impliciete kernels, met de mogelijkheid om substantiële compatibiliteitsbreuken te maken."

Halfspace is vergelijkbaar; het presenteert geen API's aan eindgebruikers, maar ik oefen mijn software-architectuurvaardigheden door een substantiële cross-platform applicatie te bouwen, en ik ben bereid om agressief te itereren en dingen te breken terwijl we vorderen.

Wat betreft cross-platform: ik maak mijn leven moeilijker door zowel het web als native platforms te targeten. In een tijdperk van supply-chain attacks is het kunnen delen van een weblink – in plaats van iemand te vragen je code te compileren en uit te voeren – geweldig voor onboarding en incidenteel gebruik.

Hiervoor heb ik de "Halfspace stack" verzameld: een set bibliotheken en patronen waarmee ik een gecombineerde native + web-applicatie kan leveren met minimale pijn. De kernonderdelen zijn momenteel:

  • Rust voor de applicatie (en alle afhankelijkheden)
  • wasm-bindgen (handmatig aangeroepen) om te bouwen voor het web
  • egui voor de GUI
  • egui_dock voor de kernabstractie van vensters en tabbladen
  • wgpu voor zowel het renderen van de UI als GPU compute
  • Rhai voor scripting
  • Rayon en wasm-bindgen-rayon, gebruikt voor twee doeleinden:
  • Het versnellen van parallelle algoritmen door werk te verdelen over veel workers (typisch gebruik).
  • Een thread pool voor kortstondige achtergrondtaken (ongebruikelijk gebruik, maar noodzakelijk omdat we op het web geen threads kunnen spawnen).
  • web-time voor cross-platform tijdsondersteuning.
  • Een zelfgebouwde worker pool voor off-thread (async) GPU-rendering.

Dit hele traject verdient een eigen verslag, en ik heb klachten over elke laag van de stack, maar over het geheel genomen is het ongelooflijk dat alles gewoon werkt.

Verbeteringen aan Fidget stimuleren

Een ander doel van Halfspace is het stimuleren van verbeteringen in de Fidget-kernel door deze te gebruiken in een niet-triviale applicatie.

De grootste overwinning op dit front is het lopende werk aan fidget-wgpu. Dit werd gemotiveerd door de prestaties op het web: de native build was aangenaam snel, maar rasterisatie op de CPU was vreselijk traag ("non-interactieve snelheden") wanneer dit via een laag WebAssembly draaide.

Na veel werk aan de Fidget-zijde kunnen zowel rasterisatie als post-processing (bijv. shading) nu puur op de GPU worden uitgevoerd, zonder tussenkomst van de CPU.

Dit was zowel een prestatie- als een architecturale winst:

  1. We hebben nu native rendersnelheid op zowel native als web-targets.
  2. De rendering-logica is niet langer verspreid tussen Fidget (op de CPU) en Halfspace (een mix van CPU- en GPU-code):
  • Fidget implementeert de canonieke rendering-logica.
  • Halfspace heeft dunne shaders die een RGBA-textuur tekenen.

(De 2D-rendering-pipeline is nu ook volledig GPU-versneld, hoewel deze in Halfspace iets geavanceerdere shaders heeft).

Afronding

Halfspace bestaat! Je kunt het uitproberen, maar je zou het waarschijnlijk niet moeten gebruiken voor kritische applicaties. Als je problemen tegenkomt, open dan een issue of discussie op Github.

Zoals bij de meeste van mijn werk, ben ik van plan hieraan te blijven werken totdat ik niets meer van het project kan leren. Ook is het project, zoals gebruikelijk, open-source onder de MPLv2-licentie.