Advanced shaders · lesson 16 of 19 · Advanced · about 5 min

Smoke simulation 1: a flow field from noise

You will build: Write flow(q): the curl of a slowly scrolling noise. Show it as colors: red = sideways flow, green = upward flow.

The picture produced by the shader in the lesson "Smoke simulation 1: a flow field from noise"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

Real smoke and water move according to fluid equations. Solving them properly needs memory (the state of the fluid in the last frame), which a single-pass pixel shader does not have. But there is a famous shortcut that looks very convincing: invent a velocity field that behaves like a fluid, and let things be carried along by it.

What makes a velocity field fluid-like? Water cannot be squashed. In a field that flows without any spot where fluid appears or vanishes (no sources, no sinks) everything just swirls. There is a recipe for building such a field from any smooth noise: take its curl. In 2D, if n(x, y) is the noise, the velocity is (dn/dy, -dn/dx): the noise "rotated by 90 degrees". We measure the slopes with finite differences, exactly like the normals earlier:

float a = noise2((q + float2(0.0, e)) * 1.8 + ...) - noise2((q - float2(0.0, e)) * 1.8 + ...);   // dn/dy
float b = noise2((q + float2(e, 0.0)) * 1.8 + ...) - noise2((q - float2(e, 0.0)) * 1.8 + ...);   // dn/dx
float2 curl = float2(a, -b) / (2.0 * e);

Scrolling the noise slowly with time (+ float2(0.0, t)) makes the swirls evolve.

This lesson only looks at the field: the red channel is the sideways flow, the green channel the vertical flow. You will see big soft vortices, and next lesson uses them to move smoke.

The numbers you need

Everything the picture depends on is given, so the challenge is the code, not guessing.

Space
p = (uv - 0.5) * float2(aspect, 1.0) * 2.0
Noise coordinates
noise2(point * 1.8 + float2(0.0, time * 0.3))
Slopes
e = 0.05; a = noise2 at (q + (0, e)) minus noise2 at (q - (0, e)); b = noise2 at (q + (e, 0)) minus noise2 at (q - (e, 0))
Curl
float2(a, -b) / (2.0 * e)
Color
float3(0.5 + 0.18 * curl.x, 0.5 + 0.18 * curl.y, 0.5)

Try it in your own language

The lesson is written once and shown in every language. Pick yours: Unity (Built-in), Unity (URP), Unreal, Shadertoy / WebGL, OpenGL (LWJGL, raylib), three.js, GameMaker, LÖVE (Lua), Godot, Apple (iOS, macOS), WebGPU / Bevy.

Start coding