Geometry & 3D Vision
Signed Distance Function
A scalar field giving the distance to the nearest surface point, negative inside and positive outside, whose zero level set is the surface and whose gradient is the surface normal.
intermediate
A signed distance function (SDF) stores a shape as a scalar field: at every point, the distance to the nearest surface point, signed to say inside or outside, so that the surface is where the field is zero. Among the 3D representations, it answers “how far is the surface, and in which direction?” with one evaluation, which is why it underlies sphere-traced rendering, volumetric depth fusion, collision checking and many neural shape models.
Definition
For a closed surface bounding a solid, the signed distance function gives, at each point, the Euclidean distance to the nearest point of , negative inside the solid, positive outside, and zero on the surface. Some systems flip the sign, so check it when mixing code or data.
Intuition
A depth map stores one distance per pixel, along its ray. An SDF stores, at every point, the radius of the largest ball centred there that touches the surface without crossing it. Walking out from the centre of a ball of radius , the value rises by exactly one unit per unit moved, from through at the surface.
Formal Definition
Let be the solid and its surface. With ,
Hart (1996, §2.1) uses the same definition and sign.
Eikonal property. is 1-Lipschitz, , and wherever it is differentiable,
The distance is the viscosity solution of this eikonal equation with on the surface. The gradient fails to exist on the medial axis, the points with two or more nearest surface points, such as the centre of a ball or the diagonal planes inside a box; this set has zero volume, so the property holds almost everywhere (a standard result, stated without a source).
Normal and closest point. Off the medial axis, is the unit vector along the line from the nearest surface point, pointing outward: away from that point outside the solid, toward it inside. Hart (1996) uses this property outside a convex solid in the proof of Theorem 4. On the surface it is the outward unit normal, and any nearby point projects onto the surface by
Properties
Exact distances and distance bounds
Hart (1996, §2.1) calls a function a signed distance bound when : right sign and zero set, but possibly an underestimate. A bound suffices wherever only a ball of free space is needed, as in sphere tracing, and by Hart’s Theorem 1, is a bound for any with Lipschitz constant .
SDFs combine into constructive solid geometry (CSG) shapes by Boolean operations (Hart, 1996, §2.4), but the results are not all exact:
Hart proves that bounds the distance to the intersection (Theorem 3), so subtraction, an intersection with a complement, gives only a bound; his Eq. 15 and Table 1 present as the distance to the union. Each is exact on one side only: inside the intersection, while outside it the nearest point of the intersection can be farther than either surface; outside the union, while inside it the nearest surface point of one shape may lie buried in the other (reasoning from the definitions; the example measures both). Hart’s Table 1 also lists blends, tapers and twists among the operations that give bounds.
Unsigned distance and occupancy
The unsigned distance has the same zero set but no inside, and a V-shaped, non-differentiable minimum at the surface. Hoppe et al. (1992, §3.1) chose a signed field for this reason: zero is a regular value of but not of , so the zero set of is a manifold. Unsigned fields suit open surfaces, which have no inside.
Occupancy keeps only the sign, so it supports neither safe ray steps nor sub-voxel surface placement.
Truncation
A truncated SDF (TSDF) clips it to , often divided by , where is the truncation distance. Curless and Levoy (1996) restricted their signed distances and weights to the vicinity of the range measurements, so that surfaces on opposite sides of an object do not interfere and the volume stays compact and cheap to update. A TSDF keeps the zero set but loses the distance property beyond : its gradient vanishes there, and a sphere tracer can advance at most per step. The TSDF fusion article covers fusing depth maps into one.
Computing an SDF
Analytic primitives. Many shapes have closed-form SDFs; Hart (1996, Appendix A) derives several. For a sphere, . For an axis-aligned box with centre and half-sizes , with taken per axis,
The first term is the distance from outside to the nearest face, edge or corner, the second the negative distance to the nearest face from inside (our derivation, checked against brute force in the example).
From a mesh. The unsigned distance is the distance to the nearest triangle, found with a spatial index; the sign needs an inside test. Ray parity, an odd number of crossings meaning inside, works on clean watertight meshes but fails when the ray hits an edge or the mesh has holes. The generalized winding number of Jacobson et al. (2013), , sums the signed solid angles the triangles subtend at the point; it is 1 inside and 0 outside a closed, consistently oriented mesh and degrades smoothly with holes and non-manifold parts, so thresholding it at gives a sign (the authors themselves go further, with a graph cut). Park et al. (2019, §5) note that human-designed meshes are commonly not watertight and contain internal structures; they computed signed distances from surface points and normals seen by virtual cameras around each object.
From points. Hoppe et al. (1992) fitted a consistently oriented tangent plane (centre , normal ) to each point’s neighbours, as described in the point cloud article, and defined for the plane whose centre is closest to . Where the projection of falls farther from the data than the sampling density and noise allow, is undefined, which preserves boundaries.
On grids. Given distances in a thin band of voxels around the surface, the rest of a grid is filled by solving the eikonal equation outward: the fast marching method (Sethian, 1996) advances the front monotonically, and the fast sweeping method (Zhao, 2005) runs Gauss–Seidel sweeps in alternating directions, at cost for grid points with sweeps sufficient for the distance in dimensions, according to its abstract.
Using an SDF
Sphere tracing. To find where a ray first meets the surface, step by the field’s value, , starting from . The ball of radius around the current point contains no surface, so no step overshoots; Hart (1996, §2.2) calls these unbounding spheres, and the method needs only a distance bound, not a derivative. Hart proves linear convergence to the first root (Theorem 2), notes that a ray parallel to a ground plane steps at fixed intervals without ever converging (§2.5.5), and found on a sphere that halving the distance estimate doubled the number of steps (§4.3).
Inside and collision tests. The sign is an inside test and the value a margin: a ball of radius centred at is collision-free when , and a penetrating point is pushed out by along (reasoning from the definition), which motion planning and physics simulation exploit.
Offsets and morphology. is the solid dilated by a ball of radius and the solid eroded by it, continuous counterparts of the operators of mathematical morphology. The shifted field is exact outside the dilated solid but in general only a bound inside it, so an opening (erosion, then dilation) needs the distance recomputed in between (reasoning from the definitions).
Surface extraction. On a grid, marching cubes extracts the zero level set as a triangle mesh, interpolating along voxel edges to place vertices inside cells.
Neural SDFs. DeepSDF (Park et al., 2019) regresses the signed distance, with this sign convention, from a network conditioned on a latent code per shape. Its loss clamps prediction and target to with ; a larger allows faster ray tracing, a smaller one concentrates capacity near the surface (§3), the trade-off of TSDF truncation. The neural implicit representations article covers neural fields.
Examples
The NumPy and SciPy code combines a sphere and an overlapping box into a union, an intersection and the box minus the sphere. On a grid over it checks each field two ways: the eikonal test (finite-difference more than 0.05 from 1) and brute force (the distance to 400,000 surface samples, signed by the exact inside test; sampling error up to about 0.004). It then sphere-traces a depth image from a camera with the world-to-camera convention of the camera parameters article, compares each depth with exact ray–primitive intersections, and builds a TSDF of the union.
import numpy as np
from scipy.spatial import cKDTree
# Two primitives, negative inside, positive outside, zero on the surface.
C_S, R_S = np.array([0.25, 0.0, 0.0]), 0.5 # sphere: centre, radius
C_B, H_B = np.array([-0.2, 0.0, 0.0]), np.array([0.4, 0.3, 0.3]) # box: centre, half-sizes
def sphere(p):
return np.linalg.norm(p - C_S, axis=-1) - R_S
def box(p):
q = np.abs(p - C_B) - H_B # per-axis distance past each face
outside = np.linalg.norm(np.maximum(q, 0.0), axis=-1)
inside = np.minimum(q.max(axis=-1), 0.0)
return outside + inside
shapes = {
"sphere": sphere,
"box": box,
"union": lambda p: np.minimum(sphere(p), box(p)),
"intersection": lambda p: np.maximum(sphere(p), box(p)),
"box - sphere": lambda p: np.maximum(box(p), -sphere(p)),
}
# Dense, area-uniform samples of each primitive's surface (fixed seed).
rng = np.random.default_rng(0)
u = rng.normal(size=(400_000, 3))
s_pts = C_S + R_S * u / np.linalg.norm(u, axis=1, keepdims=True)
areas = 4 * np.array([H_B[1] * H_B[2], H_B[0] * H_B[2], H_B[0] * H_B[1]])
n_face = np.round(400_000 * areas / areas.sum()).astype(int)
b_pts = []
for ax in range(3):
for side in (-1, 1):
q = rng.uniform(-1, 1, (n_face[ax] // 2, 3)) * H_B
q[:, ax] = side * H_B[ax]
b_pts.append(C_B + q)
b_pts = np.concatenate(b_pts)
# Keep the samples that lie on each composite's boundary.
eps = 1e-9
surface = {
"sphere": s_pts,
"box": b_pts,
"union": np.concatenate([s_pts[box(s_pts) >= -eps], b_pts[sphere(b_pts) >= -eps]]),
"intersection": np.concatenate([s_pts[box(s_pts) <= eps], b_pts[sphere(b_pts) <= eps]]),
"box - sphere": np.concatenate([s_pts[box(s_pts) <= eps], b_pts[sphere(b_pts) >= -eps]]),
}
# Query grid over [-1, 1]^3.
n = 48
h = 2.0 / (n - 1)
g = np.linspace(-1, 1, n)
P = np.stack(np.meshgrid(g, g, g, indexing="ij"), -1)
print("field |grad|-1|>0.05 error>0.01 of which |grad|~1 max error in out |f|<=|d|")
for name, f in shapes.items():
F = f(P)
grad = np.stack(np.gradient(F, h), -1)
dev = np.abs(np.linalg.norm(grad, axis=-1) - 1.0)
interior = np.zeros(F.shape, bool); interior[1:-1, 1:-1, 1:-1] = True
d, _ = cKDTree(surface[name]).query(P.reshape(-1, 3), workers=-1)
d = np.where(F.ravel() < 0, -d, d) # sign from the (exact) inside test
err = np.abs(F.ravel() - d)
ins, out = F.ravel() < 0, F.ravel() >= 0
bound = np.all(np.abs(F.ravel()) <= np.abs(d) + 0.01)
dev, inexact = dev[interior], (err > 0.01)[interior.ravel()]
hidden = f"{np.mean(dev[inexact] <= 0.05):.1%}" if inexact.any() else "-"
print(f"{name:13s} {np.mean(dev > 0.05):13.1%} {np.mean(inexact):10.1%} {hidden:>16s} "
f"{err[ins].max():11.3f} {err[out].max():5.3f} {bound}")
# Sphere tracing a 32 x 24 depth image. World-to-camera X_c = R X_w + t.
Cam = np.array([0.8, -2.2, 1.2]) # camera centre (world)
z = -Cam / np.linalg.norm(Cam) # optical axis looks at the origin
x = np.cross(z, [0.0, 0.0, 1.0]); x /= np.linalg.norm(x)
y = np.cross(z, x) # image y points down
R = np.stack([x, y, z]) # rows: camera axes; t = -R Cam
W, H, fpx = 32, 24, 30.0
K = np.array([[fpx, 0, (W - 1) / 2], [0, fpx, (H - 1) / 2], [0, 0, 1]])
uu, vv = np.meshgrid(np.arange(W), np.arange(H))
rays = np.stack([uu, vv, np.ones_like(uu)], -1).reshape(-1, 3) @ np.linalg.inv(K).T
D = rays @ R # directions in world: R^T d_c
D /= np.linalg.norm(D, axis=1, keepdims=True)
def slab(c, half): # exact ray-box interval
with np.errstate(divide="ignore"):
a, b = (c - half - Cam) / D, (c + half - Cam) / D
return np.minimum(a, b).max(1), np.maximum(a, b).min(1)
def ball(c, r): # exact ray-sphere interval
oc = Cam - c
bq = (D * oc).sum(1); disc = bq**2 - (oc @ oc - r**2)
sq = np.sqrt(np.maximum(disc, 0))
return np.where(disc >= 0, -bq - sq, np.inf), np.where(disc >= 0, -bq + sq, -np.inf)
(s0, s1), (b0, b1) = ball(C_S, R_S), slab(C_B, H_B)
s_hit, b_hit = s0 <= s1, b0 <= b1
inf = np.full(len(D), np.inf)
exact = { # first hit by interval CSG along each ray
"union": np.minimum(np.where(s_hit, s0, inf), np.where(b_hit, b0, inf)),
"intersection": np.where(s_hit & b_hit & (np.maximum(s0, b0) <= np.minimum(s1, b1)),
np.maximum(s0, b0), inf),
"box - sphere": np.where(~b_hit, inf,
np.where(~s_hit | (b0 < s0) | (b0 > s1), b0,
np.where(s1 < b1, s1, inf))),
}
def trace(f, eps=1e-4, t_max=6.0, max_steps=500):
s = np.zeros(len(D)); steps = np.zeros(len(D), int)
active = np.ones(len(D), bool); hit = np.zeros(len(D), bool)
for _ in range(max_steps):
dist = f(Cam + s[active, None] * D[active])
idx = np.flatnonzero(active)
steps[idx] += 1
hit[idx[dist < eps]] = True
s[idx] += dist
active[idx[(dist < eps) | (s[idx] > t_max)]] = False
if not active.any():
break
return np.where(hit, s, np.inf), steps, hit
mu = 0.1 # TSDF truncation distance
fields = [("union", shapes["union"]), ("intersection", shapes["intersection"]),
("box - sphere", shapes["box - sphere"]),
("union / 2", lambda p: 0.5 * shapes["union"](p)),
("union TSDF", lambda p: np.clip(shapes["union"](p), -mu, mu))]
print("\nfield hit px wrong px |dZ| median max steps hit: mean max miss: mean")
for name, f in fields:
s, steps, hit = trace(f)
ref = exact[name.split(" /")[0].split(" TSDF")[0]]
zdir = D @ z # depth Z = ray length * cos(angle)
both = hit & np.isfinite(ref)
dZ = np.abs(s[both] - ref[both]) * zdir[both]
print(f"{name:13s} {hit.sum():6d} {np.sum(hit != np.isfinite(ref)):8d} {np.median(dZ):11.1e} {dZ.max():7.1e}"
f" {steps[hit].mean():15.1f} {steps[hit].max():4d} {steps[~hit].mean():10.1f}")
# A TSDF on the 48^3 grid: the union's distance clipped to [-mu, mu].
F = shapes["union"](P)
T = np.clip(F, -mu, mu)
band = np.abs(F) < mu
print(f"\nTSDF mu = {mu} ({mu / h:.1f} voxels): {band.mean():.1%} of voxels in the band, "
f"{np.mean(T == mu):.1%} at +mu, {np.mean(T == -mu):.1%} at -mu")
i = k = n // 2 # one grid line along x (y = z = 0.02)
print("x: ", " ".join(f"{v:5.2f}" for v in g[::4]))
print("SDF: ", " ".join(f"{v:5.2f}" for v in F[::4, i, k]))
print("TSDF:", " ".join(f"{v:5.2f}" for v in T[::4, i, k]))
# Zero crossings along every x-line, by linear interpolation between voxels.
def crossings(V):
a, b = V[:-1], V[1:]
c = np.sign(a) != np.sign(b)
return c, a[c] / (a[c] - b[c]) # fraction of a voxel step
cF, oF = crossings(F); cT, oT = crossings(T)
Pc = np.stack(np.broadcast_arrays(g[:-1, None, None], g[None, :, None], g[None, None, :]), -1)[cF]
Pc[:, 0] += h * oF
print(f"{cF.sum()} zero crossings; max |SDF| at them: {np.abs(shapes['union'](Pc)).max():.1e}; "
f"same from the TSDF: {np.array_equal(cF, cT) and np.allclose(oF, oT)}")
Output:
field |grad|-1|>0.05 error>0.01 of which |grad|~1 max error in out |f|<=|d|
sphere 0.1% 0.0% - 0.002 0.004 True
box 1.4% 0.0% - 0.001 0.002 True
union 1.9% 0.2% 38.3% 0.080 0.003 True
intersection 1.8% 8.4% 91.3% 0.002 0.048 True
box - sphere 2.7% 48.1% 96.8% 0.004 0.352 True
field hit px wrong px |dZ| median max steps hit: mean max miss: mean
union 158 0 2.4e-05 3.4e-03 11.5 127 8.1
intersection 47 0 2.5e-05 3.4e-04 14.2 33 6.1
box - sphere 65 0 2.4e-05 4.4e-04 13.4 34 6.7
union / 2 158 0 1.4e-04 6.3e-03 26.9 255 16.4
union TSDF 158 0 2.3e-05 3.5e-03 31.9 147 61.6
TSDF mu = 0.1 (2.4 voxels): 9.5% of voxels in the band, 86.5% at +mu, 4.0% at -mu
x: -1.00 -0.83 -0.66 -0.49 -0.32 -0.15 0.02 0.19 0.36 0.53 0.70 0.87
SDF: 0.40 0.23 0.06 -0.11 -0.28 -0.28 -0.27 -0.43 -0.38 -0.22 -0.05 0.12
TSDF: 0.10 0.10 0.06 -0.10 -0.10 -0.10 -0.10 -0.10 -0.10 -0.10 -0.05 0.10
864 zero crossings; max |SDF| at them: 9.0e-04; same from the TSDF: True
The sphere and box agree with brute force to within the sampling error, confirming the box formula. The composites err only where predicted: the union only inside (up to 0.080), the intersection and difference only outside, the difference on 48% of the grid by up to 0.352, where the nearest box surface has been cut away. All are valid bounds. The eikonal test misses this: it fires on 1.4–2.7% of voxels in the box and the composites, exact or not, at creases and the medial axis, while 38–97% of the inexact voxels pass it.
Sphere tracing classifies every pixel correctly, bounds included. Median depth errors are about , below the stopping threshold , except for the halved field, whose test allows twice the distance; the maxima are larger because the test bounds the distance to the surface, not along the ray, so a nearly tangential ray can stop short of the hit (reasoning from the stopping rule). Halving the field multiplies the mean step count by 2.3, close to Hart’s observation. The TSDF finds its 864 zero crossings at the same positions as the full SDF, within of the true surface (linear interpolation error), although only 9.5% of voxels carry a distance; tracing it costs more, since no step exceeds : 61.6 steps per missing ray instead of 8.1.
Common Misconceptions
- “ almost everywhere means is a distance.” It is necessary, not sufficient: min and max of exact SDFs satisfy it while underestimating the distance on one side.
- “A TSDF is an SDF with a smaller range.” Beyond it carries only a sign; its gradient vanishes, and it cannot produce normals or closest points there.
Where It Is Used
- Depth fusion and dense mapping: TSDF grids fuse range images (Curless & Levoy, 1996), multi-view stereo depth maps, and RGB-D frames in dense SLAM.
- Surface reconstruction: signed fields from oriented point clouds (Hoppe et al., 1992); see 3D reconstruction.
- Rendering and robotics: sphere tracing of analytic and CSG models (Hart, 1996); clearance and penetration tests.
- Learned shape models: DeepSDF and later neural fields.
Related
- Point Cloud
A set of 3D points sampled from the surfaces of a scene, possibly with color, intensity, or normals per point, and the most direct output of 3D sensors and reconstruction.
- Depth Map
An image whose pixels store the distance to the scene, usually the depth Z along the optical axis, so that each pixel with a known camera can be lifted to a 3D point.
- Mathematical Morphology
A theory and toolbox for processing images by shape, probing them with a small set called a structuring element through erosion, dilation, and the operators built from them, such as opening, closing, top-hats, and morphological reconstruction.
- 3D Reconstruction
Recovering the 3D shape of objects and scenes from images, depth maps, or range scans, as point clouds, meshes, volumes, or implicit and radiance fields, and measuring how accurate and complete the result is.
- Multi-View Stereo
Recovering dense 3D geometry, as depth maps, point clouds, meshes, or volumes, from many images whose camera poses and intrinsics are known.
- Simultaneous Localization and Mapping (SLAM)
Building a map of an unknown environment while estimating the trajectory of the sensor moving through it, and using recognized revisits to keep both globally consistent.
References
- Hart, J. C. (1996). Sphere Tracing: A Geometric Method for the Antialiased Ray Tracing of Implicit Surfaces. The Visual Computer, 12(10), 527–545.
- Hoppe, H., DeRose, T., Duchamp, T., McDonald, J. & Stuetzle, W. (1992). Surface Reconstruction from Unorganized Points. Computer Graphics (Proceedings of SIGGRAPH '92), 26(2), 71–78.
- Jacobson, A., Kavan, L. & Sorkine-Hornung, O. (2013). Robust Inside-Outside Segmentation using Generalized Winding Numbers. ACM Transactions on Graphics, 32(4), 1–12.
- Sethian, J. A. (1996). A Fast Marching Level Set Method for Monotonically Advancing Fronts. Proceedings of the National Academy of Sciences, 93(4), 1591–1595.
- Zhao, H. (2005). A Fast Sweeping Method for Eikonal Equations. Mathematics of Computation, 74(250), 603–627.
- Curless, B. & Levoy, M. (1996). A Volumetric Method for Building Complex Models from Range Images. Proceedings of the 23rd Annual Conference on Computer Graphics and Interactive Techniques (SIGGRAPH), 303–312.
- Park, J. J., Florence, P., Straub, J., Newcombe, R. & Lovegrove, S. (2019). DeepSDF: Learning Continuous Signed Distance Functions for Shape Representation. IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 165–174.