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.

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.

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.