Inspiration for SDF3UI
Introduction: Life before sdf3ui
sdfx
is an amazing library. We've used it for generating 3D printable parts for work at an aerospace firm. The parts We've printed have been used to support PCBs, make DIY ATX power supplies and even for pipe-fitting designs for industrial processes. It is an exciting time to be alive in the maker industry. The process for generating these shapes is quite simple:
- Create a .go file and run
go mod init project
go get github.com/deadsy/sdfx
and start coding your shape and have it save the shape to an STL file- Compile and run the program and visualize the resulting shape it in your program of choice.
- Print shape when happy with result.
We were using CURA by Ultimaker to visualize changes, it had a handy "reload shape" button that showed up when changes were detected in the shape. This was fine for the most part, but it was a hassle to switch between windows, having to refocus the editor window before resuming programming. CURA does not have an autoreload setting. Shape generation also had to be manually run with go run
. We knew it could be done better.
Existing tools
There exists a tool that seeks to solve this problem called https://github.com/Yeicor/sdfx-ui. It autoreloads on code edit, so no running the program nor hitting "reload file"! This sounds great on paper but truth was that sdfx-ui used CPU rendering of 3D shapes, so it could not do fluid user-interactive rotations or zoom. It felt like much was being lost coming from CURA, so the idea of using a GPU library to implement an auto-loading UI was considered.
GPU rendering library of choice
The choice of the GPU facing library was easy enough. For the last year Patricio has been working on Go WASM bindings for the excellent three.js library. Patricio has found it works consistently on every device thrown at it, and fast. Even without a dedicated GPU one regularly gets great frame rates.
sdfx rewrite
So without further due we set out to read the sdfx
code to find out how to implement the renderer-browser interface. Speaking as Patricio: Boy did I not know what I was getting myself into. You can find the dramatized version of what happened after I started reading the sdfx
code here. Long story short, I rewrote sdfx
because of creative differences with Jason, the lead author of sdfx. The library I was to use now in place of sdfx
was sdf
.
What sdf3ui does
You run it via command line with the name of the file with the Go code that defines the 3D shape.
When sdf3ui
is running it
- Spins up an http server on http://[::]:8080 (equivalent of
localhost
). - It
go run
's the shape file and stores the rendered shape. - When one navigates to said URL with a browser one is greeted with an UI with a 3D visualization of the designed 3D shape. This visualization is interacive friendly (use mouse right/left click and drag, mouse scroll, etc.)
- It registers a operating system listener for changes on the file. If file is modified and saved the file is re-run, the new shape saved and a status message is broadcast to all browsers viewing the shape is sent out. Browsers that receive this broadcast will automatically fetch the new shape and display the changes as soon as possible.
This means that one can be editing the shape on a computer while your phone or second monitor displays the latest and updated shape. This makes working with sdf
a breeze. It also means many people can visualize the latest shape iteration at the same time on different computers. Talk about maker collaboration.
How we built it
The application consists of two programs running simulataneously:
- The CLI is the desktop application that generates the shape data from your
.go
project and serves it on a TCP server (websocket+http). - The App, or colloquially named browser, is a WASM/http/js program that runs in your web browser. It requests the shape data via websocket and http requests and displays the 3D shape to the user.
Infrastructure
A lot of time was spent on building the infrastructure for this project (a.k.a. yak shaving). Over a week (April 18-29) was spent just rewriting sdfx
so that it was easier to build on top of and also much faster (performance). Tweaks to the new sdf
library were prevalent during the whole project.
Designing the CLI-Browser interface
Some high level concepts of designing the sdf3ui
tool will be detailed here:
Shape lifetime on the CLI:
- The first shape is rendered on running
sdf3ui
with the.go
project as is and stored in memory. - The shape is rendered upon changes to the
.go
project and stored as a ShapeModel in memory. Any previous existing ShapeModel is invalidated and discarded (becomes "stale") via use ofcontext.Context
. - Shape is stored and used when requested until invalidated by a new rendered shape.
Shape status protocol. Necessary for letting browsers know exactly when there is a change to the shape.
- On starting the CLI a websocket listener is opened with the sdf3ui subprotocol.
- A browser will listen idly on this websocket for messages.
- When the CLI renders a new shape a message will be sent over this websocket to all listeners (browsers).
- Browsers will receive the status message and can judge whether to request new ShapeModel based on the Sequence number. For now the browser updates the ShapeModel regardless. The ShapeModel update is a simple
http.Get
endpoint which streams the ShapeModel as anencoding/gob
payload. - Browsers will continue listening for new updates idly and repeat steps 2-4.
Browser shape visualization steps:
- Upon updating the ShapeModel the browser makes sure the Sequence number has changed and then must process the ShapeModel data into data suitable for WebGL rendering (float64->float32 conversion).
- After the data conversion and 3D model generation the browser displays the updated shape.
Frontend framework
Vecty was used as the frontend framework for it's simple and idiomatic API. A big bonus is the absence of tooling necessary to work with it- it's just pure Go.
Vecty manages the HTML rendering and event handlers to Go functions. The program is in the app
folder under sdf3ui
.
Thanks
Special thanks to Jason who wrote the sdfx
library, which is still something that blows us away in scope and execution to this day.
Another special thanks to the reddit community, whose abrasive suggestions led us to adopt a better error handling methodology.
Challenges we ran into
Faceoffs with sdfx maintainer
It started as a simple demonstration draft PR with suggested changes to the unfortunate design of the sdfx renderer interface. Patricio feels that the suggestions were not met with an open mind nor a desire to improve the library where it mattered.
Silly event-loop bugs
2 false positive bugs were filed, one in golang/go and another in nhooyr/websocket, due to lack of understanding of how the javascript event loop works.
Downloading data from browser
To this day there is no Go WASM reliable way of downloading data as a file without using an existing javascript library. A stack overflow inspired implementation by Patricio has bugs and is known to corrupt data for large files. Because of this there is no Download Shape
button on the sdf3ui user interface, instead the shape is saved in the folder of whomever is running the CLI application.
Accomplishments that we're proud of
Fast & Furious UX
The user experience is much better than in the previously mentioned existing tools. Once the renderer is up and running it usually takes 1-2 seconds between the time the user hits the save
button on the editor and the moment the new 3D shape is displayed on screen. The visualization can be rotated, zoomed and even saved to file with a given name.
This is a game-changer for developing with sdfx
or sdf
.
Rethinking of sdfx from the ground up
The sdfx
library was overhauled and little was left unchanged. The comparison speaks for itself:
_Comparison sdfx vs. sdf _
Advantages of deadsy/sdfx:
- Widely used
- More helper functions
- Working 2D renderer
Advantages of soypat/sdf:
- Very fast rendering
deadsy/sdfx
is over 2 times slower and has ~5 times more allocations.
- Minimal and idiomatic API
Renderer
interface is dead-simple, idiomatic Go and not limited to SDFs- deadsy/sdfx
Renderer3
interface has filledrender
package with technical debt.
- deadsy/sdfx
- Has
SDFUnion
andSDFDiff
interfaces for blending shapes easily - No
nil
valued SDFs- deadsy/sdfx internally makes use of
nil
SDFs as "empty" objects. This can later cause panics during rendering well after the point of failure causing hard to debug issues.
- deadsy/sdfx internally makes use of
- Well defined package organization.
- deadsy/sdfx dumps helper and utility functions in
sdf
- deadsy/sdfx dumps helper and utility functions in
- End-to-end tested.
- Ensures functioning renderer and SDF functions using image comparison preventing accidental changes.
- Error-free API under
must3
andmust2
packages for makers.- For simple projects these packages allow for streamlined error handling process using
panic
instead of returned errors. - deadsy/sdfx only allows for Go-style error handling like the
form3
andform2
packages.
- For simple projects these packages allow for streamlined error handling process using
- Sound use of
math
package for best precision and overflow prevention.math.Hypot
used for all length calculations.deadsy/sdfx
does not usemath.Hypot
.spatial
types from gonum library with correct Triangle degeneracy calculation.deadsy/sdfx
's Degenerate calculation is incorrect.
- Uses gonum's
spatial
packagesdfx
has own vector types with methods which hurt code legibility
Contributions to gonum
Due to the user/programmer close interaction with the spatial types (i.e. Triangles, Vec, Boxes) these had to have special treatment to ensure the best developer experience.
Using gonum for all spatial types is high priority since it gives the module
- Stability: gonum abides by the Go compatibility promise.
- Correctness: gonum favors correct code and is held in high regard in the Go community.
- Familiarity: gonum is widely used.
Gonum issues related to sdf
:
What we learned
Conflict resolution via forks
Forking projects is a normal endeavour. Sometimes its necessary for code to evolve into something new and maybe even better. The prospect of joining the forks in the future is always an option.
Websockets are easier than you think
Who would have thunk websockets were such a pleasure to use in Go! The way we've been using them in sdf3ui
is to synchronize http requests for the new shape, much like a channel synchronizes goroutines in Go!
What's next for SDF 3D shape visualizer
During the project we were approached by two people interested in the sdf
project (not the UI per se).
Ultimate UX experience
Gerard wanted to develop an desktop application that streamed SDF shapes from a centralized server in such a way that the changes happened in the server and should be propagated to hundreds of users that could be editing/viewing the shape or assembly of shapes. Together we talked about the sdf
design decisions and direction. Gerard was kind enough to instruct us in the use of websockets and gave us future ideas for streaming "diffing" changes to shapes. This could provide the ultimate user experience by reducing (to the order of the millisecond) the time between a change in the program and the time it takes for that change to be visible in the browser. Right now the main source of delay is the time it takes to stream all the model triangles to the browser.
This change requires a hefty amount of engineering and the sdf3ui
authors would be more comfortable using an existing library with this functionality.
Gerard was also keen on telling us he wanted to start learning aframe to implement a virtual reality CAD package using sdf
sometime in the future.
Go beyond the server room
So far the community response to the sdf
package has been overwhelmingly positive (with the exception of Reddit). We look forward to seeing more and more people use it to design their 3D printed parts and more! This is where sdf3ui
might provide an important weight in the decision matrix to use Go. sdf3ui
has the potential to make Go a no-brainer choice for makers wanting to design 3D printed parts. Right now it needs users who can bring needs of those other than the author of sdf3ui
and formulate them as issues and pull requests.
Link to SDF3UI.
Built With
- go
- sdf
- signed-distance-function
- three.js
- vecty
- wasm
- websockets
Log in or sign up for Devpost to join the conversation.