sdf (originally sdfx)
A rewrite of the original CAD package sdfx
for generating 2D and 3D geometry using Go. See Why was this package rewritten?
- Objects are modelled with 2d and 3d signed distance functions (SDFs).
- Objects are defined with Go code.
- Objects are rendered to an STL file to be viewed and/or 3d printed.
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
Roadmap
Remove superfluous outward facing API insdf
andrender
which clutters namespace, likeCapsule3D
and triangle rendering functions.Add examples.Remove returned errors from basicKeep adding shapes!sdf
functions likeCylinder3D
,Box3D
,Sphere3D
and similar (see Questionable API design.Perform a rewrite of 2D rendering functions and data structures likesdf.V2
->r2.Vec
among others.- Add a 2D renderer and it’s respective
Renderer2
interface. - Make 3D renderer multicore.
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:
Performance
sdfx is needlessly slow. 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.
Questionable API design
- deadsy/sdfx#48 Vector API redesign
- deadsy/sdfx#35 Better STL save functions.
- deadsy/sdfx#50 Removing returned errors from shape generation functions
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:
-
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?
-
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
-
Using a channel to send individual triangles is probably a bottleneck.
-
I would liken
meshCells
to an implementation detail of the renderer used. This can be passed as an argument when instantiating the renderer used. -
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.
That said there are some minor changes I’d also like to make. Error handling in Go is already one of the major pain points, and there is no reason to bring it to sdfx
in full force for simple shape generation. See the following code from sdfx
:
// Cylinder3D return an SDF3 for a cylinder (rounded edges with round > 0).
func Cylinder3D(height, radius, round float64) (SDF3, error) {
if radius <= 0 {
return nil, ErrMsg("radius <= 0")
}
if round < 0 {
return nil, ErrMsg("round < 0")
}
if round > radius {
return nil, ErrMsg("round > radius")
}
if height < 2.0*round {
return nil, ErrMsg("height < 2 * round")
}
//...
An error on a function like Cylinder3D
can only be handled one way really: correcting the argument to it in the source code as one generates the shape! This is even implied with the implementation of the ErrMsg
function: it includes the line number of the function that yielded the error. panic
already does that and saves us having to formally handle the error message.
The sdfx
author claims:
I don’t want to write a fragile library that crashes with invalid user input, I want it to return an error with some data telling them exactly what their problem is. The user then gets to work out how they want to treat that error, rather than the library causing a panic.
This is contrasted by the fact the many of the SDF manipulation functions of sdfx
will return a nil SDF3
or SDF2
interface and no error when receiving invalid inputs. This avoids a panic on the sdfx
library side and instead passes a ticking timebomb to the user who’s program will panic the instant the returned value is used anywhere. I do not need to explain why this particular design decision is objectively bad.
sdf
and sdfx
consolidation
None planned.
My understanding is the sdfx
author has a very different design goal to what I envision. See the bullet-list of issues at the start of Questionable API design.