Library / SDK
fogleman/sdf avatar
fogleman/sdf

fogleman/sdf: generating 3D meshes from signed distance functions in Python

Simple SDF mesh generation in Python

2,011 stars171 forksPythonMIT

At a glance

What is it?
The sdf library generates binary STL and other 3D mesh formats from signed distance functions using Marching Cubes, numpy batch evaluation and multiple threads. Installing requires cloning the repository and running pip install -e ., as the package is not published on PyPI.
Who is it for?
fogleman/sdf is a good fit for a Python developer who wants to generate STL files by writing mathematical shape expressions rather than using a CAD tool. Before adopting it, check whether any SDF in your pipeline uses non-uniform scaling: that is the documented cause of holes in the output mesh, and the fix is setting sparse=False in the save call.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

CSG operations through Python operators instead of manual mesh editing

Signed distance functions offer a clean path to boolean shape operations. A signed distance function returns, for any point in 3D space, the distance to the nearest surface of a shape: negative inside, positive outside. Boolean operations on shapes reduce to arithmetic on those values, and that arithmetic is exact where the input SDFs are exact.

The canonical CSG (Constructive Solid Geometry) example in the README defines a sphere intersected with a box, then subtracts three oriented cylinders:

python
from sdf import *

f = sphere(1) & box(1.5)

c = cylinder(0.5)
f -= c.orient(X) | c.orient(Y) | c.orient(Z)

f.save('out.stl')

The README notes the use of operators for union, intersection and difference, producing a model that can be 3D printed or used in a 3D application. Six lines of Python cover the full shape definition. The examples directory includes gearlike.py, knurling.py, blobby.py and weave.py, covering both mechanical and organic shapes.

The audience is Python developers who prefer writing code over using a GUI modeler, particularly for parametric or programmatic shapes where a dimension or count needs to be a variable rather than a hardcoded vertex position. A sphere radius as a function parameter, a cylinder count that changes at runtime, or a bounding box that adapts to input data are all natural in this approach. In a traditional CAD modeler, making those values variable requires the modeler's own scripting layer; here they are plain Python variables.

Marching Cubes on numpy batches, evaluated across multiple threads

Mesh generation runs the Marching Cubes algorithm over a regular 3D grid of sampled SDF values. Marching Cubes finds where the field crosses zero and constructs triangles at each crossing, producing a surface mesh that follows the shape boundary.

Running this naively in Python over millions of grid points would be too slow for practical use. Evaluation of the SDF happens over entire batches of points at once using numpy, which shifts the arithmetic into numpy's compiled C layer. Multiple threads process batches in parallel, defaulting to multiprocessing.cpu_count() threads. The README calls the result "surprisingly fast (for marching cubes)" before the text ends, and the qualifier matters: Marching Cubes at high resolutions is computationally expensive regardless of implementation.

f.generate() returns a flat array of floats. The count is three times the number of triangles, since each triangle contributes three vertices as consecutive entries. f.save() writes those points to a binary STL by default, or to another format if the file extension matches one meshio supports. Separating generation from saving is useful when you want to inspect or process the point data before writing to disk:

python
points = f.generate() # takes the same optional arguments as `save`
print(len(points)) # print number of points (3x the number of triangles)
print(points[:3]) # print the vertices of the first triangle

Installing from source and confirming the first STL with example.py

The package is not on PyPI. Cloning and installing with pip uses an editable install:

bash
git clone https://github.com/fogleman/sdf.git
cd sdf
virtualenv env
. env/bin/activate
pip install -e .

The editable install flag means changes to the cloned source take effect immediately without reinstalling. setup.py handles installing all six dependencies during this step. Confirm the install produced a working environment:

bash
python examples/example.py

This should write a file named out.stl to the current directory. An alternative the README notes: if scripts always run from the sdf repository root, the install step can be skipped and sdf imported directly from the local directory.

setup.py lists the six required packages as matplotlib, meshio, numpy, scikit-image>=0.17, scipy and Pillow. The README's requirements section lists the same packages but omits the version floor on scikit-image. A developer writing a requirements.txt by hand from the README would not see the 0.17 requirement; pip enforces it from setup.py at install time. Beyond example.py, the examples directory holds gearlike.py, knurling.py, blobby.py, weave.py, image.py, text.py and others; the README shows rendered screenshots for gearlike, knurling, blobby and weave, while image.py and text.py have no accompanying documentation in the README.

Step, samples and bounds: the three parameters that set resolution and scope

Resolution defaults to approximately four million sample points. Two parameters override it. step sets the voxel size directly:

python
f.save('out.stl', step=0.01)
f.save('out.stl', step=(0.01, 0.02, 0.03))  # non-uniform resolution

samples sets an approximate point count:

python
f.save('out.stl', samples=2**24)  # sample about 16M points

That default of 2 to the 22nd power is documented in the README. A practical tip from the README: use the default while iterating on the shape and increase resolution for the final output, since generation time scales with the number of sampled points.

Bounds are estimated automatically from the SDF. When the SDF is inexact (for example, because of non-uniform scaling), the auto-estimate can be wrong. Specifying bounds manually bypasses the estimation entirely:

python
f.save('out.stl', bounds=((-1, -1, -1), (1, 1, 1)))

The README identifies non-uniform scaling as the specific problematic case for both the bounds estimation and the sparse sampling optimization. Both break for the same reason: inexact SDF values no longer represent the true minimum distance to the surface, which both algorithms assume.

Sparse sampling skips empty batches, but holes appear for inexact SDFs

Each evaluation processes batches of 32^3 = 32768 points by default. An optimization skips batches where SDF values indicate the batch is entirely away from the surface. That sparse sampling reduces computation for shapes occupying a small fraction of the bounding box.

