RasmahRasmah

Neural signed distance fields

Overview

The previous chapter showed that an MLP is a differentiable function. This chapter turns that function into geometry: a signed distance field represented by a network, not a formula.

The idea is the same one behind every SDF in Rasmah — a field $s(\mathbf{x})$ that is negative inside, zero on the surface, positive outside. The difference is that here $s$ is the output of a neural network. That makes the field learnable: instead of writing the formula for a shape, you fit a network to reproduce it, then treat the result like any other geometry.

A network as a field

NeuralSDF wraps an MLP, its parameters, and an activation into an ImplicitGeometry — so it plugs into field, gradient, bounds, marching cubes, and CSG exactly like an analytic SDF:

Rasmah.Neural.NeuralSDFType
NeuralSDF(mlp, θ, act, lo, hi)

A signed-distance field represented by an MLP with parameters θ and hidden activation act (use siren_activation for a SIREN network). lo/hi bound the region the network was fitted to; see bounds. The output layer is linear (a raw signed distance).

source
sizes = [3, 16, 16, 1]
net = MLP(sizes)
θ = siren_init(MersenneTwister(0), sizes)
act = siren_activation(30.0)
n = NeuralSDF(net, θ, act, [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5])
NeuralSDF{SirenActivation{Float64}}(MLP([3, 16, 16, 1]), [-0.03751277003496704, -0.3251055230370392, -0.2807154561298028, -0.22010855097324397, 0.03940441711296468, -0.26719687647463414, 0.29504731946018287, 0.3088171242551921, -0.008093882038976355, 0.151888097010342  …  -0.004313824301640936, 0.015020788645468023, 0.012336822605123674, -0.0008079898268037414, -0.0025575395316854437, 0.01930135155525001, -0.003444327281904761, 0.020009172723378196, 0.016667664575825192, 0.0], SirenActivation{Float64}(30.0), [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5])
field(n, [0.0, 0.0, 0.0])
0.0

The SIREN activation

Ordinary activations like relu are poor at representing signed distance fields, whose Laplacian and gradients matter. SIREN uses a sine — a periodic activation that captures fine spatial detail far better:

Rasmah.Neural.SirenActivationType
SirenActivation(ω₀)

Callable SIREN activation x -> sin(ω₀ * x). A concrete callable struct (not a closure) so it stays type-stable and reverse-mode-AD friendly.

source
siren_activation(30.0)(0.5)
0.6502878401571168

The eikonal regularizer

An exact SDF has unit gradient magnitude almost everywhere, $\|\nabla s\| = 1$. A neural field only learns this if you ask it to. The eikonal loss measures how far the field's gradient magnitude is from 1:

Rasmah.Neural.eikonal_lossFunction
eikonal_loss(mlp, θ, X; act, ε=1e-4)

Mean squared deviation of the field's spatial-gradient magnitude from 1 over the columns of X (3×N): mean((‖∇g(x)‖ − 1)²). The spatial gradient is computed by central finite differences over the input, so the loss stays differentiable in θ for any AD backend (no nested forward-mode needed).

source
X = randn(MersenneTwister(1), 3, 32)
eikonal_loss(net, θ, X; act=act)
0.39696600414579203

Fitting a neural SDF

fit_sdf combines these pieces: sample points, define an MSE-plus-eikonal loss, and train:

Rasmah.Neural.fit_sdfFunction
fit_sdf(target, lo, hi; …) -> NeuralSDF

Fit a SIREN NeuralSDF to target (an ImplicitGeometry or a callable x -> sdf) over the box [lo, hi]. Samples uniform off-surface points plus surface points (obtained by projecting onto the zero level set), and trains with mean-squared-error on the SDF values plus an eikonal regularizer weighted by λ.

Keywords: n (off-surface samples), hidden (hidden layer widths), ω₀ (SIREN frequency), epochs, lr, λ (eikonal weight; 0 disables it), seed, backend (AD backend forwarded to train!).

Backend and scale

Training uses train!'s default AD backend, which is neural_backend — a reverse-mode backend (Mooncake/Enzyme) when one is loaded, otherwise ForwardDiff (forward mode). Forward-mode gradient cost grows linearly with the parameter count, so with ForwardDiff use a modest configuration (hidden widths ≲ 32, n ≲ a few hundred); the default hidden = [64, 64], n = 4000 assumes a reverse-mode backend.

source
fit = fit_sdf(to_sdf(sphere(1.0)), [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5];
              hidden=[8, 8], n=50, epochs=100, lr=1e-3, seed=0)
NeuralSDF{SirenActivation{Float64}}(MLP([3, 8, 8, 1]), [-0.053334447489399806, -0.3289191618749901, -0.2975499931477904, -0.20121520688325797, 0.008257084066878367, -0.284430245213816, 0.31266456525729297, 0.3167208839889777, -0.04608867576132386, 0.15667595994754205  …  0.04198638618827228, 0.07153654645803563, 0.0643555062235597, -0.05832887223481961, 0.061877263222284734, -0.07275500840688322, -0.050248461114152126, 0.06906696595795957, 0.06222103863548914, 0.048387810747574034], SirenActivation{Float64}(30.0), [-1.5, -1.5, -1.5], [1.5, 1.5, 1.5])
field(fit, [0.0, 0.0, 0.0])
0.4150620210697129
vtk(fit)

This is a deliberately tiny fit (a $[3, 8, 8, 1]$ network trained for 100 epochs), so the result only roughly resembles a sphere. With a reverse-mode backend loaded the default configuration is much larger, and the fit converges to a smooth SDF indistinguishable from the analytic one.

Next steps

A neural field can also solve a differential equation directly — the idea behind Physics-informed neural networks.