Fast, end-to-end differentiable generation of Signed Distance Functions (SDF) for geometrical meshes and domains. Built specifically for boundary immersion methods and computational fluid dynamics (CFD) on cartesian grids, as well as geometric deep learning (GDL) and volumetric neural operator applications.
The SDF computation runs on PyTorch and is device-agnostic (CUDA / Apple MPS / CPU). For large domains, uniform grids often waste resolution in far-field regions. xSDF supports non-uniform grid generation via geometric stretching, concentrating cells near the geometry while keeping the far-field coarse.
xSDF is very fast: fully vectorized, with accelerated methods for both query and signing. This code features:
- GPU acceleration via PyTorch (CUDA, Apple MPS, or CPU).
- Linear Bounding Volume Hierarchy (LBVH) — device-parallel BVH with Morton-sort + longest-common-prefix construction, built on
device(Karras, 2012). - Greedy-leaf warm-start + batched-BFS LBVH traversal for exact unsigned distance. Kernel launches scale with tree depth, not per-query path length; chunked for memory safety.
- Hierarchical Fast Winding Number (FWN) for fast sign assignment (Barill et al., 2018).
- Gradient-consistent flood fill — the FWN pass only runs where needed (narrow band + unresolved voxels), not on the entire grid (large-domain accelerator).
- Geometric grid stretching: uniform, center-point single-stretch, or piecewise multi-segment per-axis.
- HDF5 output (
.h5) with grid metadata (levelset,x/y/z_coords,origin,grid_size,non_uniform_grid). - Differentiable point-query API (
compute_sdf,compute_grad,build_bvh) — end-to-end gradients through both query points and mesh vertices, no grid required; pre-build the LBVH once for repeated queries on the same mesh.
One-shot SDF generation on the Stanford bunny (70,372 tris; 129×128×106 = 1,750,272-voxel grid; Apple M4), including the BVH build — xSDF on MPS vs the trimesh backend, 100% sign agreement:
SDF accuracy evaluation on the bunny showing a mid-plane slice (z=0):
The hot point-triangle distance kernel is fused with torch.compile on first call (uncompiled fallback where unavailable). tests/bench_stanford.py is the offline regression gate; --full runs the live trimesh head-to-head plus the Stanford dragon.
Requires Python ≥ 3.9. From the repo root:
pip install -e . # core: import + point-query API (numpy, torch, trimesh, h5py, matplotlib)
pip install -e ".[viz]" # + pyvista; required to run the xSDF.py CLI (grid preview / visualization)The point-query API is then importable from anywhere:
from xsdf import compute_sdf, compute_grad, build_bvhcompute_sdf / compute_grad evaluate the SDF and its gradient at arbitrary query points, no grid required:
from xsdf import compute_sdf, compute_grad
# V: (Nv, 3) float F: (Nf, 3) int points: (Nq, 3) float (np or torch)
sdf = compute_sdf(V, F, points, device='mps') # (Nq,) float
grad = compute_grad(V, F, points, device='mps') # (Nq, 3) float, |grad| ≈ 1| arg | type | default | meaning |
|---|---|---|---|
V |
(Nv,3) float |
— | mesh vertices (np or torch) |
F |
(Nf,3) int |
— | triangles |
points |
(Nq,3) float |
— | query points |
device |
str / torch.device | points.device if tensor, else cuda > mps > cpu |
compute device |
fwn_beta |
float | 2.0 |
Barill admissibility ratio (~4 digits FWN accuracy) |
bvh |
lbvh.LBVH |
None |
pre-built LBVH from build_bvh(V, F); skips the per-call build |
The pipeline is end-to-end differentiable: the autograd graph is preserved through both points and the mesh vertices V:
V = torch.tensor(V_np, requires_grad=True)
sdf = compute_sdf(V, F, points)
loss = (sdf - target).pow(2).mean()
loss.backward() This repo. has been optimized on Apple hardware for Apple MPS, large point batches are evaluated in sub-batches auto-sized from the device's recommended working-set memory; xsdf.set_mps_query_chunk(n) overrides the size (trades memory for fewer per-batch launches). For CPU & cuda, this is not used.
- Configure a case in
sdf_config.json(or copy one fromexamples/). - Run the generator:
python xSDF.py # use sdf_config.json
python xSDF.py my_config.json # use a custom config
python xSDF.py examples/sdf_config_bunny.jsonThe repo ships a ready-to-run Ahmed body example. Generate an SDF from examples/ahmed_1.stl with one command:
python xSDF.py examples/sdf_config_ahmed_piecewise.jsonThis loads the STL, builds a piecewise-stretched grid clustered around the body, previews the grid (close the window to proceed), and writes the SDF to an HDF5 file.
The config (examples/sdf_config_ahmed_piecewise.json) is the simplest way to get started: edit the domain bounds, the STL path, and the stretch parameters to fit your geometry. You will be prompted with a visual of the geometry in the defined domain:
After computation, the complete SDF is shown visually:
A full xSDF config is a JSON document with five top-level sections: output, domain, geometry, grid, and backend. The Ahmed body example (examples/sdf_config_ahmed_piecewise.json) is annotated below as a reference.
"output": {
"save_name": "AHMED_PWGrid_CS002-STRCH105.h5",
"visualize": true
}| Field | Description |
|---|---|
save_name |
HDF5 file written by xSDF. Contains levelset, x_coords, y_coords, z_coords, origin, grid_size, and a non_uniform_grid attribute. |
visualize |
If true, post-run plotting routines are called on the saved H5 (slices through the SDF, isosurface, etc.). Set false for headless / batch runs. |
"domain": {
"bounds": {
"x": [-2.0, 2.5],
"y": [-1.0, 1.0],
"z": [ 0.0, 1.0]
}
}The domain block defines the cartesian bounding box that the SDF will be evaluated over. Bounds are in the same length units as the geometry. The first/last face position on each axis is snapped to these exact bounds, regardless of the stretching method.
"geometry": {
"type": "stl",
"path": "examples/ahmed_1.stl",
"transformations": {
"scale": 1.0,
"translate": [0.0, 0.0, 0.0],
"rotate": [0.0, 0.0, 0.0]
}
}| Field | Description |
|---|---|
type |
"stl" to load a mesh from disk (handles .stl, .ply, .obj, etc. via trimesh.load), or "cube" / "cylinder" for a built-in primitive (handy for sanity checks without an STL). |
path |
Path to the mesh file — only used when type = "stl". |
transformations.scale |
Uniform scale factor applied to the mesh vertices before placement. Use this to convert mm → m (e.g. 0.001) or to upscale a unit-cube STL. |
transformations.translate |
[tx, ty, tz] offset applied after scaling. Use this to position the geometry within the domain.bounds block. |
transformations.rotate |
[rx, ry, rz] Euler rotation angles in degrees, applied via trimesh.transformations.euler_matrix. Use this to set angle of attack, sideslip, or yaw. |
The transformation order is: scale → rotate → translate. After loading and transforming the mesh, xSDF reports the resulting bounds and watertightness. xSDF does not auto-repair meshes: FWN signs non-watertight geometry without issue, but the gradient flood fill can leak through large open holes (e.g. the raw Stanford bunny case) — repair such meshes first (e.g. with pymeshfix, which is what I've used for the bunny).
"grid": {
"target_min_size": 0.02,
"stretch_factor": 1.05,
"stretch_axes": { "x": { ... }, "y": { ... }, "z": { ... } },
"preview_stretch": true
}| Field | Description |
|---|---|
target_min_size |
Default fine cell size. Used as dx_min (center-point) or dx_target (piecewise) when an axis leaves it null / unspecified. |
stretch_factor |
Default geometric growth ratio. Used as r_max (center-point) or r_target (piecewise) when not overridden per-axis. |
preview_stretch |
If true, a matplotlib window pops up showing the 1D coordinate distributions and a 3D scatter of the face-vertex grid with the geometry overlaid. Close the window to proceed, or cancel to abort the run before SDF computation. |
stretch_axes holds a per-axis sub-block (x, y, z). Two stretching modes are supported:
Center-point (single geometric stretch):
"x": { "center": 0.0, "dx_min": null, "r_max": 1.025 }| Field | Description |
|---|---|
center |
Focus point where spacing is finest. |
dx_min |
Smallest cell size at center (inherits target_min_size if null). |
r_max |
Geometric growth ratio outward from center (inherits stretch_factor if null; set to 1.0 for a uniform axis). |
Piecewise (multi-segment):
"x": {
"type": "piecewise",
"segments": [
{"type": "DECREASING", "lower_bound": -2.0, "upper_bound": -1.25},
{"type": "CONSTANT", "lower_bound": -1.25, "upper_bound": 0.5 },
{"type": "INCREASING", "lower_bound": 0.5, "upper_bound": 2.5 }
]
}The segments list is ordered lower-to-upper along the axis. Each entry has a type (CONSTANT, INCREASING, or DECREASING) and [lower_bound, upper_bound]. dx_target and r_target inherit from target_min_size and stretch_factor unless overridden in the sub-block. See docs/stretching_methods.md for the full reference.
"backend": {
"method": "torch",
"memory_budget_gb": 8.0,
"torch": {
"device": "mps",
"fwn_beta": 2.0,
"fwn_band_width_cells": 3.0,
"cos_theta_min": 0.8
}
}| Field | Description |
|---|---|
method |
"torch" for the LBVH+FWN GPU backend (default), or "trimesh" for fallback and testing. |
memory_budget_gb |
Memory budget consumed by the trimesh backend only (controls its grid chunk size). The torch backend uses a fixed empirical MPS-safe chunk threshold internally and ignores this field. |
torch.device |
"cuda", "mps" (Apple silicon), or "cpu". |
torch.fwn_beta |
Barill dipole admissibility ratio (default 2.0 — ~4 digits FWN accuracy). Raise to 3.0+ if sign is wrong near thin features. |
torch.fwn_band_width_cells |
Width of the narrow band around the surface, in cells (default 3.0). Voxels inside this band always get exact FWN signing; outside it, the gradient flood fill runs first and FWN only revisits conflicts and unresolved voxels. |
torch.cos_theta_min |
Gradient-alignment threshold for flood-fill voting (default 0.8). Higher = more conservative propagation; lower = more aggressive. |
A standalone FWN-tuning example: examples/sdf_config_ahmed_fwn.json.
See /examples for various config examples using the Ahmed body case for the different grid types.
If you have found this useful or used xSDF in your work, please cite!
@software{xSDF,
author = {Hubbard, Ian},
title = {xSDF: GPU-accelerated signed distance fields with a linear BVH and fast winding numbers},
year = {2026},
version = {0.1.0},
url = {https://github.com/srcterm/xSDF}
}Found any bugs or issues? please just let me know!
- Speed tests on CUDA.
- Surface Area Heuristic (SAH), fix for MPS dispatch overhead as LBVH -> LHBVH?
- LBVH: Karras, "Maximizing Parallelism in the Construction of BVHs, Octrees, and k-d Trees", High Performance Graphics (2012)
- Fast Winding Number (FWN): Barill, Dickson, Schmidt, Levin, Jacobson, "Fast Winding Numbers for Soups and Clouds", SIGGRAPH (2018)
- Solid Angle Signing (on FWN leaf evaluation): Van Oosterom & Strackee, "The Solid Angle of a Plane Triangle" (1983)
- Point-Triangle Distance (AABB): Ericson. C, "Real-Time Collision Detection" (2004)





