Raking through the embers

September 26, 2017

Let’s build up the “Juhannus Embers” GLSL fragment shader step-by-step to see how wandering particles emerge from an almost stateless system.

We start with a simple field of triangle wave oscillations between 0 and 1 along the horizontal (x) and vertical (y) axes.

By applying smoothstep functions in both directions, and multiplying those, we immediatly get anti-aliased boxes, the basis of our particles.

void main()
{
    // Fit 4 x 4 repetitions to the frame
    vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); 
    a.y -= time; // Slowly move upwards
    a.x += 0.5;  // Shift sideways by half
    vec2 f = fract(a);
    f += f - 1.;
    f = abs(f); // Triangle wave
    // EITHER 1st/Left - view field as colour
    gl_FragColor = vec4(f,0.,1.);
    // OR 2nd/Right - create anti-aliased boxes
    float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y);
    gl_FragColor = vec4(e,e,e,1.);
}
void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); gl_FragColor = vec4(f,0.,1.); } void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; a.x += 0.5; // ? vec2 f = fract(a); f += f - 1.; f = abs(f); float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y); gl_FragColor = vec4(e,e,e,1.); }

Next we give the columns of boxes their own vertical speed by creating a unique integer value for each row and column that can be used to modify all the pixels in that region.

    ...
    a.y -= time;
    vec2 i = floor(a); // Create a 2D integer for each box
    a.y -= time*fract(abs(i.x*.712)); // Columns have different speed
    a.x += 0.5; 
    ...
void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); gl_FragColor = vec4(f,0.,1.); } void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y); gl_FragColor = vec4(e,e,e,1.); }

To make it more interesting, we add a function based on time and our per-column integer, to make the columns subtly change speed.

    ...
    vec2 i = floor(a);
    float it = fract(time * (i.x+10.)*.03);
    it += it - 1.;
    a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Columns change speed
    a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers"
    ...
void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); gl_FragColor = vec4(f,0.,1.); } void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.x += 0.5; // ? vec2 f = fract(a); f += f - 1.; f = abs(f); float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y); gl_FragColor = vec4(e,e,e,1.); }

Now we apply another modifier to change the vertical scale of each column.

    ...
    a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers"
    a.y *= .5+.5*fract(i.x*.5); // Variable column scale
    a.y *= 2.;
    a.x += 0.5; 
    ...
void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); gl_FragColor = vec4(f,0.,1.); } void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y); gl_FragColor = vec4(e,e,e,1.); }

To break away from simple vertical columns, we need to distort the original field before we generate our base triangle waves.

We do this with another pair of triangle waves derived from the vertical position and the current time. This slowly distorts the columns.

    ...
    a.y -= time;
    float w = fract(time*.3+.9*a.y*.1);
    w += w - 1.;
    w *= .1;
    float c = fract(a.y*.5);
    c += c - 1.;
    c *= .2;
    a.x += a.y * abs(w) * .3 * abs(c);
    vec2 i = floor(a);
    ...
void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; float w = fract(time*.3+.9*a.y*.1); w += w - 1.; w *= .1; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; // ? vec2 f = fract(a); f += f - 1.; f = abs(f); gl_FragColor = vec4(f,0.,1.); } void main() { vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; float w = fract(time*.3+.9*a.y*.1); w += w - 1.; w *= .1; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; vec2 f = fract(a); f += f - 1.; f = abs(f); float e = smoothstep(.7,.9,f.x)*smoothstep(.5,.7,f.y); gl_FragColor = vec4(e,e,e,1.); }

Next we reduce and vary the sizes of our boxes, and allow the density to change over time.

    // Create slowly time varying density value
    float density = .5+.5*abs(fract(time*.01)*2.-1.);
    ...
    a.x += 0.5; 
    vec2 i2 = floor(a+.5);
    vec2 f = fract(a);
    ---
    f = abs(f);
    // Draw only a subset of boxes based on current density value
    float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232));
    // Small boxes that vary in size by column
    float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y);
    gl_FragColor = vec4(f,0.,1.);
    ...
