Candle
A small flickering flame with a blue base and a warm halo, drawn as light over black.
- 01GoalElements
- 02TechniqueFire & flames
- 03ExecutionCandle
Parameters
Copy for your platform
- GLSLCopy
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
fbm()Texture2Dfbm()fbm()fbm() UVTime01 Rising noise
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In02 Teardrop mask
clampClampclampclampclamp In03 Heat
mixMixmixmixmix In04 Hot palette
*Multiply*** In05 Halo and intensity
How it works
A candle flame is the simplest fire to look at, and that makes it a good one to build: one tongue, mostly still at the bottom, restless at the tip. The card makes it from a teardrop mask, two noises and a palette, and writes the result as emission: light, over black.
Step by step
- Rising noise. An fBmA sum of several copies of a noise, each at a higher frequency and a lower strength than the last, so the result has detail at many sizes at once, like clouds, smoke or terrain.Fundamental · Noise
Fractal Brownian motion (fBm)
A sum of several copies of a noise, each at a higher frequency and a lower strength than the last, so the result has detail at many sizes at once, like clouds, smoke or terrain.
fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p)Sourcesof four octavesOne layer of noise in a fractal sum. Each octave usually has twice the frequency and half the amplitude of the one before, adding finer and fainter detail.FundamentalOctave
One layer of noise in a fractal sum. Each octave usually has twice the frequency and half the amplitude of the one before, adding finer and fainter detail.
fbm(p) = Σ gainᵏ · noise(lacunarityᵏ · p)Sourcesreads a coordinate whoseysubtractstime * speed, so the noise pattern travels upward through the flame like hot gas; the preview shows that value, around middle gray. Minus 0.5 it becomes the turbulence, centered on zero. A second, one-dimensional fBm of time alone gives the slow side-to-side sway of the whole flame. - Teardrop mask.
riseruns from 0 at the wick to 1 at the tip. The half-width of the flame at each height iswidth * sqrt(rise) * (1 - rise): the square root rounds the bottom, the1 - risecloses the top to a point, and the widest part lands a third of the way up, where a candle flame bulges. Both the sway and the turbulence move the pixel’sxbefore it is compared with that width, and both are multiplied by the height (the sway by its square), so the base stays on the wick and the tip moves most. A smoothstepA soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between.FundamentalSmoothstep
A soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between.
t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)Sourcesfrom half the width to the full width makes the edge soft.- smoothstep (opens in a new tab)Reference
- Smoothstep (opens in a new tab)Reference
- Heat. Inside the mask, heat is highest in the middle and near the bottom of the flame, and falls toward the edge and the tip. The turbulence is added in, so the bands of heat break into tongues instead of staying smooth ovals. Right at the wick the heat fades in over the first fifth of the height, since the wax vapor there hasn’t caught fire yet.
- Hot palette.
hotPalette()turns heat into color with three chained mixesBlends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.FundamentalLinear interpolation (mix)
Blends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.
mix(a, b, t) = a (1 − t) + b tSources: from a dark version of Outer color to the full outer color, then to Inner color, then to near white. It is the order a hot object glows through, red, orange, yellow, white, and because one number drives it, the colors can never come out in the wrong order. Where the heat is low near the wick, the color is blended toward Base color: the faint blue at the bottom of a real candle flame, where the vapor burns with the most air and makes little soot.- mix (opens in a new tab)Reference
- Linear interpolation (opens in a new tab)Reference
- Halo and intensity. A soft glow,
expof the distance to the flame’s center, adds some of the outer color around the flame, the light it throws on the air. Everything is multiplied by Intensity.
Over black, then over a scene
The card’s Blending Combining the color a fragment shader outputs with the color already in the framebuffer, instead of replacing it. Transparency, additive glow and multiply all come from it.render.json says "output": "emission": the result is light the surface gives off, not a base color. The
preview draws it opaque over its own black, which is how this step of the learning order shows fire. Over a real scene
the same shader goes on a quad with additive blendingCombining the color a fragment shader outputs with the color already in the framebuffer, instead of replacing it. Transparency, additive glow and multiply all come from it.result = src × srcFactor + dst × dstFactor: black adds nothing, so the quad’s
square edges vanish, and the halo brightens what is behind it. In Godot that is render_mode blend_add; on the
canvas_item shader (the Godot guide shows it), and on the web gl.blendFunc(gl.ONE, gl.ONE). With Intensity above
1, an HDR pipeline with bloom and the clamp removed from the copied main(), the core glows past white.
Cost
Two fBm calls of four octaves are eight octaves of value noise, each four integer hashes and three mixes, about 95
operations, so about 760. The mask, the heat, the palette’s three smoothsteps and mixes, the sqrt, the exp and the
length of the halo add about 90. About 850 operations per pixel, no texture reads, one pass. A candle covers a few
hundred pixels, so this is nothing; a wall of fire covering the screen is a different matter, and that is where a
flipbook or a scrolled noise texture wins.
Why it looks like fire
Three cues sell it, and each is a separate line of the shader. Motion goes up and grows with height (the turbulence
scroll and the rise factor). The color follows heat in the right order (the palette). The flame adds light rather
than covering (emission, then additive blending). Break any one of them, a sway that moves the base, a palette that
goes yellow at the edges, a flame drawn with alpha over a bright background, and it stops reading as fire long before
anyone notices the noise itself.
Make it in GLSL
The copied shader is a complete GLSL ES 3.00 fragment shader: the helpers, effect(), the uniforms and a main() that
fills Inputs and writes the color encoded to sRGB.
- Draw a quad whose UV runs 0 to 1 from the bottom-left, passed as
v_uv. The flame stands at the center, from 20% of the height up. - Every frame, set
u_timeto seconds (performance.now() / 1000);u_resolutionto the canvas size. - Set the params once: the floats as they are, the three colors converted from sRGB to linear.
Over black, as on the card, draw it opaque. Over a scene, draw the quad after everything opaque, with depth writes off and additive blending:
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
gl.depthMask(false);
The shader writes alpha 1, so ONE, ONE adds its color as is. Black adds nothing, so the quad’s edges vanish and only
the flame and its halo show. For a candle in 3D, turn the quad to face the camera every frame (a billboard), and give
each candle a different time offset so they don’t flicker in sync.
Make it in Godot
The export draws the flame opaque over black, like the card. Over a scene, add blend_add to its render_mode line (render_mode unshaded, blend_add;). In VisualShader, the rising noise is a Texture2D node reading a NoiseTexture2D with UV plus Time, and the palette is a GradientTexture1D read by the heat.
The export is a canvas_item shader, for a flame drawn in 2D.
- Add a
ColorRect(or aSprite2Dwith any texture) where the candle is. The flame is shaped in UV space and stretches with the rect: at the preview’s proportions, 1.6 times wider than tall (for example 160 by 100 pixels), it looks as it does on the site. It stands in the middle, from 20% of the height up; on a narrower rect, raise Width. - In its Material slot, create a New ShaderMaterial, then a New Shader, and paste the copied code.
- To draw it over the rest of the scene instead of over black, change the render mode line to
render_mode unshaded, blend_add;. Godot then adds the flame’s color to what is behind, and the black corners of the rect disappear.
The shader flips Godot’s top-left UV, so the flame points up as on the site. For a candle in a 3D scene, the same body
works in a spatial shader on a QuadMesh with billboard mode on and blend_add, unshaded in its render mode; the
generated code here is the 2D one, so that is an adaptation by hand.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Fire & flames → Candle, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_height; // Height: 0.6 (0.2 to 0.8)uniform float p_width; // Width: 0.25 (0.05 to 0.6)uniform float p_speed; // Flicker speed: 2.5 (0.0 to 6.0)uniform float p_intensity; // Intensity: 1.2 (0.0 to 3.0)uniform vec3 p_outer; // Outer color: vec3(1.0, 0.42, 0.08), sRGB; upload it decoded to linearuniform vec3 p_inner; // Inner color: vec3(1.0, 0.82, 0.35), sRGB; upload it decoded to linearuniform vec3 p_baseColor; // Base color: vec3(0.12, 0.25, 0.85), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// Integer bit mixing instead of the one-line sine hash: it gives the same value on every GPU, and the rising// coordinates can grow for hours without the noise turning into stripes.uint scramble(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { return float(scramble(uint(cell.x) ^ scramble(uint(cell.y)))) / 4294967295.0;}float valueNoise(vec2 p) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); float a = hashCell(cell); float b = hashCell(cell + ivec2(1, 0)); float c = hashCell(cell + ivec2(0, 1)); float d = hashCell(cell + ivec2(1, 1)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}// Four octaves are enough: the flame is small and the detail only has to read as flickerfloat fbm(vec2 p) { float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 4; k++) { sum += amplitude * valueNoise(p); p = p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}// From cool to hot: dark red, the outer color, the inner color, then near white, each taking over a third of the rangevec3 hotPalette(float t) { vec3 color = mix(p_outer * 0.15, p_outer, smoothstep(0.0, 0.35, t)); color = mix(color, p_inner, smoothstep(0.3, 0.7, t)); return mix(color, vec3(1.0, 0.97, 0.9), smoothstep(0.75, 1.0, t));}vec4 effect(Inputs i) { // 0 at the wick, 1 at the tip float rise = (i.uv.y - 0.2) / p_height; vec2 scroll = vec2(i.uv.x * 6.0, i.uv.y * 4.0 - i.time * p_speed); float rising = fbm(scroll); float turbulence = rising - 0.5; // The tip is light and moves a lot; the base is anchored to the wick, so the sway grows with the square of the height float sway = (fbm(vec2(i.time * 0.7, 3.1)) - 0.5) * 0.12 * rise * rise; float x = i.uv.x - 0.5 - sway - turbulence * 0.08 * rise; // A teardrop: sqrt(rise) rounds the bottom and (1 - rise) closes the top to a point float halfWidth = p_width * sqrt(max(rise, 0.0)) * max(1.0 - rise, 0.0); float edge = abs(x) / max(halfWidth, 0.0001); float flame = (1.0 - smoothstep(0.5, 1.0, edge)) * step(0.0, rise) * step(rise, 1.0); // Hottest a little above the wick and in the middle, cooling toward the edge and the tip; the noise breaks the bands // into tongues. Right at the wick the vapor hasn't caught yet, so the heat fades in over the first fifth. float heat = flame * smoothstep(0.0, 0.2, rise) * clamp(1.1 - edge * 0.6 - rise * 0.8 + turbulence * 0.6, 0.0, 1.0); vec3 fire = hotPalette(heat) * flame; // Near the wick a real flame burns blue, where the vapor gets the most air float base = flame * (1.0 - smoothstep(0.0, 0.15, rise)) * (1.0 - heat * heat); fire = mix(fire, p_baseColor, base); float halo = exp(-length(vec2(i.uv.x - 0.5, (i.uv.y - 0.2 - p_height * 0.4) * 0.6)) * 9.0) * 0.25; vec3 glow = (fire + p_outer * halo) * p_intensity; return vec4(glow, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Fire & flames → Candle, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float height : hint_range(0.2, 0.8, 0.01) = 0.6;uniform float width : hint_range(0.05, 0.6, 0.01) = 0.25;uniform float speed : hint_range(0.0, 6.0, 0.1) = 2.5;uniform float intensity : hint_range(0.0, 3.0, 0.05) = 1.2;uniform vec3 outer : source_color = vec3(1.0, 0.42, 0.08);uniform vec3 inner : source_color = vec3(1.0, 0.82, 0.35);uniform vec3 baseColor : source_color = vec3(0.12, 0.25, 0.85);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// Integer bit mixing instead of the one-line sine hash: it gives the same value on every GPU, and the rising// coordinates can grow for hours without the noise turning into stripes.uint scramble(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}float hashCell(ivec2 cell) { return float(scramble(uint(cell.x) ^ scramble(uint(cell.y)))) / 4294967295.0;}float valueNoise(vec2 p) { ivec2 cell = ivec2(floor(p)); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); float a = hashCell(cell); float b = hashCell(cell + ivec2(1, 0)); float c = hashCell(cell + ivec2(0, 1)); float d = hashCell(cell + ivec2(1, 1)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);}// Four octaves are enough: the flame is small and the detail only has to read as flickerfloat fbm(vec2 p) { float sum = 0.0; float amplitude = 0.5; for (int k = 0; k < 4; k++) { sum += amplitude * valueNoise(p); p = p * 2.0 + vec2(17.3, 9.1); amplitude *= 0.5; } return sum;}// From cool to hot: dark red, the outer color, the inner color, then near white, each taking over a third of the rangevec3 hotPalette(float t) { vec3 color = mix(u_toLinear(outer) * 0.15, u_toLinear(outer), smoothstep(0.0, 0.35, t)); color = mix(color, u_toLinear(inner), smoothstep(0.3, 0.7, t)); return mix(color, vec3(1.0, 0.97, 0.9), smoothstep(0.75, 1.0, t));}vec4 effect(Inputs i) { // 0 at the wick, 1 at the tip float rise = (i.uv.y - 0.2) / height; vec2 scroll = vec2(i.uv.x * 6.0, i.uv.y * 4.0 - i.time * speed); float rising = fbm(scroll); float turbulence = rising - 0.5; // The tip is light and moves a lot; the base is anchored to the wick, so the sway grows with the square of the height float sway = (fbm(vec2(i.time * 0.7, 3.1)) - 0.5) * 0.12 * rise * rise; float x = i.uv.x - 0.5 - sway - turbulence * 0.08 * rise; // A teardrop: sqrt(rise) rounds the bottom and (1 - rise) closes the top to a point float halfWidth = width * sqrt(max(rise, 0.0)) * max(1.0 - rise, 0.0); float edge = abs(x) / max(halfWidth, 0.0001); float flame = (1.0 - smoothstep(0.5, 1.0, edge)) * step(0.0, rise) * step(rise, 1.0); // Hottest a little above the wick and in the middle, cooling toward the edge and the tip; the noise breaks the bands // into tongues. Right at the wick the vapor hasn't caught yet, so the heat fades in over the first fifth. float heat = flame * smoothstep(0.0, 0.2, rise) * clamp(1.1 - edge * 0.6 - rise * 0.8 + turbulence * 0.6, 0.0, 1.0); vec3 fire = hotPalette(heat) * flame; // Near the wick a real flame burns blue, where the vapor gets the most air float base = flame * (1.0 - smoothstep(0.0, 0.15, rise)) * (1.0 - heat * heat); fire = mix(fire, u_toLinear(baseColor), base); float halo = exp(-length(vec2(i.uv.x - 0.5, (i.uv.y - 0.2 - height * 0.4) * 0.6)) * 9.0) * 0.25; vec3 glow = (fire + u_toLinear(outer) * halo) * intensity; return vec4(glow, 1.0);}void fragment() { Inputs i; // UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = 1.0 / SCREEN_PIXEL_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.
Further reading
- Flame (opens in a new tab)Reference
- Black-body radiation (opens in a new tab)Reference
- Palettes (opens in a new tab)Article
- Fractal Brownian Motion (opens in a new tab)Book
- smoothstep (opens in a new tab)Reference
- mix (opens in a new tab)Reference
- CanvasItem shaders (opens in a new tab)Docs
- Shading language (opens in a new tab)Docs