Fireflies
A meadow at night below the kiln, and two thousand fireflies, each a clock that flashes as it comes round. Each one sees the fireflies near it and nudges its own clock towards theirs, and out of those small nudges the meadow falls into step until it flashes as one. Every thirty seconds the kiln door opens, its glare scatters their beat, and they find it again. Bells ring as the fireflies flash, scattered at first, then together.
· Particles & Simulation · Compute shader
Teaches Coupled oscillators: particles that see each other blink in time
Builds on Cracked Clay (integer hash, sound noise), Glass Orb (the kiln's dark), Murmuration
Watch
The idea
In a few places, the Great Smoky Mountains among them, fireflies on a summer night flash together: thousands of them, the whole hillside at once. None of them leads. Each firefly has its own rhythm and sees only the ones near it, and each shifts its next flash a little towards the flashes it sees. That is enough.
The model is Yoshiki Kuramoto's, from 1975. Each firefly is an oscillator: a phase that runs from 0 to 1, round and round, a flash each time it wraps. Its speed is its own rhythm, plus a pull from every firefly it can see, by the sine of how far ahead of it that one is: forward if they are a little ahead, back if they are a little behind, nothing if they are exactly in step or exactly opposite. Sum the pulls and the fireflies settle into step. The particles are the same machinery as Murmuration: a buffer of them on the GPU, stepped sixty times a second, each reading all the others through Swarm. It is built in four steps.
Stage 1 · Lights in the grass
Two thousand fireflies, each a struct Particle: its position in a space two units tall and as wide as the picture, its heading, and a random seed. spawn puts each one anywhere in the meadow, between LOW, just above the bottom of the picture, and a ceiling of its own, ceilingOf(seed), from just over the grass to above the trees, so they thin out with height. Each step update turns the heading by a random amount (hashed from the particle and the step, as Murmuration's wobble is) and moves it forward at 0.15 units a second, times drift, faster for some than others. Reaching the floor or its ceiling flips the heading's vertical part, and the sides wrap round 0.1 past the edge of the picture, so a dot never pops on screen. place draws each as a dot 4 to 5.5 % of the picture's height, and ink gives it a near-white core inside a halo of the kiln's gold turned a little green, added to the others' light. fade keeps 60 % of the last step's image, so light lingers a moment. Behind them, sky is deep violet with a few stars and land is a tree line, the kiln on its hill with its door glowing, and grass blades made of sines raised to high powers.
stage-1-lights-in-the-grass.metal
// Stage 1 · Lights in the grass
// Two thousand fireflies in the meadow below the kiln, each a few numbers on
// the GPU. Sixty times a second each turns a little at random and drifts on.
// Each is drawn as a dot of light, added to the others'. For now they glow.
//
// @particles 2048
// @param drift 1.0 0.0 2.0 How fast the fireflies wander
// @param glow 1.0 0.3 2.0 How bright a firefly glows
constant float TAU = 6.2831853;
// They fly from just above the bottom of the picture up to a ceiling of
// each one's own, so they thin out with height.
constant float LOW = -0.85;
constant float HIGH = 0.5;
float ceilingOf(float seed) { return mix(-0.35, HIGH, seed); }
// A firefly: where it is, which way it wanders, a random number of its own.
struct Particle {
float2 position;
float heading;
float seed;
};
// Cracked Clay's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// Each starts anywhere in the meadow, wandering any way.
Particle spawn(uint id, constant Params& p) {
Particle q;
float aspect = 16.0 / 9.0;
q.seed = hashU(id * 6u + 4u);
float x = (hashU(id * 6u) * 2.0 - 1.0) * (aspect + 0.1);
float y = mix(LOW, ceilingOf(q.seed), hashU(id * 6u + 1u));
q.position = float2(x, y);
q.heading = TAU * hashU(id * 6u + 2u);
return q;
}
// One step: it wanders.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
// The heading turns a little at random. The grass and its ceiling turn it
// back; the sides wrap round past the edge of the picture.
uint k = id * 3u + u.frame * 7919u;
q.heading += (hashU(k) - 0.5) * 0.6;
float2 dir = float2(cos(q.heading), sin(q.heading));
bool under = q.position.y < LOW && dir.y < 0.0;
bool over = q.position.y > ceilingOf(q.seed) && dir.y > 0.0;
if (under || over) q.heading = -q.heading;
q.position += dir * 0.15 * p.drift * (0.6 + 0.8 * q.seed) * dt;
float edge = aspect + 0.1;
if (q.position.x > edge) q.position.x -= 2.0 * edge;
if (q.position.x < -edge) q.position.x += 2.0 * edge;
}
// Where to draw a firefly (the picture's uv) and how big its glow is.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.04 + 0.015 * q.seed);
}
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// Its light: a near-white core in a halo of gold turned a little green.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float r = length(at);
float light = 0.12 * p.glow;
float3 halo = kiln(0.72) * float3(0.85, 1.05, 0.45);
float3 core = kiln(0.95) * float3(0.95, 1.0, 0.7);
float3 c = core * exp(-r * r * 45.0) * 2.0;
c += halo * exp(-r * r * 6.0) * 0.5;
return float4(c * light * smoothstep(1.0, 0.7, r), 1.0);
}
// What is left of the last step's light: a little, so a flash lingers.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.6;
}
// The night sky: deep violet, warmer down at the trees, with a few stars.
float3 sky(float2 uv, float aspect) {
float3 low = float3(0.085, 0.060, 0.120);
float3 high = float3(0.012, 0.010, 0.032);
float3 c = mix(low, high, smoothstep(0.33, 1.0, uv.y));
float2 s = uv * float2(aspect, 1.0) * 90.0;
float2 cell = floor(s);
float h = hashU(uint(cell.x + 4000.0) * 977u + uint(cell.y) * 131u);
float star = step(0.993, h) * smoothstep(0.25, 0.0, length(fract(s) - 0.5));
c += float3(0.5, 0.48, 0.42) * star * smoothstep(0.45, 0.7, uv.y);
return c;
}
// The trees, the kiln on its hill, and the grass.
float3 land(float2 uv, float3 c, float aspect, float t) {
float x = uv.x * aspect;
float trees = 0.34 + 0.035 * sin(x * 5.1) + 0.02 * sin(x * 13.7 + 1.0);
trees += 0.012 * sin(x * 37.0);
float3 dark = float3(0.012, 0.012, 0.018);
c = mix(c, dark, smoothstep(0.002, -0.002, uv.y - trees));
float2 k = float2(x - 0.8 * aspect, uv.y - 0.39);
float hill = uv.y - (0.385 - 0.35 * k.x * k.x);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.011, abs(k.y - 0.09) - 0.075);
float solid = min(min(hill, dome), stack);
float3 brick = float3(0.016, 0.013, 0.016);
c = mix(c, brick, smoothstep(0.002, -0.002, solid));
float door = length((k - float2(0.0, 0.018)) * float2(1.6, 1.0)) - 0.024;
float flicker = 0.85 + 0.15 * sin(t * 9.0) * sin(t * 5.3);
float inDoor = smoothstep(0.003, -0.001, door);
c += kiln(0.6) * inDoor * flicker * 0.4;
c += kiln(0.55) * exp(-max(door, 0.0) * 40.0) * 0.06 * flicker;
// Each blade a sharp peak: a sine raised to a high power.
float blade = 0.07 + 0.02 * sin(x * 3.0);
blade += 0.09 * pow(abs(sin(x * 41.0)), 9.0);
blade += 0.06 * pow(abs(sin(x * 67.0 + 1.0)), 7.0);
blade += 0.11 * pow(abs(sin(x * 17.0 + 2.0)), 24.0);
float grass = uv.y - blade;
float3 g = float3(0.008, 0.012, 0.010);
return mix(c, g, smoothstep(0.003, -0.003, grass));
}
// The picture: the night, and the fireflies' light rolled off over it.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 c = land(uv, sky(uv, aspect), aspect, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}Stage 2 · A clock each
Each firefly gets a phase from 0 to 1 and a rate, its own rhythm in flashes a second: BEAT, 0.75, give or take spread (6 % by default), set once in spawn. update adds rate × timeDelta to the phase and wraps it with fract. blink turns a phase into brightness: a rise over the first 2.5 % of the cycle, then exp(-14 × phase), about a quarter of a second of light, and ink multiplies its glow by it, with a faint 0.015 left so a firefly never quite goes out. The phases start at random and no firefly sees another, so the meadow twinkles at random, and would for ever.
stage-2-a-clock-each.metal11 new lines since stage 1
// Stage 2 · A clock each
// Each firefly gets a clock: a phase from 0 to 1, run at a rhythm of its own,
// a flash each time it wraps round. They start at random and none sees
// another, so the meadow twinkles at random, and would for ever.
//
// @particles 2048
// @param spread 0.06 0.0 0.3 How different the fireflies' own rhythms are
// @param drift 1.0 0.0 2.0 How fast the fireflies wander
// @param glow 1.0 0.3 2.0 How bright a flash is
constant float TAU = 6.2831853;
// The fireflies' rhythm, on average: flashes a second.
constant float BEAT = 0.75;
// They fly from just above the bottom of the picture up to a ceiling of
// each one's own, so they thin out with height.
constant float LOW = -0.85;
constant float HIGH = 0.5;
float ceilingOf(float seed) { return mix(-0.35, HIGH, seed); }
// A firefly: where it is, which way it wanders, its phase, its own rhythm in
// flashes a second, and a random number of its own.
struct Particle {
float2 position;
float heading;
float phase;
float rate;
float seed;
};
// Cracked Clay's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// A flash just after the phase wraps round: a quick rise, a slower fade.
float blink(float phase) {
return smoothstep(0.0, 0.025, phase) * exp(-phase * 14.0);
}
// Each starts anywhere in the meadow, wandering any way, at a random point
// of its cycle, with a rhythm near BEAT: spread says how near.
Particle spawn(uint id, constant Params& p) {
Particle q;
float aspect = 16.0 / 9.0;
q.seed = hashU(id * 6u + 4u);
float x = (hashU(id * 6u) * 2.0 - 1.0) * (aspect + 0.1);
float y = mix(LOW, ceilingOf(q.seed), hashU(id * 6u + 1u));
q.position = float2(x, y);
q.heading = TAU * hashU(id * 6u + 2u);
q.phase = hashU(id * 6u + 3u);
float r = 2.0 * hashU(id * 6u + 5u) - 1.0;
q.rate = BEAT * (1.0 + p.spread * r);
return q;
}
// One step: the phase runs on at its own rhythm and wraps at 1.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
q.phase = fract(q.phase + q.rate * dt);
// The heading turns a little at random. The grass and its ceiling turn it
// back; the sides wrap round past the edge of the picture.
uint k = id * 3u + u.frame * 7919u;
q.heading += (hashU(k) - 0.5) * 0.6;
float2 dir = float2(cos(q.heading), sin(q.heading));
bool under = q.position.y < LOW && dir.y < 0.0;
bool over = q.position.y > ceilingOf(q.seed) && dir.y > 0.0;
if (under || over) q.heading = -q.heading;
q.position += dir * 0.15 * p.drift * (0.6 + 0.8 * q.seed) * dt;
float edge = aspect + 0.1;
if (q.position.x > edge) q.position.x -= 2.0 * edge;
if (q.position.x < -edge) q.position.x += 2.0 * edge;
}
// Where to draw a firefly (the picture's uv) and how big its glow is.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.04 + 0.015 * q.seed);
}
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// Its light: a near-white core in a halo of gold turned a little green,
// as bright as its flash, and never quite out.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float r = length(at);
float light = blink(q.phase) * p.glow + 0.015;
float3 halo = kiln(0.72) * float3(0.85, 1.05, 0.45);
float3 core = kiln(0.95) * float3(0.95, 1.0, 0.7);
float3 c = core * exp(-r * r * 45.0) * 2.0;
c += halo * exp(-r * r * 6.0) * 0.5;
return float4(c * light * smoothstep(1.0, 0.7, r), 1.0);
}
// What is left of the last step's light: a little, so a flash lingers.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.6;
}
// The night sky: deep violet, warmer down at the trees, with a few stars.
float3 sky(float2 uv, float aspect) {
float3 low = float3(0.085, 0.060, 0.120);
float3 high = float3(0.012, 0.010, 0.032);
float3 c = mix(low, high, smoothstep(0.33, 1.0, uv.y));
float2 s = uv * float2(aspect, 1.0) * 90.0;
float2 cell = floor(s);
float h = hashU(uint(cell.x + 4000.0) * 977u + uint(cell.y) * 131u);
float star = step(0.993, h) * smoothstep(0.25, 0.0, length(fract(s) - 0.5));
c += float3(0.5, 0.48, 0.42) * star * smoothstep(0.45, 0.7, uv.y);
return c;
}
// The trees, the kiln on its hill, and the grass.
float3 land(float2 uv, float3 c, float aspect, float t) {
float x = uv.x * aspect;
float trees = 0.34 + 0.035 * sin(x * 5.1) + 0.02 * sin(x * 13.7 + 1.0);
trees += 0.012 * sin(x * 37.0);
float3 dark = float3(0.012, 0.012, 0.018);
c = mix(c, dark, smoothstep(0.002, -0.002, uv.y - trees));
float2 k = float2(x - 0.8 * aspect, uv.y - 0.39);
float hill = uv.y - (0.385 - 0.35 * k.x * k.x);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.011, abs(k.y - 0.09) - 0.075);
float solid = min(min(hill, dome), stack);
float3 brick = float3(0.016, 0.013, 0.016);
c = mix(c, brick, smoothstep(0.002, -0.002, solid));
float door = length((k - float2(0.0, 0.018)) * float2(1.6, 1.0)) - 0.024;
float flicker = 0.85 + 0.15 * sin(t * 9.0) * sin(t * 5.3);
float inDoor = smoothstep(0.003, -0.001, door);
c += kiln(0.6) * inDoor * flicker * 0.4;
c += kiln(0.55) * exp(-max(door, 0.0) * 40.0) * 0.06 * flicker;
// Each blade a sharp peak: a sine raised to a high power.
float blade = 0.07 + 0.02 * sin(x * 3.0);
blade += 0.09 * pow(abs(sin(x * 41.0)), 9.0);
blade += 0.06 * pow(abs(sin(x * 67.0 + 1.0)), 7.0);
blade += 0.11 * pow(abs(sin(x * 17.0 + 2.0)), 24.0);
float grass = uv.y - blade;
float3 g = float3(0.008, 0.012, 0.010);
return mix(c, g, smoothstep(0.003, -0.003, grass));
}
// The picture: the night, and the fireflies' light rolled off over it.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 c = land(uv, sky(uv, aspect), aspect, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}1 line from stage 1 that are gone
float light = 0.12 * p.glow;Stage 3 · Seeing the neighbours
update now takes Swarm swarm and loops over every firefly. One nearer than sight (as a share of the picture's height, so 0.25 is half a unit) pulls with weight 1 - d / sight, by sin(2π × (o.phase - q.phase)): the sine of how far ahead it is, in turns. The pulls are averaged, and the phase moves at rate + coupling × average pull. With the defaults, the fireflies near each other fall into step within a few seconds, those patches pull on each other, and after fifteen seconds or so the whole meadow flashes together. Because a firefly only sees so far, the meadow can also settle into a wave that runs across it, each patch a little behind the last: it takes the wandering, which mixes neighbours, to undo it. With coupling at 0 this is stage 2 again; with spread far above coupling, rhythms too different to pull together, it stays so.
stage-3-seeing-the-neighbours.metal15 new lines since stage 2
// Stage 3 · Seeing the neighbours
// Now each firefly sees the others near it, and each pulls its phase toward
// theirs by the sine of the gap. That is Kuramoto's model: the meadow falls
// into step, first in patches, then all of it at once.
//
// @particles 2048
// @param coupling 0.3 0.0 1.0 How hard each firefly pulls its beat toward the flashes it sees
// @param sight 0.25 0.1 0.5 How far a firefly sees the others, as a fraction of the picture's height
// @param spread 0.06 0.0 0.3 How different the fireflies' own rhythms are
// @param drift 1.0 0.0 2.0 How fast the fireflies wander
// @param glow 1.0 0.3 2.0 How bright a flash is
constant float TAU = 6.2831853;
// The fireflies' rhythm, on average: flashes a second.
constant float BEAT = 0.75;
// They fly from just above the bottom of the picture up to a ceiling of
// each one's own, so they thin out with height.
constant float LOW = -0.85;
constant float HIGH = 0.5;
float ceilingOf(float seed) { return mix(-0.35, HIGH, seed); }
// A firefly: where it is, which way it wanders, its phase, its own rhythm in
// flashes a second, and a random number of its own.
struct Particle {
float2 position;
float heading;
float phase;
float rate;
float seed;
};
// Cracked Clay's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// A flash just after the phase wraps round: a quick rise, a slower fade.
float blink(float phase) {
return smoothstep(0.0, 0.025, phase) * exp(-phase * 14.0);
}
// Each starts anywhere in the meadow, wandering any way, at a random point
// of its cycle, with a rhythm near BEAT: spread says how near.
Particle spawn(uint id, constant Params& p) {
Particle q;
float aspect = 16.0 / 9.0;
q.seed = hashU(id * 6u + 4u);
float x = (hashU(id * 6u) * 2.0 - 1.0) * (aspect + 0.1);
float y = mix(LOW, ceilingOf(q.seed), hashU(id * 6u + 1u));
q.position = float2(x, y);
q.heading = TAU * hashU(id * 6u + 2u);
q.phase = hashU(id * 6u + 3u);
float r = 2.0 * hashU(id * 6u + 5u) - 1.0;
q.rate = BEAT * (1.0 + p.spread * r);
return q;
}
// One step: the phase runs on at its own rhythm and wraps at 1.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
// Each firefly it can see pulls on its phase by the sine of how far
// ahead that one is, nearer ones harder.
float sight = p.sight * 2.0;
float pull = 0.0, seen = 0.0;
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float d = length(q.position - o.position);
if (d >= sight) continue;
float w = 1.0 - d / sight;
pull += w * sin(TAU * (o.phase - q.phase));
seen += w;
}
// Kuramoto: its own rhythm, plus the coupling times the average pull.
float speed = q.rate;
if (seen > 0.0) speed += p.coupling * pull / seen;
q.phase = fract(q.phase + speed * dt);
// The heading turns a little at random. The grass and its ceiling turn it
// back; the sides wrap round past the edge of the picture.
uint k = id * 3u + u.frame * 7919u;
q.heading += (hashU(k) - 0.5) * 0.6;
float2 dir = float2(cos(q.heading), sin(q.heading));
bool under = q.position.y < LOW && dir.y < 0.0;
bool over = q.position.y > ceilingOf(q.seed) && dir.y > 0.0;
if (under || over) q.heading = -q.heading;
q.position += dir * 0.15 * p.drift * (0.6 + 0.8 * q.seed) * dt;
float edge = aspect + 0.1;
if (q.position.x > edge) q.position.x -= 2.0 * edge;
if (q.position.x < -edge) q.position.x += 2.0 * edge;
}
// Where to draw a firefly (the picture's uv) and how big its glow is.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.04 + 0.015 * q.seed);
}
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// Its light: a near-white core in a halo of gold turned a little green,
// as bright as its flash, and never quite out.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float r = length(at);
float light = blink(q.phase) * p.glow + 0.015;
float3 halo = kiln(0.72) * float3(0.85, 1.05, 0.45);
float3 core = kiln(0.95) * float3(0.95, 1.0, 0.7);
float3 c = core * exp(-r * r * 45.0) * 2.0;
c += halo * exp(-r * r * 6.0) * 0.5;
return float4(c * light * smoothstep(1.0, 0.7, r), 1.0);
}
// What is left of the last step's light: a little, so a flash lingers.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.6;
}
// The night sky: deep violet, warmer down at the trees, with a few stars.
float3 sky(float2 uv, float aspect) {
float3 low = float3(0.085, 0.060, 0.120);
float3 high = float3(0.012, 0.010, 0.032);
float3 c = mix(low, high, smoothstep(0.33, 1.0, uv.y));
float2 s = uv * float2(aspect, 1.0) * 90.0;
float2 cell = floor(s);
float h = hashU(uint(cell.x + 4000.0) * 977u + uint(cell.y) * 131u);
float star = step(0.993, h) * smoothstep(0.25, 0.0, length(fract(s) - 0.5));
c += float3(0.5, 0.48, 0.42) * star * smoothstep(0.45, 0.7, uv.y);
return c;
}
// The trees, the kiln on its hill, and the grass.
float3 land(float2 uv, float3 c, float aspect, float t) {
float x = uv.x * aspect;
float trees = 0.34 + 0.035 * sin(x * 5.1) + 0.02 * sin(x * 13.7 + 1.0);
trees += 0.012 * sin(x * 37.0);
float3 dark = float3(0.012, 0.012, 0.018);
c = mix(c, dark, smoothstep(0.002, -0.002, uv.y - trees));
float2 k = float2(x - 0.8 * aspect, uv.y - 0.39);
float hill = uv.y - (0.385 - 0.35 * k.x * k.x);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.011, abs(k.y - 0.09) - 0.075);
float solid = min(min(hill, dome), stack);
float3 brick = float3(0.016, 0.013, 0.016);
c = mix(c, brick, smoothstep(0.002, -0.002, solid));
float door = length((k - float2(0.0, 0.018)) * float2(1.6, 1.0)) - 0.024;
float flicker = 0.85 + 0.15 * sin(t * 9.0) * sin(t * 5.3);
float inDoor = smoothstep(0.003, -0.001, door);
c += kiln(0.6) * inDoor * flicker * 0.4;
c += kiln(0.55) * exp(-max(door, 0.0) * 40.0) * 0.06 * flicker;
// Each blade a sharp peak: a sine raised to a high power.
float blade = 0.07 + 0.02 * sin(x * 3.0);
blade += 0.09 * pow(abs(sin(x * 41.0)), 9.0);
blade += 0.06 * pow(abs(sin(x * 67.0 + 1.0)), 7.0);
blade += 0.11 * pow(abs(sin(x * 17.0 + 2.0)), 24.0);
float grass = uv.y - blade;
float3 g = float3(0.008, 0.012, 0.010);
return mix(c, g, smoothstep(0.003, -0.003, grass));
}
// The picture: the night, and the fireflies' light rolled off over it.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 c = land(uv, sky(uv, aspect), aspect, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}2 lines from stage 2 that are gone
constant Uniforms& u, constant Params& p) {
⋯
q.phase = fract(q.phase + q.rate * dt);Stage 4 · Fireflies
The kiln door. glareAt rises at the start of every 30 seconds and dies away over a few: the door glows white-hot, and its light spreads over the sky and the meadow. At the step the door opens, update gives every firefly a new random phase (hashed from its number and the cycle), so they come out of the glare scattered and fall into step all over again. And the meadow is lit by its fireflies: shade samples the light's image at 12 points across the picture, and the average brightens the sky above the trees and the tips of the grass, so when the fireflies flash together the whole night flickers with them.
the kiln palette and shade(), from the finished shader
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// The picture. The light at 12 points, averaged, says how lit the meadow
// is: a flash together brightens the sky and the grass.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 all = float3(0.0);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
float2 at = float2((float(i) + 0.5) / 4.0, 0.15 + 0.22 * float(j));
all += trails.at(at).rgb;
}
}
float lit = min(dot(all, float3(0.33)) * 0.5, 1.0);
float glare = glareAt(u.time);
float3 c = sky(uv, aspect);
c += float3(0.04, 0.05, 0.02) * lit * smoothstep(0.7, 0.3, uv.y);
c = land(uv, c, aspect, lit, glare, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}fireflies.metal, the whole file78 new lines since stage 3
// Ray Kiln · Fireflies
//
// @episode Fireflies
// @length 60
// @variation 30
// @variations 1,3,4,5,6,7,10,12
// @sweeps coupling, sight, glow
// @still 20.9
// @teaches Coupled oscillators: particles that see each other blink in time
// @category Compute
// @tags fireflies, synchronization, coupled oscillators, kuramoto model, particles, compute shaders, swarm, procedural sound
// @final Fireflies | Every thirty seconds the kiln door opens: its glare scatters every firefly's beat, and the meadow finds it again.
//
// A summer night in the meadow below the kiln. Each firefly is a clock: a
// phase that runs from 0 to 1 and flashes as it wraps round, each a little
// faster or slower than the others. Every firefly sees the ones near it and,
// at every step, nudges its phase toward theirs: forward if they are ahead,
// back if they are behind. That is Kuramoto's model of coupled oscillators,
// from 1975, and out of it comes what the fireflies of the Great Smoky
// Mountains do: patches fall into step, the patches pull on each other, and
// soon the whole meadow flashes as one. Every thirty seconds the kiln door
// opens, its glare scatters their beat, and they find it again.
// Each @param line below becomes a slider in the host app and a row in the
// page's parameters table:
// // @param name default min max description
//
// @particles 2048
// @param coupling 0.3 0.0 1.0 How hard each firefly pulls its beat toward the flashes it sees
// @param sight 0.25 0.1 0.5 How far a firefly sees the others, as a fraction of the picture's height
// @param spread 0.06 0.0 0.3 How different the fireflies' own rhythms are
// @param drift 1.0 0.0 2.0 How fast the fireflies wander
// @param glow 1.0 0.3 2.0 How bright a flash is
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The kiln door opens every CYCLE seconds.
constant float CYCLE = 30.0;
// The fireflies' rhythm, on average: flashes a second.
constant float BEAT = 0.75;
// They fly from just above the bottom of the picture up to a ceiling of
// each one's own, so they thin out with height.
constant float LOW = -0.85;
constant float HIGH = 0.5;
float ceilingOf(float seed) { return mix(-0.35, HIGH, seed); }
// A firefly: where it is, which way it wanders, its phase, its own rhythm in
// flashes a second, and a random number of its own.
struct Particle {
float2 position;
float heading;
float phase;
float rate;
float seed;
};
// Cracked Clay's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// A flash just after the phase wraps round: a quick rise, a slower fade.
float blink(float phase) {
return smoothstep(0.0, 0.025, phase) * exp(-phase * 14.0);
}
// Its glare: up at once, gone in a few seconds.
float glareAt(float t) {
float x = fmod(t, CYCLE);
return smoothstep(0.0, 0.12, x) * exp(-x * 1.6);
}
// Each starts anywhere in the meadow, wandering any way, at a random point
// of its cycle, with a rhythm near BEAT: spread says how near.
Particle spawn(uint id, constant Params& p) {
Particle q;
float aspect = 16.0 / 9.0;
q.seed = hashU(id * 6u + 4u);
float x = (hashU(id * 6u) * 2.0 - 1.0) * (aspect + 0.1);
float y = mix(LOW, ceilingOf(q.seed), hashU(id * 6u + 1u));
q.position = float2(x, y);
q.heading = TAU * hashU(id * 6u + 2u);
q.phase = hashU(id * 6u + 3u);
float r = 2.0 * hashU(id * 6u + 5u) - 1.0;
q.rate = BEAT * (1.0 + p.spread * r);
return q;
}
// One step: the phase runs on at its own rhythm and wraps at 1.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
// Each firefly it can see pulls on its phase by the sine of how far
// ahead that one is, nearer ones harder.
float sight = p.sight * 2.0;
float pull = 0.0, seen = 0.0;
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float d = length(q.position - o.position);
if (d >= sight) continue;
float w = 1.0 - d / sight;
pull += w * sin(TAU * (o.phase - q.phase));
seen += w;
}
// Kuramoto: its own rhythm, plus the coupling times the average pull.
float speed = q.rate;
if (seen > 0.0) speed += p.coupling * pull / seen;
q.phase = fract(q.phase + speed * dt);
// As it opens, each firefly loses its phase.
float cycle = floor(u.time / CYCLE);
if (cycle != floor((u.time - dt) / CYCLE)) {
q.phase = hashU(id * 7919u + uint(cycle) * 104729u);
}
// The heading turns a little at random. The grass and its ceiling turn it
// back; the sides wrap round past the edge of the picture.
uint k = id * 3u + u.frame * 7919u;
q.heading += (hashU(k) - 0.5) * 0.6;
float2 dir = float2(cos(q.heading), sin(q.heading));
bool under = q.position.y < LOW && dir.y < 0.0;
bool over = q.position.y > ceilingOf(q.seed) && dir.y > 0.0;
if (under || over) q.heading = -q.heading;
q.position += dir * 0.15 * p.drift * (0.6 + 0.8 * q.seed) * dt;
float edge = aspect + 0.1;
if (q.position.x > edge) q.position.x -= 2.0 * edge;
if (q.position.x < -edge) q.position.x += 2.0 * edge;
}
// Where to draw a firefly (the picture's uv) and how big its glow is.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.04 + 0.015 * q.seed);
}
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// Its light: a near-white core in a halo of gold turned a little green,
// as bright as its flash, and never quite out.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float r = length(at);
float light = blink(q.phase) * p.glow + 0.015;
float3 halo = kiln(0.72) * float3(0.85, 1.05, 0.45);
float3 core = kiln(0.95) * float3(0.95, 1.0, 0.7);
float3 c = core * exp(-r * r * 45.0) * 2.0;
c += halo * exp(-r * r * 6.0) * 0.5;
return float4(c * light * smoothstep(1.0, 0.7, r), 1.0);
}
// What is left of the last step's light: a little, so a flash lingers.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.6;
}
// The night sky: deep violet, warmer down at the trees, with a few stars.
float3 sky(float2 uv, float aspect) {
float3 low = float3(0.085, 0.060, 0.120);
float3 high = float3(0.012, 0.010, 0.032);
float3 c = mix(low, high, smoothstep(0.33, 1.0, uv.y));
float2 s = uv * float2(aspect, 1.0) * 90.0;
float2 cell = floor(s);
float h = hashU(uint(cell.x + 4000.0) * 977u + uint(cell.y) * 131u);
float star = step(0.993, h) * smoothstep(0.25, 0.0, length(fract(s) - 0.5));
c += float3(0.5, 0.48, 0.42) * star * smoothstep(0.45, 0.7, uv.y);
return c;
}
// The land, lit by the fireflies and the open door.
float3 land(float2 uv, float3 c, float aspect, float lit, float glare,
float t) {
float x = uv.x * aspect;
float trees = 0.34 + 0.035 * sin(x * 5.1) + 0.02 * sin(x * 13.7 + 1.0);
trees += 0.012 * sin(x * 37.0);
float3 dark = float3(0.012, 0.012, 0.018);
c = mix(c, dark, smoothstep(0.002, -0.002, uv.y - trees));
float2 k = float2(x - 0.8 * aspect, uv.y - 0.39);
float hill = uv.y - (0.385 - 0.35 * k.x * k.x);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.011, abs(k.y - 0.09) - 0.075);
float solid = min(min(hill, dome), stack);
float3 brick = float3(0.016, 0.013, 0.016);
c = mix(c, brick, smoothstep(0.002, -0.002, solid));
float door = length((k - float2(0.0, 0.018)) * float2(1.6, 1.0)) - 0.024;
float flicker = 0.85 + 0.15 * sin(t * 9.0) * sin(t * 5.3);
float inDoor = smoothstep(0.003, -0.001, door);
c += kiln(0.6 + 0.35 * glare) * inDoor * flicker * (0.4 + 0.8 * glare);
// Open, the door's light spreads over the whole meadow.
float spill = exp(-max(door, 0.0) * (40.0 - 30.0 * glare));
c += kiln(0.55) * spill * (0.06 + 0.19 * glare) * flicker;
c += kiln(0.5) * glare * 0.45 * exp(-length(k) * 1.3);
// Each blade a sharp peak: a sine raised to a high power.
float blade = 0.07 + 0.02 * sin(x * 3.0);
blade += 0.09 * pow(abs(sin(x * 41.0)), 9.0);
blade += 0.06 * pow(abs(sin(x * 67.0 + 1.0)), 7.0);
blade += 0.11 * pow(abs(sin(x * 17.0 + 2.0)), 24.0);
float grass = uv.y - blade;
float3 g = float3(0.008, 0.012, 0.010);
float tip = smoothstep(-0.1, 0.0, grass);
g += float3(0.10, 0.13, 0.04) * lit * tip + kiln(0.55) * glare * 0.25 * tip;
return mix(c, g, smoothstep(0.003, -0.003, grass));
}
// The picture. The light at 12 points, averaged, says how lit the meadow
// is: a flash together brightens the sky and the grass.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 all = float3(0.0);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
float2 at = float2((float(i) + 0.5) / 4.0, 0.15 + 0.22 * float(j));
all += trails.at(at).rgb;
}
}
float lit = min(dot(all, float3(0.33)) * 0.5, 1.0);
float glare = glareAt(u.time);
float3 c = sky(uv, aspect);
c += float3(0.04, 0.05, 0.02) * lit * smoothstep(0.7, 0.3, uv.y);
c = land(uv, c, aspect, lit, glare, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}
// A smooth random value from 0 to 1 that changes `rate` times a second:
// two hashed values eased into each other.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// The sound can't see the swarm, so it tells the same story from the clock.
// Twelve bells on A major pentatonic, each ringing as its own phase wraps.
// Each cycle they start scattered, and the gaps between them shrink with
// the seconds since the door opened, faster with more coupling, until they
// ring together. Under them, two crickets, the kiln's hum, and the roar of
// the door while the bells are silent.
float2 sound(float t, constant Params& p) {
float x = fmod(t, CYCLE);
uint cycle = uint(floor(t / CYCLE));
// Silent through the glare and just before the next: no jump is heard.
float gate = smoothstep(1.2, 2.4, x) * smoothstep(CYCLE, CYCLE - 0.4, x);
float together = 1.0 - exp(-max(x - 1.0, 0.0) * 0.6 * p.coupling);
float notes[6] = { 880.0, 987.8, 1108.7, 1318.5, 1480.0, 1760.0 };
float2 bells = float2(0.0);
for (uint i = 0; i < 12; i++) {
float apart = hashU(i * 13u + cycle * 101u);
float phase = BEAT * t + apart * (1.0 - together);
float age = fract(phase) / BEAT;
float env = smoothstep(0.0, 0.004, age) * exp(-age * 7.0);
float f = notes[i % 6] * (i >= 6 ? 0.5 : 1.0);
float ring = sin(TAU * f * age);
ring += 0.25 * sin(TAU * f * 2.76 * age) * exp(-age * 12.0);
float pan = 0.15 + 0.7 * hashU(i * 7u + 3u);
bells += float2(1.0 - pan, pan) * ring * env;
}
bells *= gate * 0.2;
// Two crickets: a trill pulsed 30 times a second, in chirps of a third
// of a second.
float2 crickets = float2(0.0);
for (uint c = 0; c < 2; c++) {
float period = 1.1 + 0.27 * float(c);
float n = floor(t / period);
float age = t - n * period - 0.2 * hashU(uint(n) * 3u + c);
float on = smoothstep(0.0, 0.03, age) * smoothstep(0.34, 0.26, age);
float chirp = age > 0.0 ? on : 0.0;
float pulse = 0.5 + 0.5 * sin(TAU * 30.0 * t + float(c));
float f = 4300.0 + 180.0 * float(c);
float s = sin(TAU * f * t) * chirp * pulse * pulse * 0.07;
crickets += c == 0 ? float2(s * 0.8, s * 0.2) : float2(s * 0.25, s * 0.75);
}
// The kiln's hum, and the roar of the door: noise at three rates.
float hum = (sin(TAU * 55.0 * t) + 0.5 * sin(TAU * 82.5 * t)) * 0.1;
float roar = noise1(t, 900.0, 3u) - 0.5;
roar += 0.7 * (noise1(t, 220.0, 7u) - 0.5);
roar += 0.5 * (noise1(t, 60.0, 11u) - 0.5);
roar *= glareAt(t) * 1.4;
float2 door = float2(0.35, 0.65) * roar;
float2 v = bells + crickets + float2(hum) + door;
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(v * 1.6) / 1.4 * fadeIn * p.volume;
}4 lines from stage 3 that are gone
float3 land(float2 uv, float3 c, float aspect, float t) {
⋯
c += kiln(0.6) * inDoor * flicker * 0.4;
c += kiln(0.55) * exp(-max(door, 0.0) * 40.0) * 0.06 * flicker;
⋯
float3 c = land(uv, sky(uv, aspect), aspect, u.time);The sound
The sound is a function of time alone and can't see the swarm, so it tells the same story from the clock. Twelve voices are soft bells on A major pentatonic (A5, B5, C♯6, E6, F♯6 and A6, then the same an octave lower), each with a partial at 2.76 times its note that dies away faster, placed between the ears at random. Each rings as its own phase wraps, three times every four seconds, and each phase is BEAT × t plus an offset of its own, new every cycle. The offsets shrink by 1 - exp(-0.6 × coupling × seconds) since the door opened, a curve chosen to follow the picture roughly, so the bells start scattered and, at the default coupling, have mostly drawn together after ten to fifteen seconds. The bells are silent from the moment the door opens until 1.2 s after, and fade out over the last 0.4 s of each cycle, so a voice's jump to its new offset is never heard.
Under them, two crickets trill at 4.3 and 4.48 kHz, pulsed 30 times a second, in chirps of about a third of a second, one to each side. The kiln hums at 55 and 82.5 Hz, and when its door opens it roars: noise smoothed at three rates, shaped by the same glareAt as the picture's glare. Every sine is at a zero crossing on each thirty seconds and the bells are gated off there, so the minute ends at zero, with no click. tanh rounds off the loudest moments, and the sound fades in over a second and a half.
At the defaults the peak is about −6.0 dBFS and the average (RMS) about −21.2 dBFS over the minute; with volume at its highest, coupling at 1 and spread at 0.3, −3.8 dBFS and −19.2 dBFS.
New in Ray Kiln
Swarm statistics in the playground. For a particle shader, the Swarm stats button under the picture shows one number of struct Particle, summed up over every particle on screen: its mean, its spread (the standard deviation), its smallest and its largest, read back from the GPU four times a second. A menu picks the number: any scalar field, or one component of a vector, such as position.y. For a field whose name has "phase" in it, it also says how in step the swarm is: the length of the average of every particle's phase as a point on a circle, 1 when all of them are at the same point of the cycle, near 0 when they are spread round it. That is Kuramoto's own measure, his order parameter. A phase between −1 and 1 is read in turns, as this episode's is; one past that, in radians.
It is a reduction pass: a compute shader in which each group of 256 threads folds its share of the buffer into one count, mean and sum of squared differences (merged as Chan's method does, so the spread keeps its precision rather than coming from a difference of two large sums), with the smallest, the largest and the phase's sums; the browser merges those in double precision. It runs on the particles as they are after the last step, without copying the buffer, and reads back at most 1,024 small partials however many particles there are. The check page runs it on every particle episode against the same sums worked out from the particles' bytes.
Open Fireflies in the playground and press Swarm stats: it starts on phase. In one run, "in step" was 0.13 at two and a half seconds, climbed to about 0.6 by six as patches formed, held there for a few seconds while a wave ran across the meadow, then passed 0.85 at fourteen and 0.99 before the door opened at thirty, when it fell to 0.01. A phase is a point on a circle, so its mean and spread are only meaningful while the swarm is clear of the wrap from 1 to 0: then the spread falls from about 0.3, that of numbers scattered evenly from 0 to 1, to 0.02, but a swarm gathered around the wrap has a mean near 0.5 and a spread near 0.5, however together it is. That is why "in step" is the number to watch. Set coupling to 0 and "in step" stays low. Each firefly's rhythm is set when it is made, so after changing spread, press Restart; then pick rate to see how different the rhythms are that the coupling has to overcome.
Try this
- Set
couplingto 0: every firefly keeps its own time, and the meadow twinkles at random. At 1 they find each other in a few seconds. - Set
sightto 0.1: each firefly sees only its nearest neighbours, and the meadow falls into patches and waves before, if ever, it flashes as one. - Raise
spreadto 0.3 withcouplinglow, and restart: each firefly's rhythm is set when it is made. Rhythms that different don't pull together; find the coupling at which they lock anyway. - Set
driftto 0: with no wandering to mix them, a wave that forms can run round the meadow for a long time. - Turn on Swarm stats in the playground, pick
phaseand change the coupling while it runs: the "in step" bar follows.
Parameters
Pick a parameter and drag the slider. Each frame is the same moment of the shader, with only that one setting changed.
| Parameter | Default | Range | What it does |
|---|---|---|---|
coupling | 0.3 | 0.0 to 1.0 | How hard each firefly pulls its beat toward the flashes it sees |
sight | 0.25 | 0.1 to 0.5 | How far a firefly sees the others, as a fraction of the picture's height |
spread | 0.06 | 0.0 to 0.3 | How different the fireflies' own rhythms are |
drift | 1.0 | 0.0 to 2.0 | How fast the fireflies wander |
glow | 1.0 | 0.3 to 2.0 | How bright a flash is |
volume | 0.8 | 0.0 to 1.0 | Loudness of the sound |
More from this shader
The same shader 8 more ways: each from a different moment, with different settings, one after another. Each chapter below says which settings moved most, then lists them all.
Wallpapers
A still from the shader, rendered at full size for a desktop or a phone screen. Free to use as your wallpaper.
Clips to post
Fifteen seconds of the shader with its sound, sized for posting: square for feeds, vertical for stories and short videos. Free to post, ideally with a link to this page.
Made from this episode
The full source
This is the whole episode: the picture, the parameters and the sound.
fireflies.metal
// Ray Kiln · Fireflies
//
// @episode Fireflies
// @length 60
// @variation 30
// @variations 1,3,4,5,6,7,10,12
// @sweeps coupling, sight, glow
// @still 20.9
// @teaches Coupled oscillators: particles that see each other blink in time
// @category Compute
// @tags fireflies, synchronization, coupled oscillators, kuramoto model, particles, compute shaders, swarm, procedural sound
// @final Fireflies | Every thirty seconds the kiln door opens: its glare scatters every firefly's beat, and the meadow finds it again.
//
// A summer night in the meadow below the kiln. Each firefly is a clock: a
// phase that runs from 0 to 1 and flashes as it wraps round, each a little
// faster or slower than the others. Every firefly sees the ones near it and,
// at every step, nudges its phase toward theirs: forward if they are ahead,
// back if they are behind. That is Kuramoto's model of coupled oscillators,
// from 1975, and out of it comes what the fireflies of the Great Smoky
// Mountains do: patches fall into step, the patches pull on each other, and
// soon the whole meadow flashes as one. Every thirty seconds the kiln door
// opens, its glare scatters their beat, and they find it again.
// Each @param line below becomes a slider in the host app and a row in the
// page's parameters table:
// // @param name default min max description
//
// @particles 2048
// @param coupling 0.3 0.0 1.0 How hard each firefly pulls its beat toward the flashes it sees
// @param sight 0.25 0.1 0.5 How far a firefly sees the others, as a fraction of the picture's height
// @param spread 0.06 0.0 0.3 How different the fireflies' own rhythms are
// @param drift 1.0 0.0 2.0 How fast the fireflies wander
// @param glow 1.0 0.3 2.0 How bright a flash is
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The kiln door opens every CYCLE seconds.
constant float CYCLE = 30.0;
// The fireflies' rhythm, on average: flashes a second.
constant float BEAT = 0.75;
// They fly from just above the bottom of the picture up to a ceiling of
// each one's own, so they thin out with height.
constant float LOW = -0.85;
constant float HIGH = 0.5;
float ceilingOf(float seed) { return mix(-0.35, HIGH, seed); }
// A firefly: where it is, which way it wanders, its phase, its own rhythm in
// flashes a second, and a random number of its own.
struct Particle {
float2 position;
float heading;
float phase;
float rate;
float seed;
};
// Cracked Clay's integer hash.
float hashU(uint n) {
n = (n << 13u) ^ n;
n = n * (n * n * 15731u + 789221u) + 1376312589u;
return float(n & 0x7fffffffu) / float(0x7fffffff);
}
// A flash just after the phase wraps round: a quick rise, a slower fade.
float blink(float phase) {
return smoothstep(0.0, 0.025, phase) * exp(-phase * 14.0);
}
// Its glare: up at once, gone in a few seconds.
float glareAt(float t) {
float x = fmod(t, CYCLE);
return smoothstep(0.0, 0.12, x) * exp(-x * 1.6);
}
// Each starts anywhere in the meadow, wandering any way, at a random point
// of its cycle, with a rhythm near BEAT: spread says how near.
Particle spawn(uint id, constant Params& p) {
Particle q;
float aspect = 16.0 / 9.0;
q.seed = hashU(id * 6u + 4u);
float x = (hashU(id * 6u) * 2.0 - 1.0) * (aspect + 0.1);
float y = mix(LOW, ceilingOf(q.seed), hashU(id * 6u + 1u));
q.position = float2(x, y);
q.heading = TAU * hashU(id * 6u + 2u);
q.phase = hashU(id * 6u + 3u);
float r = 2.0 * hashU(id * 6u + 5u) - 1.0;
q.rate = BEAT * (1.0 + p.spread * r);
return q;
}
// One step: the phase runs on at its own rhythm and wraps at 1.
void update(thread Particle& q, uint id,
constant Uniforms& u, constant Params& p, Swarm swarm) {
float dt = u.timeDelta;
float aspect = u.resolution.x / u.resolution.y;
// Each firefly it can see pulls on its phase by the sine of how far
// ahead that one is, nearer ones harder.
float sight = p.sight * 2.0;
float pull = 0.0, seen = 0.0;
for (uint j = 0; j < swarm.count; j++) {
if (j == id) continue;
Particle o = swarm.at(j);
float d = length(q.position - o.position);
if (d >= sight) continue;
float w = 1.0 - d / sight;
pull += w * sin(TAU * (o.phase - q.phase));
seen += w;
}
// Kuramoto: its own rhythm, plus the coupling times the average pull.
float speed = q.rate;
if (seen > 0.0) speed += p.coupling * pull / seen;
q.phase = fract(q.phase + speed * dt);
// As it opens, each firefly loses its phase.
float cycle = floor(u.time / CYCLE);
if (cycle != floor((u.time - dt) / CYCLE)) {
q.phase = hashU(id * 7919u + uint(cycle) * 104729u);
}
// The heading turns a little at random. The grass and its ceiling turn it
// back; the sides wrap round past the edge of the picture.
uint k = id * 3u + u.frame * 7919u;
q.heading += (hashU(k) - 0.5) * 0.6;
float2 dir = float2(cos(q.heading), sin(q.heading));
bool under = q.position.y < LOW && dir.y < 0.0;
bool over = q.position.y > ceilingOf(q.seed) && dir.y > 0.0;
if (under || over) q.heading = -q.heading;
q.position += dir * 0.15 * p.drift * (0.6 + 0.8 * q.seed) * dt;
float edge = aspect + 0.1;
if (q.position.x > edge) q.position.x -= 2.0 * edge;
if (q.position.x < -edge) q.position.x += 2.0 * edge;
}
// Where to draw a firefly (the picture's uv) and how big its glow is.
float3 place(Particle q, constant Uniforms& u, constant Params& p) {
float aspect = u.resolution.x / u.resolution.y;
float2 uv = q.position / float2(aspect, 1.0) * 0.5 + 0.5;
return float3(uv, 0.04 + 0.015 * q.seed);
}
// The kiln palette from Flowing Color Field.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.78, t));
c = mix(c, c4, smoothstep(0.78, 1.00, t));
return c;
}
// Its light: a near-white core in a halo of gold turned a little green,
// as bright as its flash, and never quite out.
float4 ink(Particle q, float2 at, constant Uniforms& u, constant Params& p) {
float r = length(at);
float light = blink(q.phase) * p.glow + 0.015;
float3 halo = kiln(0.72) * float3(0.85, 1.05, 0.45);
float3 core = kiln(0.95) * float3(0.95, 1.0, 0.7);
float3 c = core * exp(-r * r * 45.0) * 2.0;
c += halo * exp(-r * r * 6.0) * 0.5;
return float4(c * light * smoothstep(1.0, 0.7, r), 1.0);
}
// What is left of the last step's light: a little, so a flash lingers.
float4 fade(float2 uv, Trails previous,
constant Uniforms& u, constant Params& p) {
return previous.at(uv) * 0.6;
}
// The night sky: deep violet, warmer down at the trees, with a few stars.
float3 sky(float2 uv, float aspect) {
float3 low = float3(0.085, 0.060, 0.120);
float3 high = float3(0.012, 0.010, 0.032);
float3 c = mix(low, high, smoothstep(0.33, 1.0, uv.y));
float2 s = uv * float2(aspect, 1.0) * 90.0;
float2 cell = floor(s);
float h = hashU(uint(cell.x + 4000.0) * 977u + uint(cell.y) * 131u);
float star = step(0.993, h) * smoothstep(0.25, 0.0, length(fract(s) - 0.5));
c += float3(0.5, 0.48, 0.42) * star * smoothstep(0.45, 0.7, uv.y);
return c;
}
// The land, lit by the fireflies and the open door.
float3 land(float2 uv, float3 c, float aspect, float lit, float glare,
float t) {
float x = uv.x * aspect;
float trees = 0.34 + 0.035 * sin(x * 5.1) + 0.02 * sin(x * 13.7 + 1.0);
trees += 0.012 * sin(x * 37.0);
float3 dark = float3(0.012, 0.012, 0.018);
c = mix(c, dark, smoothstep(0.002, -0.002, uv.y - trees));
float2 k = float2(x - 0.8 * aspect, uv.y - 0.39);
float hill = uv.y - (0.385 - 0.35 * k.x * k.x);
float dome = length(k * float2(1.0, 1.25)) - 0.075;
float stack = max(abs(k.x - 0.03) - 0.011, abs(k.y - 0.09) - 0.075);
float solid = min(min(hill, dome), stack);
float3 brick = float3(0.016, 0.013, 0.016);
c = mix(c, brick, smoothstep(0.002, -0.002, solid));
float door = length((k - float2(0.0, 0.018)) * float2(1.6, 1.0)) - 0.024;
float flicker = 0.85 + 0.15 * sin(t * 9.0) * sin(t * 5.3);
float inDoor = smoothstep(0.003, -0.001, door);
c += kiln(0.6 + 0.35 * glare) * inDoor * flicker * (0.4 + 0.8 * glare);
// Open, the door's light spreads over the whole meadow.
float spill = exp(-max(door, 0.0) * (40.0 - 30.0 * glare));
c += kiln(0.55) * spill * (0.06 + 0.19 * glare) * flicker;
c += kiln(0.5) * glare * 0.45 * exp(-length(k) * 1.3);
// Each blade a sharp peak: a sine raised to a high power.
float blade = 0.07 + 0.02 * sin(x * 3.0);
blade += 0.09 * pow(abs(sin(x * 41.0)), 9.0);
blade += 0.06 * pow(abs(sin(x * 67.0 + 1.0)), 7.0);
blade += 0.11 * pow(abs(sin(x * 17.0 + 2.0)), 24.0);
float grass = uv.y - blade;
float3 g = float3(0.008, 0.012, 0.010);
float tip = smoothstep(-0.1, 0.0, grass);
g += float3(0.10, 0.13, 0.04) * lit * tip + kiln(0.55) * glare * 0.25 * tip;
return mix(c, g, smoothstep(0.003, -0.003, grass));
}
// The picture. The light at 12 points, averaged, says how lit the meadow
// is: a flash together brightens the sky and the grass.
float4 shade(float2 uv, constant Uniforms& u, constant Params& p,
Trails trails) {
float aspect = u.resolution.x / u.resolution.y;
float3 all = float3(0.0);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
float2 at = float2((float(i) + 0.5) / 4.0, 0.15 + 0.22 * float(j));
all += trails.at(at).rgb;
}
}
float lit = min(dot(all, float3(0.33)) * 0.5, 1.0);
float glare = glareAt(u.time);
float3 c = sky(uv, aspect);
c += float3(0.04, 0.05, 0.02) * lit * smoothstep(0.7, 0.3, uv.y);
c = land(uv, c, aspect, lit, glare, u.time);
float3 light = trails.at(uv).rgb;
c += light / (1.0 + light) * 1.2;
float2 q = uv - 0.5;
c *= 1.0 - dot(q, q) * 0.6;
return float4(c, 1.0);
}
// A smooth random value from 0 to 1 that changes `rate` times a second:
// two hashed values eased into each other.
float noise1(float t, float rate, uint salt) {
float x = t * rate;
float i = floor(x);
float f = x - i;
f = f * f * (3.0 - 2.0 * f);
uint n = uint(int(i) + 1000000);
return mix(hashU(n * 31u + salt), hashU((n + 1u) * 31u + salt), f);
}
// The sound can't see the swarm, so it tells the same story from the clock.
// Twelve bells on A major pentatonic, each ringing as its own phase wraps.
// Each cycle they start scattered, and the gaps between them shrink with
// the seconds since the door opened, faster with more coupling, until they
// ring together. Under them, two crickets, the kiln's hum, and the roar of
// the door while the bells are silent.
float2 sound(float t, constant Params& p) {
float x = fmod(t, CYCLE);
uint cycle = uint(floor(t / CYCLE));
// Silent through the glare and just before the next: no jump is heard.
float gate = smoothstep(1.2, 2.4, x) * smoothstep(CYCLE, CYCLE - 0.4, x);
float together = 1.0 - exp(-max(x - 1.0, 0.0) * 0.6 * p.coupling);
float notes[6] = { 880.0, 987.8, 1108.7, 1318.5, 1480.0, 1760.0 };
float2 bells = float2(0.0);
for (uint i = 0; i < 12; i++) {
float apart = hashU(i * 13u + cycle * 101u);
float phase = BEAT * t + apart * (1.0 - together);
float age = fract(phase) / BEAT;
float env = smoothstep(0.0, 0.004, age) * exp(-age * 7.0);
float f = notes[i % 6] * (i >= 6 ? 0.5 : 1.0);
float ring = sin(TAU * f * age);
ring += 0.25 * sin(TAU * f * 2.76 * age) * exp(-age * 12.0);
float pan = 0.15 + 0.7 * hashU(i * 7u + 3u);
bells += float2(1.0 - pan, pan) * ring * env;
}
bells *= gate * 0.2;
// Two crickets: a trill pulsed 30 times a second, in chirps of a third
// of a second.
float2 crickets = float2(0.0);
for (uint c = 0; c < 2; c++) {
float period = 1.1 + 0.27 * float(c);
float n = floor(t / period);
float age = t - n * period - 0.2 * hashU(uint(n) * 3u + c);
float on = smoothstep(0.0, 0.03, age) * smoothstep(0.34, 0.26, age);
float chirp = age > 0.0 ? on : 0.0;
float pulse = 0.5 + 0.5 * sin(TAU * 30.0 * t + float(c));
float f = 4300.0 + 180.0 * float(c);
float s = sin(TAU * f * t) * chirp * pulse * pulse * 0.07;
crickets += c == 0 ? float2(s * 0.8, s * 0.2) : float2(s * 0.25, s * 0.75);
}
// The kiln's hum, and the roar of the door: noise at three rates.
float hum = (sin(TAU * 55.0 * t) + 0.5 * sin(TAU * 82.5 * t)) * 0.1;
float roar = noise1(t, 900.0, 3u) - 0.5;
roar += 0.7 * (noise1(t, 220.0, 7u) - 0.5);
roar += 0.5 * (noise1(t, 60.0, 11u) - 0.5);
roar *= glareAt(t) * 1.4;
float2 door = float2(0.35, 0.65) * roar;
float2 v = bells + crickets + float2(hum) + door;
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(v * 1.6) / 1.4 * fadeIn * p.volume;
}Running it in your own project
Every Ray Kiln shader is the same shape. The standalone download above is the shader with the small wrapper around it that the Ray Kiln host adds, so it compiles with the ordinary Metal compiler and runs in your own app. It defines three entry points: rk_vertex (a full-screen triangle), rk_fragment (calls the shader's shade) and, for shaders with sound, the compute kernel rk_sound.
To draw it, pass the uniforms at fragment buffer 0 and the parameter values at buffer 1, in the order they are declared:
Drawing it, in Swift
struct Uniforms {
var resolution: SIMD2<Float>
var time: Float
var timeDelta: Float
var mouse: SIMD2<Float>
var frame: UInt32
var pad: UInt32 = 0
}
let library = try device.makeLibrary(source: standaloneSource, options: nil)
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "rk_vertex")
descriptor.fragmentFunction = library.makeFunction(name: "rk_fragment")
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipeline = try device.makeRenderPipelineState(descriptor: descriptor)
// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [0.3, 0.25, 0.06, 1.0, 1.0, 0.8] // the @param defaults, in the order they are declared
encoder.setRenderPipelineState(pipeline)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0)
encoder.setFragmentBytes(¶ms, length: params.count * MemoryLayout<Float>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)Time is in seconds, mouse is in pixels from the bottom left, and uv in the shader runs 0 to 1 with (0, 0) at the bottom left. Write the output as sRGB: the shader's numbers go to the screen as they are.
More in Particles & Simulation
Press ? for keyboard shortcuts.


