Live parameters
Motion
1.00
1.00
20.0
Light
1.00
1.00
Color
GLSL source
// "The Last Breath"
// By Noztol
// Features:
// - Volumetric additive flame
// - Realistic "Struggle/Sputter" phase before death
// - Flame violently shrinks, shakes, and flashes
// - Specular point-lighting & Procedural smoke
#define MAX_STEPS 100
#define SURF_DIST 0.002
#define MAX_DIST 15.0
#define S(x,y,z) smoothstep(x,y,z)
#ifndef TIME_SCALE
#define TIME_SCALE 1.0
#endif
#ifndef INTENSITY
#define INTENSITY 1.0
#endif
#ifndef SHAKE_AMOUNT
#define SHAKE_AMOUNT 1.0
#endif
#ifndef AURA_STRENGTH
#define AURA_STRENGTH 1.0
#endif
#ifndef CYCLE_LENGTH
#define CYCLE_LENGTH 20.0
#endif
#ifndef FLAME_TINT
#define FLAME_TINT vec3(1.0)
#endif
#ifndef LIGHT_TINT
#define LIGHT_TINT vec3(1.0, 0.7, 0.3)
#endif
const float pi = 3.141592653589793238;
const float twopi = 6.283185307179586;
// --- Utilities ---
float Hash21(vec2 p) {
p = fract(p*vec2(123.34, 456.21));
p += dot(p, p+45.32);
return fract(p.x*p.y);
}
float Noise3D(vec3 p) {
vec3 s = vec3(7, 157, 113);
vec3 ip = floor(p); p -= ip;
vec4 h = vec4(0., s.yz, s.y+s.z) + dot(ip, s);
p = p*p*(3.-2.*p);
h = mix(fract(sin(h)*43758.5453), fract(sin(h+s.x)*43758.5453), p.x);
h.xy = mix(h.xz, h.yw, p.y);
return mix(h.x, h.y, p.z);
}
float fbm(vec3 p, int octaves) {
float total = 0.0;
float amplitude = 1.0;
float frequency = 1.0;
for (int i = 0; i < octaves; i++) {
total += Noise3D(p * frequency) * amplitude;
frequency *= 2.0;
amplitude *= 0.5;
}
return total / 2.0;
}
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
// --- Geometry ---
// 1. The Solid Candle & Wick
float GetDist(vec3 p) {
float cRadius = 0.7;
float candle = length(p.xz) - cRadius;
float a = atan(p.x, p.z);
float drips = max(0.0, sin(a * 20.0));
float bump = max(drips * 0.7 * sin(p.y * 5.0) - p.y, 0.0) * 0.04;
vec3 q = p * 12.0;
float melt = sin(p.y)*(sin(q.x+sin(q.y+sin(q.z*pi)))+sin(q.x*2.+q.z+q.y)) * 0.5;
bump += melt * S(0.2, -2.5, p.y) * 0.15;
candle -= bump;
candle = max(candle, p.y - 0.0);
candle = max(candle, -p.y - 2.5);
float crater = length(p.xz) - 0.55;
crater = max(crater, abs(p.y + 0.01) - 0.06);
candle = max(candle, -crater);
float wick = length(p.xz) - 0.025;
wick = max(wick, p.y - 0.25);
wick = max(wick, -p.y + 0.05);
return min(candle, wick);
}
// 2. The Flame SDF (With Struggle Parameters)
float GetFlameDist(vec3 p, float life, float struggle) {
float height = 0.21;
float flameHeight = 0.35 * life;
float smth = 0.06;
float baseSize = 0.11 * life * (0.95 + 0.05 * Noise3D(vec3(iTime*15.0)));
// Violent horizontal shaking that activates during the struggle phase
float shake = (Noise3D(vec3(iTime * 40.0, p.y * 10.0, 0.0)) - 0.5) * 0.15 * struggle * SHAKE_AMOUNT;
float wave = sin(p.y * 8.0 - iTime * 6.0) * 0.02 * life + shake;
float d = MAX_DIST;
for(float i = 0.0; i < 1.0; i += 0.1) {
float fH = height + (i * flameHeight);
float size = baseSize * mix(0.5, 0.01, i);
float fX = wave * i * i;
vec3 spherePos = vec3(fX, fH, 0.0);
float sphereD = length(p - spherePos) - size;
d = smin(d, sphereD, smth);
}
return d;
}
vec3 GetNormal(vec3 p) {
vec2 e = vec2(0.01, 0);
vec3 n = GetDist(p) - vec3(GetDist(p-e.xyy), GetDist(p-e.yxy), GetDist(p-e.yyx));
return normalize(n);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
vec2 m = iMouse.xy / iResolution.xy;
vec3 col = vec3(0);
if (length(iMouse.xy) < 10.0) m = vec2(0.5);
// sputter logic
float Tlife = iTime * TIME_SCALE;
float cycle = mod(Tlife, CYCLE_LENGTH);
// 1. Hard cutoff for when the flame is officially dead
float baseLife = S(15.2, 15.0, cycle);
// 2. The Struggle Phase (Starts at 12s, max panic at 15s)
float struggle = S(12.0, 15.0, cycle);
// 3. Size Shrinking (Drops to 15% size as it fights to stay lit)
float shrink = mix(1.0, 0.15, pow(struggle, 3.0));
// 4. Violent Flicker (High-frequency noise that cuts INTO the life variable)
float chaosFreq = mix(15.0, 60.0, struggle);
float chaosAmp = mix(0.02, 0.8, pow(struggle, 2.0));
float chaos = Noise3D(vec3(iTime * chaosFreq, 0.0, 0.0)) * chaosAmp;
// Combine: Shrink it, subtract the violent chaos, and apply the final kill switch
float life = max(0.0, (shrink - chaos)) * baseLife;
// Death trigger for Smoke
float deathTrigger = S(14.5, 15.5, cycle);
// --- Camera Rotation ---
float turn = (0.5 - m.x) * twopi + iTime * 0.05;
float s = sin(turn);
float c = cos(turn);
mat3 rotY = mat3(c, 0., s, 0., 1., 0., -s, 0., c);
vec3 ro = vec3(0., 0.8, -3.0) * rotY;
vec3 lookAt = vec3(0.0, 0.15, 0.0);
vec3 f = normalize(lookAt - ro);
vec3 r_cam = cross(vec3(0, 1, 0), f);
vec3 u_cam = cross(f, r_cam);
vec3 center = ro + f * 1.5;
vec3 intersect = center + r_cam * uv.x + u_cam * uv.y;
vec3 rd = normalize(intersect - ro);
// Raymarch 1: Solid Objects
float dSolid = 0.0;
for(int i=0; i<MAX_STEPS; i++) {
vec3 p = ro + rd * dSolid;
float dS = GetDist(p);
dSolid += dS;
if(dSolid > MAX_DIST || abs(dS) < SURF_DIST) break;
}
// the Candle & Wick
if(dSolid < MAX_DIST) {
vec3 p = ro + rd * dSolid;
vec3 n = GetNormal(p);
bool isWick = (p.y > 0.05 && length(p.xz) < 0.05);
if (isWick) {
col = vec3(0.05, 0.02, 0.02);
// Ember glows bright red right as the struggle peaks, then slowly fades
float emberFade = max(0.0, cycle - 15.0);
float ember = S(0.2, 0.25, p.y) * S(13.0, 15.0, cycle) * exp(-emberFade * 1.5);
col += vec3(1.0, 0.2, 0.0) * ember;
} else {
vec3 lightPos = vec3(0.0, 0.35, 0.0);
vec3 lDir = normalize(lightPos - p);
float diff = max(dot(n, lDir), 0.0);
float spec = pow(max(dot(reflect(-lDir, n), -rd), 0.0), 16.0);
float sss = exp(-abs(p.y + 0.1) * 3.0) * S(0.7, 0.0, length(p.xz));
vec3 waxCol = vec3(0.5, 0.1, 0.05);
vec3 lightCol = LIGHT_TINT * life; // Light strobes violently with flame life
col = waxCol * 0.1;
col += waxCol * diff * lightCol * 1.5;
col += vec3(1.0, 0.9, 0.8) * spec * lightCol * 0.8;
col += vec3(0.8, 0.2, 0.05) * sss * life * 0.5;
// Subtle ambient flicker
col *= (0.95 + 0.05 * sin(iTime * 30.0 + chaos * 10.0));
}
}
// Volumetric Flame
if (life > 0.0) {
vec3 flameColAcc = vec3(0.0);
float dFlame = 0.0;
for(int i=0; i<45; i++) {
vec3 p = ro + rd * dFlame;
if(dSolid < MAX_DIST && dFlame > dSolid) break;
// Pass the struggle variable to cause horizontal shaking
float dF = GetFlameDist(p, life, struggle);
float glowDensity = exp(-max(dF, 0.0) * 40.0) * 0.08;
float hRel = p.y - 0.21;
vec3 colorGrad = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 0.4, 0.0), S(-0.02, 0.05, hRel));
colorGrad = mix(colorGrad, vec3(1.0, 0.9, 0.4), S(0.05, 0.2, hRel));
colorGrad = mix(colorGrad, vec3(1.0, 0.4, 0.0), S(0.2, 0.4, hRel));
flameColAcc += colorGrad * glowDensity;
dFlame += max(dF * 0.5, 0.015);
if(dFlame > MAX_DIST) break;
}
col += flameColAcc * FLAME_TINT * INTENSITY;
vec3 flameCenter = vec3(0.0, 0.35, 0.0);
float aura = 0.02 / (length(uv - vec2(0.0, 0.1)) + 0.01);
col += vec3(1.0, 0.5, 0.1) * aura * life * 0.2 * AURA_STRENGTH; // Aura dims as life drops
}
// smoke
if(deathTrigger > 0.0) {
vec2 smokeUV = uv;
smokeUV.y -= iTime * 0.15;
float vortex = Noise3D(vec3(uv * 1.5, iTime * 0.2)) * 0.8;
vec2 warpedUV = smokeUV;
warpedUV.x += (vortex - 0.4) * 0.12 * S(-0.2, 0.6, uv.y);
warpedUV.y += Noise3D(vec3(warpedUV.x * 2.0, warpedUV.y * 3.0, iTime * 0.1)) * 0.1;
float smokeDetail = fbm(vec3(warpedUV * 8.0, iTime * 0.4), 4);
float smokeShape = S(0.1, 0.0, abs(uv.x) + smokeDetail * 0.15)
* S(-0.2, 0.5, uv.y)
* exp(-uv.y * 1.5);
col += vec3(0.35, 0.35, 0.4) * deathTrigger * (1.0 - life) * S(0.0, 0.5, smokeShape);
}
col *= 1.0 - length(uv) * 0.6; // Vignette
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}