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

Fire 1: a flame you can see into

You will build: March a ray through a swaying, turbulent flame column and add up its glow: hot at the base, cooler at the tip, on a dark night.

The picture produced by the shader in the lesson "Fire 1: a flame you can see into"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

A flame is not a surface and it does not reflect light: it makes light. So we do ray marching again, but with a different job. Instead of asking "where is the surface?" every step along the ray adds glow, as much as the flame is dense and hot there.

The shape of the flame comes from a density function:

  • a column that narrows as it rises, w = 0.8 * pow(1.0 - k, 0.9) (k is height from 0 to 1);
  • pushed sideways by a slow sway noise, so the whole flame leans;
  • eaten away by turbulence: fractal noise that scrolls downwards through the flame as time passes, p.y * 1.3 - time * 2.6, which makes the flame features appear to rise. The noise is subtracted from the density, so tongues of flame break away from the column.

The color depends on temperature. Something hot glows white-yellow, cooler is orange, then red, and cold is black. A cheap fit of this "black-body" ramp: float3(1.5 * h, h * h, 0.3 * h * h * h). Raising h to powers makes red appear first, then green, then blue, as it gets hotter.

The flame gets cooler towards the top and at its edges (heat = d * (1 - 0.6 * height)). Because fire is see-through, light behind it still shows (the transmittance T again), but it is thin, so it blocks little.

float3 fireColor(float h)
{
    return float3(1.5 * h, 1.0 * h * h, 0.3 * h * h * h);   // dark red, orange, yellow-white
}

The numbers you need

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

Flame density
flameD(p) in the Toolbox: a tapering column with sway and turbulence
Camera
ro = float3(0.0, 1.0, -3.6), rd = normalize(float3(p.x, p.y * 0.9 - 0.1, 1.7)), p = (uv - 0.5) * float2(aspect, 1.0) * 2.0
Night
lerp(float3(0.01, 0.01, 0.025), float3(0.03, 0.03, 0.07), saturate(rd.y + 0.3))
Flicker
flick = 0.9 + 0.1 * sin(time * 17.0) * sin(time * 7.3)
March
44 steps: t = 2.3 + float(i) * 0.075; d = flameD(ro + rd * t); if d > 0.01: heat = d * (1.0 - saturate(pos.y / 2.2) * 0.6); emit += T * fireColor(heat) * d * 0.075 * 6.0 * flick; T *= exp(-d * 0.075 * 1.5)
Result
night * T + emit

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

Go deeper

Part of the learning path: Game effects: smoke, fire and snow.