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