3D Volumetric Explosion — Raymarched Smoke and Fire Loop

390 views 0 replies
Live Shader
Loading versions...

This shader pushes the volumetric explosion from the previous fire tutorial into full Hollywood territory. We are talking shockwave rings, flying ember particles, a white-hot initial flash, ground illumination, mushroom cloud formation, ACES tone mapping, and 7-octave domain-warped turbulence for high-detail smoke. The entire effect loops every 5 seconds and the camera orbits the blast, giving you a cinematic view of the detonation from every angle.

The Complete Hollywood Explosion Shader

This standalone WebGL1 fragment shader renders a full cinematic explosion loop. Paste it into any GLSL sandbox with iResolution and iTime uniforms.

precision mediump float;

uniform vec2 iResolution;
uniform float iTime;

// --- Hash and 3D noise ---
float hash(float n) { return fract(sin(n) * 43758.5453); }
float hash2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }

float noise3d(vec3 p) {
    vec3 i = floor(p);
    vec3 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float n = i.x + i.y * 157.0 + i.z * 113.0;
    return mix(
        mix(mix(hash(n), hash(n+1.0), f.x),
            mix(hash(n+157.0), hash(n+158.0), f.x), f.y),
        mix(mix(hash(n+113.0), hash(n+114.0), f.x),
            mix(hash(n+270.0), hash(n+271.0), f.x), f.y), f.z);
}

// --- High-detail 3D FBM (7 octaves, rotated) ---
float fbm3(vec3 p) {
    float v = 0.0, a = 0.5;
    for (int i = 0; i < 7; i++) {
        v += a * noise3d(p);
        p = vec3(p.y*0.8+p.z*0.6, -p.x*0.6+p.z*0.8, p.y*0.6-p.x*0.8) * 2.02 + 0.7;
        a *= 0.49;
    }
    return v;
}

// --- Turbulent FBM for sharp flame wisps ---
float tfbm3(vec3 p) {
    float v = 0.0, a = 0.5;
    for (int i = 0; i < 6; i++) {
        v += a * abs(noise3d(p) * 2.0 - 1.0);
        p = vec3(p.y*0.8+p.z*0.6, -p.x*0.6+p.z*0.8, p.y*0.6-p.x*0.8) * 2.02 + 0.7;
        a *= 0.5;
    }
    return v;
}

// --- Explosion density with multiple layers ---
float explosionDensity(vec3 p, float phase) {
    float r = length(p);
    float t = phase;

    // Primary blast shell — expands and thins out
    float shellR = t * 3.0;
    float shellW = 0.5 + t * 1.2;
    float shell = exp(-pow((r - shellR) / shellW, 2.0));

    // Inner fireball core — intense, shrinks over time
    float core = exp(-r * r * (0.8 + t * 6.0)) * (1.0 - t * 0.5);

    // Secondary debris ring — slightly behind main blast
    float ring2R = t * 2.0;
    float ring2 = exp(-pow((r - ring2R) / 0.4, 2.0)) * 0.5 * (1.0 - t);

    // Mushroom cap — upward bias increases with time
    float mushroom = smoothstep(-1.0, 3.0, p.y + t * 2.5);

    // Multi-layer turbulence with domain warping
    vec3 tp = p * 1.5;
    float scrollUp = -t * 2.0;
    tp.y += scrollUp;

    // First warp layer
    vec3 warp1 = vec3(
        fbm3(tp * 0.4 + vec3(1.7, 9.2, 3.1) + t * 0.2),
        fbm3(tp * 0.4 + vec3(8.3, 2.8, 5.4) + t * 0.15),
        fbm3(tp * 0.4 + vec3(4.1, 6.5, 1.8) + t * 0.18)
    );

    // Evaluate turbulence with warped coordinates
    float turb1 = fbm3(tp + warp1 * 2.0);
    float turb2 = tfbm3(tp * 1.3 + warp1 * 1.5 + t * 0.3);
    float turb = mix(turb1, turb2, 0.45 + 0.1 * sin(t * 3.0));

    // Combine all density components
    float density = (shell * 0.6 + core * 1.2 + ring2) * turb * mushroom;

    // Fade outer boundary
    density *= smoothstep(4.0, 1.8, r);

    // Add fine detail tendrils at the edges
    float tendrils = tfbm3(p * 3.0 + vec3(0.0, scrollUp * 1.5, 0.0)) * 0.3;
    density += tendrils * shell * 0.4;

    return max(density, 0.0);
}

