experimental backend WebGL2 · not opted in
Enable WebGPU controls in settings. WebGL2 remains unchanged. preview clip · live controls are optional this browser needs the main-thread fallback, which may freeze the page.
1.15
1.25
1.00
1.00
0.72
0.05
rgb
/*{ "DESCRIPTION": "Ember-tinted smoke ribbons with layered emitters, buoyant curl, and persistent afterimages.", "INPUTS": [ { "NAME": "flowSpeed", "TYPE": "float", "DEFAULT": 1.15, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "turbulence", "TYPE": "float", "DEFAULT": 1.25, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "lift", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "dissipation", "TYPE": "float", "DEFAULT": 0.9986, "MIN": 0.97, "MAX": 0.9998 }, { "NAME": "emberRate", "TYPE": "float", "DEFAULT": 0.72, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "emitterSize", "TYPE": "float", "DEFAULT": 0.052, "MIN": 0.015, "MAX": 0.14 }, { "NAME": "smokeColor", "TYPE": "color", "DEFAULT": [0.34, 0.58, 1.0, 1.0] } ], "PASSES": [ { "TARGET": "smokeBuffer", "PERSISTENT": true, "FLOAT": true }, {} ] }*/ #define TAU 6.28318530718 vec2 velocityField(vec2 uv, float t) { vec2 p = uv - 0.5; p.x *= RENDERSIZE.x / RENDERSIZE.y; float r = length(p); float angle = atan(p.y, p.x); vec2 v = vec2(sin(TAU * uv.x), -cos(TAU * uv.y)); v += 0.44 * vec2(sin(TAU * (2.0 * uv.y + t * 0.055)), cos(TAU * (2.0 * uv.x - t * 0.071))); v += 0.30 * vec2(cos(angle * 3.0 - t * 0.19), sin(angle * 2.0 + t * 0.23)); v += vec2(0.0, (0.82 + 0.30 * smoothstep(0.0, 0.8, r)) * lift); return v; } float plume(vec2 uv, vec2 center, float size) { vec2 d = uv - center; d.x *= RENDERSIZE.x / RENDERSIZE.y; return 1.0 - smoothstep(0.0, size, length(d)); } void main() { vec2 uv = isf_FragNormCoord; if (PASSINDEX == 0) { float frameStep = min(TIMEDELTA * 60.0, 2.0); float t = TIME * flowSpeed; vec2 vel = velocityField(uv, t) * turbulence; vec2 back = uv - vel * (0.00380 * frameStep * flowSpeed); vec2 historyBack = uv - vel * (0.00100 * frameStep * flowSpeed); vec2 emberBack = uv - vel * (0.00245 * frameStep * flowSpeed); vec2 px = 1.0 / RENDERSIZE; vec4 center = IMG_NORM_PIXEL(smokeBuffer, back); float density = center.r * 0.58; density += IMG_NORM_PIXEL(smokeBuffer, back + vec2(2.0 * px.x, 0.0)).r * 0.105; density += IMG_NORM_PIXEL(smokeBuffer, back - vec2(2.0 * px.x, 0.0)).r * 0.105; density += IMG_NORM_PIXEL(smokeBuffer, back + vec2(0.0, 2.0 * px.y)).r * 0.105; density += IMG_NORM_PIXEL(smokeBuffer, back - vec2(0.0, 2.0 * px.y)).r * 0.105; density *= pow(dissipation, frameStep); float history = IMG_NORM_PIXEL(smokeBuffer, historyBack).g * 0.76; history += IMG_NORM_PIXEL(smokeBuffer, historyBack + vec2(px.x, 0.0)).g * 0.06; history += IMG_NORM_PIXEL(smokeBuffer, historyBack - vec2(px.x, 0.0)).g * 0.06; history += IMG_NORM_PIXEL(smokeBuffer, historyBack + vec2(0.0, px.y)).g * 0.06; history += IMG_NORM_PIXEL(smokeBuffer, historyBack - vec2(0.0, px.y)).g * 0.06; history *= pow(dissipation, frameStep * 0.20); float ember = IMG_NORM_PIXEL(smokeBuffer, emberBack).b * 0.78; ember += IMG_NORM_PIXEL(smokeBuffer, emberBack + px).b * 0.055; ember += IMG_NORM_PIXEL(smokeBuffer, emberBack - px).b * 0.055; ember += IMG_NORM_PIXEL(smokeBuffer, emberBack + vec2(px.x, -px.y)).b * 0.055; ember += IMG_NORM_PIXEL(smokeBuffer, emberBack + vec2(-px.x, px.y)).b * 0.055; ember *= pow(dissipation, frameStep * 0.48); float pulseA = 0.65 + 0.35 * sin(TIME * 2.7); float pulseB = 0.55 + 0.45 * sin(TIME * 1.83 + 1.7); vec2 emitterA = vec2(0.50 + 0.16 * sin(TIME * 0.57), 0.055); vec2 emitterB = vec2(0.30 + 0.12 * sin(TIME * 0.41 + 2.0), 0.045); vec2 emitterC = vec2(0.73 + 0.10 * cos(TIME * 0.33), 0.075); float source = plume(uv, emitterA, emitterSize) * pulseA; source += plume(uv, emitterB, emitterSize * 0.68) * pulseB * 0.72; source += plume(uv, emitterC, emitterSize * 0.52) * (1.0 - pulseA) * 0.62; source *= 0.78 + 0.22 * sin(uv.x * 47.0 + TIME * 4.0); density += source * 0.060 * frameStep; float thermal = plume(uv, emitterA + vec2(0.0, 0.020), emitterSize * 0.46); thermal += 0.65 * plume(uv, emitterC + vec2(0.0, 0.014), emitterSize * 0.28); ember += (thermal * (0.34 + 0.40 * sin(TIME * 5.0 + uv.x * 25.0)) + density * 0.030) * emberRate * frameStep; history = max(history, density * 0.50); ember = max(ember, thermal * 0.22); float boundary = smoothstep(0.0, 0.03, uv.x) * smoothstep(0.0, 0.03, 1.0 - uv.x) * smoothstep(0.0, 0.025, uv.y) * smoothstep(0.0, 0.06, 1.0 - uv.y); density *= boundary; history *= boundary; ember *= boundary; // A soft knee preserves wisps but makes every feedback channel finite. float hot = max(density - 0.78, 0.0); density = min(density, 0.78) + hot / (1.0 + 0.62 * hot); float historyHot = max(history - 0.70, 0.0); history = min(history, 0.70) + historyHot / (1.0 + 0.90 * historyHot); float emberHot = max(ember - 0.64, 0.0); ember = min(ember, 0.64) + emberHot / (1.0 + 1.1 * emberHot); gl_FragColor = vec4(clamp(density, 0.0, 1.35), clamp(history, 0.0, 1.20), clamp(ember, 0.0, 1.10), 1.0); } else { vec3 state = IMG_THIS_PIXEL(smokeBuffer).rgb; float density = state.r; float history = state.g; float ember = state.b; vec2 p = isf_FragNormCoord * 2.0 - 1.0; p.x *= RENDERSIZE.x / RENDERSIZE.y; vec3 cool = smokeColor.rgb; vec3 warm = vec3(1.0, 0.22, 0.035); vec3 tint = mix(vec3(0.48, 0.22, 0.14), cool, 0.52 + 0.28 * sin(density * 4.0)); vec3 col = tint * (0.42 * sqrt(max(density, 0.0)) + 2.85 * density * density); vec3 afterglow = mix(vec3(0.10, 0.06, 0.28), cool, 0.44); col += afterglow * (0.48 * sqrt(max(history, 0.0)) + 0.96 * history * history); col += warm * (0.14 * sqrt(max(ember, 0.0)) + 0.98 * ember * ember); float veil = 0.5 + 0.5 * sin(9.0 * p.x + 4.0 * p.y + TIME * 0.35); col += cool * veil * density * 0.055; col *= 1.0 - 0.14 * dot(p, p); col *= 1.65; col = col / (1.0 + col); gl_FragColor = vec4(max(col, 0.0), 1.0); } }
Ember-tinted smoke ribbons with layered emitters, buoyant curl, and persistent afterimages.
by ufffdwitnessed_generationmade with glslopCC0-1.0active·created 2026-07-13 22:44Z103 views0 forks7 paramsmultipass ×2gpt-5 · openai
lineage
forked from
export clip · render a video in your browser
Renders to video in your browser (no upload), stepping the real renderer frame by frame so multipass + feedback come out right. motion auto-animates the shader's own knobs — each export rolls a fresh take (🎲 re-roll, 🔒 to pin one you like). GIF is slower with a tighter length cap; MP4/WebM are faster. Some devices record in real time (noted below if so) — keep the tab visible.
caps: ≤1920 per side, ≤60fps, ≤18000 frames (GIF ≤300)
embed · drop this shader into any page
Renders client-side in the reader's browser (CC0 — no attribution required, but the backlink is appreciated).