/sdf

A Go library for signed distance function shape generation.

Primary LanguageGoMIT LicenseMIT

Go Report Card GoDoc

sdf (originally sdfx)

A rewrite of the original CAD package sdfx for generating 2D and 3D geometry using Go.

Highlights

  • GUI with real-time rendering using sdf3ui (or SDF Viewer).
  • 3d and 2d objects modelled with signed distance functions (SDFs).
  • Minimal and idiomatic API.
  • Render objects as triangles or save to STL, 3MF(experimental) file format.
  • End-to-end testing using image comparison.
  • must and form packages provide panicking and normal error handling basic shape generation APIs for different scenarios.
  • Dead-simple, single method Renderer interface.
  • Import mesh files: Edit STL and 3MF files as if they were native SDFs using sdfexp.ImportModel
  • Tetrahedron mesher: Measure volume or create physics models of your shapes using sdfexp.UniformTetrahedronMesh. Experimental feature. Example result image.

Examples

For real-world examples with images see examples directory README.

See images of rendered shapes in render/testdata.

Here is a rendered bolt from one of the unit tests under form3_test.go renderedBolt

Roadmap

  • Clean up thread API mess
  • Add a 2D renderer and it's respective Renderer2 interface.
  • Make 3D renderer multicore

Comparison

deadsy/sdfx

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 filled render package with technical debt.
  • Has SDFUnion and SDFDiff interfaces for blending shapes easily
    • MinPoly redesign to allow for n-degree polynomials. Also returns a sensible "undefined output" MinFunc before dividing by zero.
  • 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.
  • Well defined package organization.
    • deadsy/sdfx dumps helper and utility functions in sdf
  • End-to-end tested.
    • Ensures functioning renderer and SDF functions using image comparison preventing accidental changes.
  • Error-free API under must3 and must2 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 and form2 packages.
  • Sound use of math package for best precision and overflow prevention.
    • math.Hypot used for all length calculations. deadsy/sdfx does not use math.Hypot.
  • Uses gonum's spatial package
    • sdfx has own vector types with methods which hurt code legibility
    • spatial types from gonum library with correct Triangle degeneracy calculation. deadsy/sdfx's Degenerate calculation is incorrect.
  • Idiomatic thread package. Define arbitrary threads with ease using Threader interface.
    • deadsy/sdfx defines threads with strings i.e. "M16x2". sdf Defines threads with types corresponding to standards. i.e: thread.ISO{D:16, P:2}, which defines an M16x2 ISO thread.

Contributing

See CONTRIBUTING.

Why was sdfx rewritten?

The original sdfx package is amazing. I thank deadsy for putting all that great work into making an amazing tool I use daily. That said, there are some things that were not compatible with my needs:

Rendering speed

Here is a benchmark rendering a threaded bolt:

$ go test -benchmem -run=^$ -bench ^(BenchmarkSDFXBolt|BenchmarkBolt)$ ./render
goos: linux
goarch: amd64
pkg: github.com/soypat/sdf/render
cpu: AMD Ryzen 5 3400G with Radeon Vega Graphics    
BenchmarkSDFXBolt-8   	       6	 196941244 ns/op	14700786 B/op	   98261 allocs/op
BenchmarkBolt-8       	      13	  87547265 ns/op	18136785 B/op	   20754 allocs/op
PASS
ok  	github.com/soypat/sdf/render	4.390s

BenchmarkBolt-8 is this implementation of Octree. BenchmarkSDFXBolt-8 is the sdfx implementation of said algorithm.

Unwieldy API design

The vector math functions are methods which yield hard to follow operations. i.e:

return bb.Min.Add(bb.Size().Mul(i.ToV3().DivScalar(float64(node.meshSize)).
    Div(node.cellCounts.ToV3().DivScalar(float64(node.meshSize))))) // actual code from original sdfx.

A more pressing issue was the Renderer3 interface definition method, Render

type Renderer3 interface {
    // ...
    Render(s sdf.SDF3, meshCells int, output chan<- *Triangle3)
}

This presented a few problems:

  1. Raises many questions about usage of the function Render- who closes the channel? Does this function block? Do I have to call it as a goroutine?

  2. To implement a renderer one needs to bake in concurrency which is a hard thing to get right from the start. This also means all rendering code besides having the responsibility of computing geometry, it also has to handle concurrency features of the language. This leads to rendering functions with dual responsibility- compute geometry and also handle the multi-core aspect of the computation making code harder to maintain in the long run

  3. Using a channel to send individual triangles is probably a bottleneck.

  4. I would liken meshCells to an implementation detail of the renderer used. This can be passed as an argument when instantiating the renderer used.

  5. Who's to say we have to limit ourselves to signed distance functions? With the new proposed Renderer interface this is no longer the case.

sdf and sdfx consolidation

None planned.

Logo work

Gopher rendition by Juliette Whittingslow.
Gopher design authored by Renée French is licensed by the Creative Commons Attribution 3.0 licensed.