Home

Pouring Milk

Project:
Farsight
Year:
2026
Type:
Procedural mesh
Tech:
Unity, C#, HLSL
Milk pouring into a cereal bowl, with the wireframe switching on
The wireframe switches on mid-pour to show the stream mesh.

The goal

There's a breakfast scene where you pour your own milk and cereal. Our first version of the pour was a particle effect, and it looked like one: a spray of dots, not a stream of milk.

We wanted it to feel like actual liquid. One stream that falls in a nice arc, glugs as you tip the carton, breaks up into drops when you stop, and makes ripples where it lands.

How it works

The stream is one tube mesh that gets rebuilt in code every frame, 10 sides around and 48 segments long. A separate script on the bowl and a shader on the milk handle everything happening on the surface.

1. The arc

The tube follows the path a thrown object would take, from the carton's spout down to the milk, worked out from the pour speed and gravity. Each segment is a ring of vertices placed along that curve.

The width follows the milk's speed. Falling liquid speeds up, and the same amount of milk moving faster has to get thinner, so the radius shrinks with the square root of the speed, down to 45% of the spout. Every glug from the carton sends a slight bulge down the stream with the milk.

float radius = spoutRadius * Mathf.Max(Mathf.Sqrt(speed0 / ArcVelocity(t).magnitude), minRadiusScale);
radius *= 1f + glugAmount * Glug(time - t);
Side view of the stream with its mesh rings
The stream at real scale, with its rings.
The stream straightened out at five moments, with a bulge travelling from the spout to the surface
The stream straightened out at five moments, 50 ms apart. A glug starts at the spout and rides down. The bulge is exaggerated 3x here so you can see it.
The milk stream as it appears in gameThe milk stream with its wireframe visible
The same stream in game, and its mesh.

2. Stopping

When you stop pouring, the arc locks in place so the end of the stream doesn't swing around with the carton. The tail then falls, and the last 5 cm of it pinches into beads about 8 mm apart, using a sine along the stream's length. That's how a real stream of liquid breaks up.

The end of the stream breaking into beads over four moments
The tail of the stream after you stop pouring, zoomed in.

3. The surface

Where the stream lands it starts ripple rings and a patch of foam. Each ring is worked out in the milk shader: a wave that travels outwards at 9 cm a second, with a 1.6 cm wavelength, fading as it grows. Up to eight can run at once, and they're sent per bowl, so other bowls using the same material don't ripple too.

Top view of the milk surface with several overlapping ripple rings
Four rings of different ages, from the shader's ripple function.

Cereal makes its own ripples when it drops in, and some of it floats up and bobs as the milk rises.

The diagrams on this page were rendered with a Python port of the stream and ripple code. The clip at the top is from the game.