Python bindings for SDFGenFast with NumPy integration and GPU acceleration, built with nanobind.
The bindings provide a NumPy-based API for generating signed distance fields from triangle meshes.
Key features:
- NumPy array integration
- Automatic GPU acceleration (CUDA), with CPU fallback
- Low-level control (
generate_sdf) and high-level convenience functions (generate_from_file,generate_from_mesh) - The GIL is released during computation, so other Python threads keep running
- 51 tests
Changed in 2.2: nx, ny, and nz are the total grid size, including the padding cells. This matches the CLI. Before 2.2, generate_from_mesh and generate_from_file added 2 * padding on top of the requested size.
See BUILD.md for build details.
pip install .Verify:
import sdfgen
print(sdfgen.__version__)
print('GPU available:', sdfgen.is_gpu_available())Load a triangle mesh from a file.
Parameters:
filename(str): path to the mesh file (.objor.stl)
Returns:
vertices(ndarray): vertex positions, shape (N, 3), dtype float32triangles(ndarray): triangle indices, shape (M, 3), dtype uint32bounds(tuple):((min_x, min_y, min_z), (max_x, max_y, max_z))
vertices, triangles, bounds = sdfgen.load_mesh("mesh.obj")Generate a signed distance field from mesh arrays. This is the low-level entry point: you specify the grid placement yourself.
Parameters:
vertices(ndarray): vertex positions, shape (N, 3), dtype float32triangles(ndarray): triangle indices, shape (M, 3), dtype uint32origin(tuple): grid origin (x, y, z) in world spacedx(float): grid cell spacingnx, ny, nz(int): grid dimensionsexact_band(int, optional): width of the exactly-computed band in cells (default 1). A larger band computes more cells exactly and costs more time.backend(str, optional):'auto','cpu', or'gpu'(default'auto')num_threads(int, optional): CPU threads, 0 = auto-detect (default 0)
Returns:
sdf(ndarray): signed distance field, shape (nx, ny, nz), dtype float32
Distance convention: negative inside the mesh, positive outside, zero on the surface.
Backend behavior: with 'auto', the GPU is used when available; a GPU failure falls back to the CPU with a warning. With 'gpu', a GPU failure raises an exception.
sdf = sdfgen.generate_sdf(
vertices, triangles,
origin=(0, 0, 0),
dx=0.01,
nx=100, ny=100, nz=100,
)Save an SDF to the binary file format (see the main README for the format definition).
sdfgen.save_sdf("output.sdf", sdf, origin=(0, 0, 0), dx=0.01)Load an SDF from a binary file.
Returns:
sdf(ndarray): signed distance field, shape (nx, ny, nz), dtype float32origin(tuple): grid origin (x, y, z)dx(float): grid cell spacingbounds(tuple):((min_x, min_y, min_z), (max_x, max_y, max_z))
sdf, origin, dx, bounds = sdfgen.load_sdf("input.sdf")Return True when a CUDA GPU is available at run time and GPU support was compiled in.
generate_from_mesh(vertices, triangles, nx=None, ny=None, nz=None, dx=None, padding=1, exact_band=1, backend="auto", num_threads=0)
Generate an SDF from mesh arrays with automatic grid placement. The grid is centered on the mesh. Choose one sizing mode:
nxonly: proportional sizing.nyandnzare derived from the mesh aspect ratio.nx,ny,nz: explicit dimensions. The cell size is chosen so the mesh fits.dxonly: cell-size mode. The dimensions are derived from the mesh extent plus padding.
All dimensions are the total grid size, including the padding cells (changed in 2.2).
Returns:
sdf(ndarray): signed distance field, shape (nx, ny, nz), dtype float32metadata(dict): keys'origin','dx','bounds','backend'
# Proportional sizing
sdf, metadata = sdfgen.generate_from_mesh(vertices, triangles, nx=256, padding=2)
# Explicit dimensions
sdf, metadata = sdfgen.generate_from_mesh(vertices, triangles, nx=128, ny=128, nz=256)
# Cell-size mode
sdf, metadata = sdfgen.generate_from_mesh(vertices, triangles, dx=0.01, padding=2)generate_from_file(filename, nx=None, ny=None, nz=None, dx=None, padding=1, exact_band=1, backend="auto", num_threads=0)
The same as generate_from_mesh, but loads the mesh from a file first.
sdf, meta = sdfgen.generate_from_file("mesh.stl", nx=256)
sdf, meta = sdfgen.generate_from_file("mesh.stl", nx=128, ny=128, nz=256)
sdf, meta = sdfgen.generate_from_file("mesh.obj", dx=0.01, padding=2)import sdfgen
import numpy as np
vertices, triangles, bounds = sdfgen.load_mesh("mesh.obj")
sdf = sdfgen.generate_sdf(
vertices, triangles,
origin=bounds[0],
dx=0.01,
nx=256, ny=256, nz=256,
)
print(f"SDF shape: {sdf.shape}")
print(f"Inside cells: {np.sum(sdf < 0)}")import sdfgen
import numpy as np
import time
vertices, triangles, bounds = sdfgen.load_mesh("mesh.stl")
args = dict(origin=bounds[0], dx=0.02, nx=128, ny=128, nz=128)
start = time.time()
sdf_cpu = sdfgen.generate_sdf(vertices, triangles, backend="cpu", **args)
cpu_time = time.time() - start
if sdfgen.is_gpu_available():
start = time.time()
sdf_gpu = sdfgen.generate_sdf(vertices, triangles, backend="gpu", **args)
gpu_time = time.time() - start
print(f"CPU: {cpu_time:.3f}s, GPU: {gpu_time:.3f}s")
print(f"Max difference: {np.max(np.abs(sdf_cpu - sdf_gpu)):.6f}")import sdfgen
import numpy as np
vertices = np.array([
[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5],
[0.5, 0.5, -0.5], [-0.5, 0.5, -0.5],
[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5],
[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5],
], dtype=np.float32)
triangles = np.array([
[0, 1, 2], [0, 2, 3], # front
[4, 6, 5], [4, 7, 6], # back
[0, 3, 7], [0, 7, 4], # left
[1, 5, 6], [1, 6, 2], # right
[0, 4, 5], [0, 5, 1], # bottom
[3, 2, 6], [3, 6, 7], # top
], dtype=np.uint32)
sdf = sdfgen.generate_sdf(
vertices, triangles,
origin=(-1, -1, -1),
dx=0.05,
nx=40, ny=40, nz=40,
)- Measured performance for both backends is in the main README, Appendix A. On the test system (RTX 4090, CUDA 13.3), the GPU was 2.4x to 7.1x faster than the best multi-threaded CPU time, and the advantage grows with grid size.
- Host memory: the SDF array is
4 * nx * ny * nzbytes (float32). - GPU memory: the kernel allocates about 20 bytes per grid cell (distance/triangle pairs, intersection counts, and two sweep buffers), plus the mesh data.
The extension needs the CUDA runtime DLL. pip install . bundles it into the package. For other setups, add the CUDA bin directory to PATH (CUDA 13 keeps the DLLs in bin\x64).
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH- Check the driver:
nvidia-smi. - Check that the build found CUDA: rebuild and read the CMake output.
- Check that the CUDA runtime is loadable (see the DLL issues above).
Run pip install . from the project root.
pip install pytest
pytest python/tests -vThe suite has 51 tests; see the main README, Appendix B for the class-by-class breakdown.
python/
├── sdfgen.py # high-level API (installed as sdfgen/__init__.py)
├── sdfgen_py.cpp # nanobind extension source
├── tests/ # test suite (51 tests)
└── README.md # this file
Requires: Python 3.8+, NumPy 1.20+. Version: 2.2.0 (see CHANGELOG.md).