// --- Rich fire/smoke color ramp ---
vec3 fireColor(float temp) {
    temp = clamp(temp, 0.0, 1.0);
    vec3 col = vec3(0.02, 0.02, 0.03); // deep black smoke
    col = mix(col, vec3(0.15, 0.08, 0.05), smoothstep(0.0, 0.08, temp));  // warm smoke
    col = mix(col, vec3(0.5, 0.05, 0.0),  smoothstep(0.05, 0.2, temp));   // deep red
    col = mix(col, vec3(0.9, 0.15, 0.0),  smoothstep(0.15, 0.35, temp));  // red-orange
    col = mix(col, vec3(1.0, 0.5, 0.02),  smoothstep(0.3, 0.5, temp));    // orange
    col = mix(col, vec3(1.0, 0.8, 0.15),  smoothstep(0.45, 0.7, temp));   // yellow
    col = mix(col, vec3(1.0, 0.95, 0.6),  smoothstep(0.65, 0.85, temp));  // white-yellow
    col = mix(col, vec3(1.0, 1.0, 0.95),  smoothstep(0.85, 1.0, temp));   // white hot
    return col;
}

// --- Shockwave distortion ring ---
float shockwave(vec3 ro, vec3 rd, float phase) {
    float swRadius = phase * 6.0;
    float swThick = 0.15;
    // Intersect ray with horizontal ring at y=0
    if (abs(rd.y) < 0.001) return 0.0;
    float t = -ro.y / rd.y;
    if (t < 0.0) return 0.0;
    vec3 hitP = ro + rd * t;
    float dist = length(hitP.xz);
    float ring = exp(-pow((dist - swRadius) / swThick, 2.0));
    return ring * (1.0 - phase) * smoothstep(0.0, 0.15, phase);
}

// --- Ember particles (procedural) ---
float embers(vec3 p, float phase) {
    float sparks = 0.0;
    for (int i = 0; i < 12; i++) {
        float fi = float(i);
        float seed = fi * 7.13 + 1.0;
        // Random direction and speed for each ember
        vec3 dir = normalize(vec3(
            hash(seed) * 2.0 - 1.0,
            hash(seed + 3.0) * 0.8 + 0.3,
            hash(seed + 7.0) * 2.0 - 1.0
        ));
        float speed = 2.0 + hash(seed + 11.0) * 3.0;
        vec3 ePos = dir * speed * phase;
        // Gravity
        ePos.y -= phase * phase * 2.0;
        float d = length(p - ePos);
        float brightness = exp(-d * d * 80.0) * (1.0 - phase);
        sparks += brightness;
    }
    return sparks;
}

