Gallery

The Last Breath

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);
}