Level Sets for Liquids
Water is a nightmare to animate — not because of how it flows, but because of its
surface. When a wave curls over, a splash breaks into a thousand droplets, or two
streams pour together into one pool, the boundary between air and water is constantly tearing
apart and stitching back together. If you tried to represent that surface as a triangle mesh —
a skin of vertices you drag around — every merge and split would force you to cut, weld and re-sew
the mesh by hand. One good splash and your mesh is a topological disaster.
The trick that made liquid simulation practical for film — the water of Finding Nemo,
Ice Age, the tidal waves of countless blockbusters — is to stop tracking the
surface directly and instead track a field whose zero contour is the
surface. That field is a level set, and its magic is that merges and splits happen
for free, with no mesh surgery at all. This page builds the idea from scratch: what the field is,
how you move it, why pure level sets quietly lose water, and how particles rescue the splash.
The implicit-surface idea
Instead of storing the surface as a list of points, we store a scalar function
\phi(\mathbf{x}) — read "phi" — sampled on a background grid that fills the
whole domain. At every grid point \phi holds one number: the
signed distance to the nearest surface, negative inside the liquid and positive
outside.
- \phi(\mathbf{x}) < 0 — the point \mathbf{x}
is inside the liquid (submerged).
- \phi(\mathbf{x}) > 0 — the point is outside, in
the air.
- \phi(\mathbf{x}) = 0 — the point lies exactly on the
surface. The air–water interface is the zero level set,
\{\mathbf{x} : \phi(\mathbf{x}) = 0\}.
The surface is never stored explicitly — it is implied, hiding wherever the field crosses
zero. If \phi is a genuine signed-distance function then
its magnitude also tells you how far you are from the water at every point, and its gradient
\nabla\phi is a unit vector pointing straight out of the liquid — a free
surface normal for shading, exactly where you need it.
Picturing the field: a circular blob
Below is the simplest possible liquid: a round blob of radius r centred at
\mathbf{c}. The bold ring is the zero contour
\phi = 0 — the surface. Everything shaded inside carries
\phi < 0; everything outside carries \phi > 0.
The faint rings are other level sets (contours of constant \phi), evenly
spaced because a signed-distance field changes by one unit of distance per unit of length.
The whole point of the picture: to move the water, you do not push the bold ring around. You change
the numbers on the grid, and the bold ring goes wherever the numbers happen to cross zero.
Worked example: the signed distance to a circle
For that blob we can write \phi down in closed form. The distance from any
point \mathbf{x} to the centre is
|\mathbf{x} - \mathbf{c}|; subtracting the radius gives the signed distance
to the rim:
\phi(\mathbf{x}) = |\mathbf{x} - \mathbf{c}| - r.
Check the three regimes. At the centre, |\mathbf{x}-\mathbf{c}| = 0 so
\phi = -r — deep inside, maximally negative. On the rim,
|\mathbf{x}-\mathbf{c}| = r so \phi = 0 — exactly
the surface. A distance 2r from the centre,
\phi = 2r - r = r > 0 — out in the air. Concretely, with
\mathbf{c} = (0,0) and r = 1, the point
(0.5, 0) gives \phi = 0.5 - 1 = -0.5 (inside) and
(2, 0) gives \phi = 2 - 1 = 1 (outside). No list
of surface points anywhere — just an arithmetic rule that knows where the water is.
Now imagine a second blob nearby with its own field, and take the minimum of the two
\phi values at every grid point,
\phi = \min(\phi_1, \phi_2). Where the blobs are far apart you get two
separate zero rings; slide them together and the two negative regions overlap, the zero contour
pinches shut between them, and you are left with a single blended surface — two drops have
merged into one, and nowhere did any code special-case "these two are now touching." That is
the whole reason liquids are done this way.
Moving the water: advection
The fluid solver (an Eulerian stable-fluids
grid, or an SPH particle
method) gives us a velocity field \mathbf{u}(\mathbf{x}, t) — how fast the
water is moving everywhere. To make the surface follow the flow, we simply let the whole field
\phi be carried along by that velocity. The value of
\phi attached to a moving parcel of fluid never changes, which is exactly
the advection equation:
\frac{\partial \phi}{\partial t} + \mathbf{u} \cdot \nabla \phi = 0.
Read it as: the change of \phi in time
(\partial\phi/\partial t) plus the amount swept in by the flow
(\mathbf{u}\cdot\nabla\phi) cancels to zero. Solve this on the grid each
frame and the zero contour rides along with the water. Because it is the field that moves,
not a mesh, topology changes are automatic: a sheet of water can thin, pinch off a
droplet, and let that droplet drift away as its own little zero contour — no cutting, no welding, no
bookkeeping about "which vertex belongs to which blob."
It feels backwards at first. In an Eulerian view the grid is a fixed set of
windows onto the world — like weather stations bolted to the ground — and what changes is the
reading at each station as air (or water) blows past. The level set lives in this world:
the grid never moves, but the number \phi stored at each cell updates
every frame as the surface sweeps through. The alternative — a mesh whose vertices are the
water and move with it — is the Lagrangian view, and it is precisely that
moving-mesh that struggles with splashes. Level sets get the best of the fixed grid: a surface that
can do anything topologically, stored in a structure that never has to be re-connected.
Keeping φ honest: reinitialization
There is a catch. The advection equation moves the zero contour correctly, but it does not
keep \phi a clean signed-distance function elsewhere. Where the flow
stretches, \phi gets sheared: its slope
|\nabla\phi|, which should stay at 1 for a true
distance field, drifts up in some places and flattens toward 0 in others.
A flattened field near the surface is numerical poison — the zero crossing becomes vague and the
surface smears.
- Periodically rebuild \phi into a true
signed-distance function while holding the current zero contour fixed.
- The target is the eikonal equation
|\nabla\phi| = 1 — distance increases by one unit per unit of length.
- It changes where the surface is not (repairing the slope), never where the surface
is (the zero set is preserved).
In practice you run a few sweeps of a redistancing solver (a fast-marching or fast-sweeping method)
every handful of frames. Think of it as ironing the field flat again — same fold line (the surface),
crisp gradient everywhere else.
The dark secret of level sets: they do not conserve volume. Advecting and
reinitializing a field on a grid smooths away anything thinner than a grid cell — the crest
of a splash, a slender sheet of water, a fine spray of droplets. Those features simply evaporate
as the numerical scheme rounds them off, and over a long shot a pool can visibly shrink even though no
water should be leaving. The very thing level sets are prized for — effortless splashy topology — is
the thing they destroy.
The fix is to combine the grid with marker particles. The
particle level set method scatters massless marker particles on both sides of the
surface; when the grid's zero contour drifts away from where the particles say the water is, the
particles correct it, rebuilding the thin features the grid lost. FLIP
(and the FLIP/PIC hybrid) goes further and stores the velocity on particles too, transferring
it to and from the grid each step so that fine detail and momentum both survive. The rule of thumb for
production water: never ship a pure level set — always back it with particles so the splash
keeps its droplets and the pool keeps its volume.
From field back to a surface: marching cubes
At render time you still need actual geometry to shade and refract. You get it by running
marching cubes over the \phi grid: march through every
little cube of eight grid samples, look at which corners are inside
(\phi < 0) versus outside (\phi > 0), and
where an edge crosses zero, plant a vertex by interpolating between its two corner values. Stitch the
per-cube triangles together and you have a watertight mesh of the surface — extracted fresh every
frame, with whatever topology the water currently has. The gradient \nabla\phi
hands you smooth per-vertex normals for that glassy water look.
So the full liquid pipeline is a loop: solve for velocity \mathbf{u},
advect \phi along it, correct it with
particles, reinitialize to a clean distance field, then
marching-cubes the zero contour into a mesh to render. Repeat for the next frame, and
the water flows, splashes, merges and splits — all governed by one quiet scalar field.
Both define a surface implicitly as f = \text{constant}, so the family
resemblance is real. But a metaball field is a fixed sum of blobby potentials
you author — you place the blobs and the isosurface follows. A level set field is a
signed distance that you simulate: it is advected by a physical velocity,
redistanced, and corrected by particles, so its shape is an outcome of fluid dynamics rather than a
modelling choice. Metaballs are how you sculpt an implicit surface; level sets are how you
animate one under the laws of flow.