void main() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;

    // Loop: 5-second cycle
    float loopTime = mod(iTime, 5.0);
    float phase = loopTime / 5.0;

    // Camera orbits
    float camAngle = iTime * 0.25;
    vec3 ro = vec3(6.0 * cos(camAngle), 2.0 + sin(iTime * 0.15) * 0.5, 6.0 * sin(camAngle));
    vec3 target = vec3(0.0, 1.0 + phase * 0.5, 0.0);
    vec3 fwd = normalize(target - ro);
    vec3 right = normalize(cross(fwd, vec3(0.0, 1.0, 0.0)));
    vec3 up = cross(right, fwd);
    vec3 rd = normalize(fwd + uv.x * right + uv.y * up);

    // --- Volumetric ray march ---
    vec3 color = vec3(0.0);
    float transmittance = 1.0;
    int steps = 80;
    float tMin = 0.1;
    float tMax = 14.0;
    float dt = (tMax - tMin) / float(steps);

    for (int i = 0; i < 80; i++) {
        float t = tMin + (float(i) + 0.5) * dt;
        vec3 pos = ro + rd * t;
        float density = explosionDensity(pos, phase);

        if (density > 0.008) {
            float r = length(pos);

            // Temperature: core is hottest, outer smoke is cold
            float temp = density * exp(-r * 0.35) * (1.4 - phase * 0.8);
            // Boost inner core temperature
            temp += exp(-r * r * 2.0) * (1.0 - phase) * 0.5;
            temp = clamp(temp * 2.0, 0.0, 1.0);

            vec3 emit = fireColor(temp);

            // Emission intensity: fire glows, smoke absorbs
            float emitStr = temp * temp * 4.0;
            // Extra bloom on the hottest parts
            emitStr += pow(temp, 6.0) * 2.0;
            emit *= emitStr;

            float absorption = density * dt * 5.0;

            color += transmittance * emit * absorption;
            transmittance *= exp(-absorption);

            if (transmittance < 0.008) break;
        }
    }

    // --- Ember particles ---
    float emberGlow = embers(ro + rd * 3.0, phase) + embers(ro + rd * 5.0, phase) * 0.5;
    color += vec3(1.0, 0.6, 0.15) * emberGlow * transmittance * 8.0;

    // --- Shockwave ring ---
    float sw = shockwave(ro, rd, phase);
    // Distortion-like brightening at shockwave front
    color += vec3(1.0, 0.85, 0.6) * sw * 1.5 * transmittance;

    // --- Background: dark sky with subtle warm ambient ---
    vec3 bg = mix(vec3(0.01, 0.01, 0.02), vec3(0.04, 0.02, 0.01), max(uv.y + 0.5, 0.0));
    color += transmittance * bg;

    // --- Ground plane with illumination ---
    if (rd.y < -0.005) {
        float groundT = -ro.y / rd.y;
        if (groundT > 0.0 && groundT < 20.0) {
            vec3 gp = ro + rd * groundT;
            float gDist = length(gp.xz);

            // Ground surface with subtle grid
            float grid = smoothstep(0.02, 0.0, abs(fract(gp.x) - 0.5))
                       + smoothstep(0.02, 0.0, abs(fract(gp.z) - 0.5));
            vec3 groundCol = vec3(0.03) + vec3(0.015) * grid * 0.3;

            // Fire illumination on ground
            float fireLight = exp(-gDist * 0.25) * (1.0 - phase * 0.6);
            groundCol += vec3(1.0, 0.4, 0.08) * fireLight * 0.4;

            // Shockwave ring on ground
            float swR = phase * 6.0;
            float swGround = exp(-pow((gDist - swR) / 0.3, 2.0)) * (1.0 - phase);
            groundCol += vec3(1.0, 0.7, 0.3) * swGround * 0.6;

            // Fade ground into scene
            float groundFog = exp(-groundT * 0.06);
            color = mix(color, color + groundCol * transmittance, groundFog);
        }
    }

    // --- Initial flash on detonation ---
    float flash = exp(-phase * 15.0) * 0.8;
    color += vec3(1.0, 0.95, 0.8) * flash;

    // --- HDR tone mapping (ACES approximation) ---
    color *= 1.2;
    vec3 x = color;
    color = (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);
    color = clamp(color, 0.0, 1.0);

    // Gamma
    color = pow(color, vec3(1.0 / 2.2));

    gl_FragColor = vec4(color, 1.0);
}

What Makes It Hollywood

A basic volumetric explosion has a density field and a color ramp. A cinematic explosion layers multiple physical phenomena that happen in a real detonation, each on its own timeline. Here is what this shader stacks together:

Initial flash: Real explosions start with a blinding white flash from the rapid energy release. We simulate this with a simple exponential decay at the start of the loop — exp(-phase * 15.0) — which floods the screen with white light that fades in a fraction of a second.

// Blinding flash on detonation — fades fast
float flash = exp(-phase * 15.0) * 0.8;
color += vec3(1.0, 0.95, 0.8) * flash;

Multi-Layer Density Field

Instead of a single expanding sphere, the density function combines four distinct components that evolve independently over time:

// Primary blast shell — the main expanding fireball
float shell = exp(-pow((r - shellR) / shellW, 2.0));

// Inner core — intense, white-hot center that shrinks
float core = exp(-r * r * (0.8 + t * 6.0)) * (1.0 - t * 0.5);

// Secondary debris ring — trailing behind the main blast
float ring2 = exp(-pow((r - ring2R) / 0.4, 2.0)) * 0.5 * (1.0 - t);

// Mushroom cap bias — pushes density upward over time
float mushroom = smoothstep(-1.0, 3.0, p.y + t * 2.5);

The Gaussian falloffs (exp(-x*x)) give each component soft, natural boundaries. The mushroom cap bias makes the explosion billow upward as hot gas rises, forming the characteristic mushroom cloud shape that defines large explosions. The secondary debris ring adds the trailing column of smoke and fire beneath the main blast.