Inexact SDFs break this optimization. Non-uniform scaling produces SDF values that no longer correctly reflect the true distance to the surface. Batches near the mesh surface may be classified as distant and skipped, leaving holes in the output. Disabling sparse sampling forces every batch to be evaluated:

python
f.save('out.stl', sparse=False)  # force all batches to be completely sampled

Batch size and thread count are also configurable for tuning:

python
f.save('out.stl', batch_size=64) # instead of 32
f.save('out.stl', workers=1) # only use one worker thread

Lowering workers reduces CPU load for lower-priority background jobs. Batch size affects memory per worker more than total throughput for most shapes. For shapes using only standard primitives and exact boolean operations, the defaults are safe and the sparse sampling optimization works as intended. The batch-skip logic checks each batch's minimum SDF value before deciding whether to skip it, and that check is only reliable when the SDF returns true minimum distances.

OpenVDB support requires a manual CMake build and dependencies the README does not list

Using a mesh as an SDF input requires OpenVDB and its Python module. The README states this cannot easily be installed via pip. Building from source requires cloning the AcademySoftwareFoundation/openvdb repository and running CMake:

bash
git clone https://github.com/AcademySoftwareFoundation/openvdb.git
cd openvdb
mkdir build
cd build
cmake -D OPENVDB_BUILD_PYTHON_MODULE=ON -D USE_NUMPY=ON ..
make -j8
cp openvdb/openvdb/python/pyopenvdb.* `python -c 'import site; print(site.getsitepackages()[0])'`

The README gives these steps but does not list OpenVDB's own build dependencies. OpenVDB requires additional libraries beyond the Python module itself, and the build environment setup depends on the platform. For anyone who only needs standard shape primitives and boolean operations, OpenVDB is fully optional: the core library works without it and the build above can be skipped.

Once a mesh is ready, a viewer is needed to inspect it. MeshLab is a cross-platform option that handles most STL files. A separate viewer called meshview, written by the same author, installs via Go:

bash
$ brew install go glfw # on macOS with homebrew
$ go get -u github.com/fogleman/meshview/cmd/meshview

After installation, any STL file opens from the command line with `$ meshview your-mesh.stl`. On macOS, pressing spacebar on an STL file in Finder opens Quick Look without installing anything additional, which is convenient for a quick check after running example.py.

scikit-image>=0.17 in setup.py but not in the README, and the Alpha status

setup.py declares version 0.1 under MIT license with Development Status :: 3 - Alpha; no GitHub releases are listed in the repository.

One discrepancy between setup.py and the README: install_requires in setup.py pins scikit-image at >=0.17, while the README's requirements section lists scikit-image with no version qualifier. A developer assembling a requirements.txt by reading only the README would miss the 0.17 floor. pip enforces the setup.py constraint at install time, so the functional behavior is correct, but the documentation gap matters for anyone tracking dependencies manually.

File format support beyond binary STL comes from meshio. According to the README, meshio adds support for over 20 3D file formats including OBJ, PLY and VTK, with the output format determined by the file extension passed to f.save(). The library also provides f.show_slice() for visualizing a 2D cross-section of the SDF using matplotlib, which is useful for debugging a shape that is producing unexpected mesh output:

python
f.show_slice(z=0)
f.show_slice(z=0, abs=True)  # show abs(f)

One practical consequence of that dependency: matplotlib does not load at import time. Any usage that never calls f.show_slice() can omit matplotlib from the virtualenv entirely. Debugging a shape that produces unexpected mesh output by slicing at successive Z coordinates is a concrete use case for the function: a visible discontinuity in the 2D plot usually points to where the SDF's sign changes unexpectedly.

Editorial conclusion

fogleman/sdf is a good fit for a Python developer who wants to generate STL files by writing mathematical shape expressions rather than using a CAD tool. Before adopting it, check whether any SDF in your pipeline uses non-uniform scaling: that is the documented cause of holes in the output mesh, and the fix is setting sparse=False in the save call. If you need mesh-to-SDF conversion, plan for the OpenVDB build, which the README describes as not easily installable via pip and which requires CMake without a dependency list. setup.py classifies the project as Alpha, which is its own declaration that the API can change.

Frequently asked questions

What is fogleman/sdf?

fogleman/sdf is a Python library that generates 3D mesh files from signed distance functions using the Marching Cubes algorithm. It supports CSG operations (union, intersection, difference) through Python operators and outputs binary STL natively, with additional formats available via meshio.

How do I install fogleman/sdf?

The library is not on PyPI. Clone the repository, create a virtualenv, activate it, then run pip install -e . to install with all dependencies. Confirm the install with python examples/example.py, which should generate out.stl.

Why does my sdf mesh have holes?

Holes are caused by inexact SDFs, which the README identifies as occurring with non-uniform scaling. Sparse sampling incorrectly skips batches near the surface when SDF values are inexact. Setting sparse=False in the save call forces all batches to be evaluated and removes the holes.

What file formats does the sdf library support?

Binary STL is the native output format. For other formats, the library uses meshio, which the README says supports over 20 3D formats including OBJ, PLY and VTK, determined by the file extension passed to f.save().

Does fogleman/sdf support OpenVDB?

Yes, but using a mesh as an SDF input requires OpenVDB, which the README says cannot easily be installed via pip. The README gives CMake build steps for compiling OpenVDB from the AcademySoftwareFoundation repository, without listing OpenVDB's own build dependencies.

Official sources

  1. fogleman/sdf on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fogleman-sdf.svg)](https://hysenlabs.com/projects/fogleman-sdf)