void main() { float density = .5+.5*abs(fract(time*.01)*2.-1.); vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; float w = fract(time*.3+.9*a.y*.1); w += w - 1.; w *= .1; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; // ? vec2 i2 = floor(a+.5); vec2 f = fract(a); f += f - 1.; f = abs(f); float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232)); float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y); gl_FragColor = vec4(f,0.,1.); } void main() { float density = .5+.5*abs(fract(time*.01)*2.-1.); vec2 a = gl_FragCoord.xy * vec2(4.0/256.0); a.y -= time; float w = fract(time*.3+.9*a.y*.1); w += w - 1.; w *= .1; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(time * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= time*fract(abs(i.x*.712)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; // ? vec2 i2 = floor(a+.5); vec2 f = fract(a); f += f - 1.; f = abs(f); float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232)); float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y); gl_FragColor = vec4(e,e,e,1.); }

Finally we add colour, a few more small distortions, and give some key parameters names to get the final result.

void main()
{
    float scale = .007;
    float rise_speed = 100.0;
    float density = .5+.5*abs(fract(time*.01)*2.-1.);
    float layer_speed_scale = 1.0;
    vec2 uv = gl_FragCoord.xy * vec2(2.,2.);
    vec2 x = uv;
    x *= .001;
    uv.y -= time*rise_speed;
    vec2 a = uv * scale;
    float s = fract(x.y*.5);
    s += s - 1.;
    float w = fract(time*.3+.9*a.y*.1);
    w += w - 1.;
    w *= 1.;
    float c = fract(a.y*.5);
    c += c - 1.;
    c *= .2;
    a.x += a.y * abs(w) * .3 * abs(c);
    vec2 i = floor(a);
    float it = fract(time * (i.x+10.)*.03);
    it += it - 1.;
    a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern
    a.y -= time*layer_speed_scale*fract(abs(i.x*.312)); // Multi speed "layers"
    a.y *= .5+.5*fract(i.x*.5);
    a.y *= 2.;
    a.x += 0.5;  
    vec2 i2 = floor(a+.5);
    vec2 f = fract(a);
    f += f - 1.;
    f = abs(f);
    float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232));
    float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y);
    vec3 col = mix(vec3(1.,.8,.0),vec3(.5,.05,.05),clamp(x.y*2.+1.+fract(i.x*.3),0.,1.));
    vec3 bg = mix(vec3(.03,.01,.08),vec3(.0,.0,.0),x.y*5.);
    //e = f.y; // Uncomment to see underlying field structure
    col = mix(bg,e*col,e);
    gl_FragColor = vec4(col,1.);
}
void main() { float iTime = time; vec2 iResolution = size; float scale = .007; float rise_speed = 100.0; float density = .5+.5*abs(fract(iTime*.01)*2.-1.); float layer_speed_scale = 1.0; vec2 uv = gl_FragCoord.xy * vec2(2.,2.); uv -= iResolution.xy*vec2(1.,1.); vec2 x = uv; x *= .001; uv.y -= iTime*rise_speed; vec2 a = uv * scale; float s = fract(x.y*.5); s += s - 1.; //a.x *= 3.+((abs(s)-.5)*3.); float w = fract(iTime*.3+.9*a.y*.1); w += w - 1.; w *= 1.; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(iTime * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= iTime*layer_speed_scale*fract(abs(i.x*.312)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; // ? vec2 i2 = floor(a+.5); vec2 f = fract(a); f += f - 1.; f = abs(f); float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232)); float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y); vec3 col = mix(vec3(1.,.8,.0),vec3(.5,.05,.05),clamp(x.y*2.+1.+fract(i.x*.3),0.,1.)); vec3 bg = mix(vec3(.03,.01,.08),vec3(.0,.0,.0),x.y*5.); e = f.y; // Uncomment to see underlying field structure col = mix(bg,e*col,e); gl_FragColor = vec4(col,1.); } void main() { float iTime = time; vec2 iResolution = size; float scale = .007; float rise_speed = 100.0; float density = .5+.5*abs(fract(iTime*.01)*2.-1.); float layer_speed_scale = 1.0; vec2 uv = gl_FragCoord.xy * vec2(2.,2.); uv -= iResolution.xy*vec2(1.,1.); vec2 x = uv; x *= .001; uv.y -= iTime*rise_speed; vec2 a = uv * scale; float s = fract(x.y*.5); s += s - 1.; //a.x *= 3.+((abs(s)-.5)*3.); float w = fract(iTime*.3+.9*a.y*.1); w += w - 1.; w *= 1.; float c = fract(a.y*.5); c += c - 1.; c *= .2; a.x += a.y * abs(w) * .3 * abs(c); vec2 i = floor(a); float it = fract(iTime * (i.x+10.)*.03); it += it - 1.; a.y += ((1.+i.x)*(1.+i.x)*.3213+.2*abs(it)); // Wave pattern a.y -= iTime*layer_speed_scale*fract(abs(i.x*.312)); // Multi speed "layers" a.y *= .5+.5*fract(i.x*.5); a.y *= 2.; a.x += 0.5; // ? vec2 i2 = floor(a+.5); vec2 f = fract(a); f += f - 1.; f = abs(f); float v = step(density,fract((fract(i2.y*.1)+fract(i.x*.01))*63.232)); float e = v*smoothstep(.5+.2*fract(-i.x*.3+.5),.99,f.x) * smoothstep(.75+.14*fract(i.x*.25+.23),.99,f.y); vec3 col = mix(vec3(1.,.8,.0),vec3(.5,.05,.05),clamp(x.y*2.+1.+fract(i.x*.3),0.,1.)); vec3 bg = mix(vec3(.03,.01,.08),vec3(.0,.0,.0),x.y*5.); //e = f.y; // Uncomment to see underlying field structure col = mix(bg,e*col,e); gl_FragColor = vec4(col,1.); }

Even though the code contains no loops or branches (‘if’ statements) to create multiple layers, it still tries to create the illusion of many layers through the varying speeds of each column.

But by avoiding generating multiple layers of potential-particles in each pixel, the instruction count is kept much lower, giving the chance for 60FPS performance even on mobile devices.