Domain-Warped Turbulence

The turbulence uses 7 octaves of 3D FBM with a rotation matrix applied between each octave (to eliminate grid artifacts), plus a domain warping pass where one FBM displaces the coordinates of another. This creates the complex, folding, billowing motion you see in real explosions — where pockets of hot gas roll over each other and smoke curls into itself.

// Warp the sampling coordinates with a separate FBM field
vec3 warp1 = vec3(
    fbm3(tp * 0.4 + vec3(1.7, 9.2, 3.1) + t * 0.2),
    fbm3(tp * 0.4 + vec3(8.3, 2.8, 5.4) + t * 0.15),
    fbm3(tp * 0.4 + vec3(4.1, 6.5, 1.8) + t * 0.18)
);

// Regular FBM + turbulent FBM blend for soft/sharp detail mix
float turb1 = fbm3(tp + warp1 * 2.0);
float turb2 = tfbm3(tp * 1.3 + warp1 * 1.5);
float turb = mix(turb1, turb2, 0.45);

The turbulent FBM variant uses abs(noise * 2.0 - 1.0) to create sharp ridge-like features — the wispy tendrils at the edges of the explosion. Blending regular FBM (smooth, billowy) with turbulent FBM (sharp, wispy) at roughly 55/45 gives you both the broad rolling shapes and the fine detail.

Shockwave Ring

Real explosions produce a visible shockwave — a ring of compressed air that expands outward from the blast. In the shader, we intersect the camera ray with the ground plane and draw a Gaussian ring at the expanding shockwave radius.

float shockwave(vec3 ro, vec3 rd, float phase) {
    float swRadius = phase * 6.0;
    float swThick = 0.15;
    float t = -ro.y / rd.y;
    vec3 hitP = ro + rd * t;
    float dist = length(hitP.xz);
    float ring = exp(-pow((dist - swRadius) / swThick, 2.0));
    return ring * (1.0 - phase);
}

The shockwave also appears on the ground plane as an expanding bright ring, which adds to the sense of scale and impact.

Procedural Ember Particles

Flying embers and sparks sell the effect. Instead of a particle system, we place 12 procedural point lights at positions computed from hash-based random directions. Each ember flies outward from the blast center with a unique speed and direction, with gravity pulling it downward over time.

for (int i = 0; i < 12; i++) {
    vec3 dir = normalize(vec3(
        hash(seed) * 2.0 - 1.0,
        hash(seed + 3.0) * 0.8 + 0.3,
        hash(seed + 7.0) * 2.0 - 1.0
    ));
    float speed = 2.0 + hash(seed + 11.0) * 3.0;
    vec3 ePos = dir * speed * phase;
    ePos.y -= phase * phase * 2.0; // gravity arc
    float d = length(p - ePos);
    sparks += exp(-d * d * 80.0) * (1.0 - phase);
}

The exp(-d*d*80.0) creates tight, bright point-like glows. The gravity term phase * phase * 2.0 gives each ember a parabolic arc — they shoot upward and outward, then curve back down, exactly like burning debris in a real explosion.

Ground Illumination

The ground plane receives dynamic lighting from the explosion. We compute the fire illumination as an exponential falloff from the blast center, tinted orange-red. The shockwave ring also appears on the ground as a bright expanding circle. A subtle grid pattern on the ground adds spatial reference so you can see the scale of the blast.

ACES Tone Mapping

The raw HDR values from the volumetric accumulation easily exceed the 0-1 display range. Instead of the simple Reinhard operator, this shader uses the ACES (Academy Color Encoding System) filmic tone mapping curve used in film and AAA games. It preserves more color saturation in the highlights while rolling off naturally to white.

// ACES filmic tone mapping approximation
vec3 x = color;
color = (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);
color = clamp(color, 0.0, 1.0);

Performance Notes

This shader is heavy — 80 volumetric steps, each evaluating multiple 7-octave FBMs with domain warping. On a discrete GPU it runs smoothly, but on integrated graphics or mobile you will want to reduce steps to 40, octaves to 5, and remove the ember particle loop. The early exit on low transmittance saves significant work for rays that pass through the dense core, but edge rays that skim through thin smoke still pay the full cost.

Moonjump
Forum Search Shader Sandbox
Sign In Register