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

Smoke simulation 2: follow the smoke back to its source

You will build: Trace each pixel back along the flow to the chimney. Where it started inside the chimney it is smoke, thinner the older it is, puffed up and shaded with noise, glowing orange at the source.

The picture produced by the shader in the lesson "Smoke simulation 2: follow the smoke back to its source"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

We have a velocity field, but a pixel shader cannot move smoke around: every pixel is computed alone. The solution is one of the oldest tricks in fluid graphics: ask where this pixel's smoke came from.

Take the pixel's position and walk backwards along the flow: q -= flow(q) * 0.08, again and again (14 times here). That traces a path from the pixel back to where its smoke started. Count the time you walked, age. At the end, look at where you arrived. Is it near the chimney, the place that makes smoke? If so, this pixel contains smoke, and the older it is (the longer the walk), the thinner it has become: exp(-age * 0.5).

The flow adds a steady upward drift, float2(0.0, 0.45), so the smoke rises, plus the curl swirls so it wiggles and billows.

Two finishing touches: a second layer of noise puffs up the density (0.5 + fbm2(...)) so the plume is lumpy, and the shading of the smoke itself is another noise (bright lumps, dark gaps), plus an orange glow at the source from the fire below.

float2 q = p;
float age = 0.0;
for (int i = 0; i < 14; i++)
{
    q -= flow(q) * 0.08;      // step back along the flow
    age += 0.08;
}
float2 src = (q - float2(0.0, -0.55)) * float2(1.0, 2.2);   // offset from the chimney
float body = exp(-dot(src, src) * 6.0);                      // 1 at the chimney, fades away from it

This is called semi-Lagrangian advection. It is the same step real fluid simulators use, only here the "previous frame" is a function we can evaluate on the spot.

The numbers you need

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

Flow
flow(q) = float2(0.0, 0.45) + curlNoise(q) * 0.18 (the curl from the last lesson, with the upward drift)
Trace back
p = (uv - 0.5) * float2(aspect, 1.0); q = p; age = 0.0; 14 times: q -= flow(q) * 0.08; age += 0.08
Source
src = (q - float2(0.0, -0.55)) * float2(1.0, 2.2); body = exp(-dot(src, src) * 6.0)
Puffs
puff = 0.5 + 1.0 * fbm2(p * 3.2 + float2(0.0, time * 0.4))
Density
d = body * exp(-age * 0.5) * puff
Smoke shade
shade = saturate(fbm2(p * 4.0 + float2(3.0, time * 0.35)) * 1.5); smoke = lerp(float3(0.16, 0.16, 0.19), float3(0.86, 0.86, 0.9), shade)
Glow at the source
smoke += float3(0.9, 0.4, 0.1) * 0.7 * exp(-length((p - float2(0.0, -0.55)) * float2(1.0, 1.5)) * 4.5)
Background
lerp(float3(0.05, 0.06, 0.09), float3(0.16, 0.18, 0.25), uv.y)
Result
lerp(background, smoke, saturate(d * 2.4))

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