Lights in the meadow
If you read my post about the traveling salesman problem, you know I have a soft spot for algorithms that steal their ideas from nature. Ants leaving pheromone trails, genes crossing over and mutating, that sort of thing. Swarm intelligence in particular has had me hooked for years. You take a bunch of simple creatures, give each of them a handful of dumb rules that only care about what's right around them, and out of nowhere you get behaviour that looks like it was carefully planned. Nobody is in charge, nobody knows the big picture, and still the whole thing works. I find that endlessly fascinating, and I wanted to build something with it that isn't just a dry demo of an algorithm, but something you can actually play with and get lost in for a while.
So: a meadow at night, and fireflies. What started out as "let's flock a few thousand dots around the screen" slowly grew into two worlds in one demo. In the swarm, up to a million fireflies flock together, steered by nothing more than three rules each one applies to the friends it can see. In the colony, a few hundred fireflies live off a hollow stump in the corner of the meadow. They get hungry, go looking for nectar, recruit their friends, sync their flashes, plant new flowers without even knowing it, and die when the food runs out. On top of all that, there's music that's generated on the spot and listens to what happens on the meadow. Everything runs in Rust, compiled to WebAssembly, and on your graphics card through WebGPU if your browser supports it. Fair warning: this one got a bit out of hand. In a good way, I'd like to think ๐
Try it right here: [space] starts it, [w] switches between the colony and the swarm, [m] turns on the sound (do it, it's worth it), [x] goes fullscreen and [h] help shows all the controls. With a keyboard, click the meadow and press the keys between the brackets. On a phone, [โฆ] more has a button for each of them.
A quick tour
Before we dig into how it all works, let me show you around. There are four tools, which you pick with the keys 1 to 4, and you use them by clicking or tapping on the meadow:
- attract: the swarm streams to the spot you tapped and circles around it, until you tap the mark again
- flower: in the colony, drops a flower full of nectar on the meadow
- rock: places a rock, or removes the one you tap
- predator: follows your mouse, or your finger while you hold it down, and everything around it scatters
The rocks are shared between both modes, by the way. The colony's meadow is simply the middle of the swarm's world, so a rock you place in one is there in the other too. In the swarm, โ and โ give you more or fewer fireflies, and three sliders let you tune the rules of the flock, which is where most of the fun is. The colony has no count to set: it decides its own size, as you'll see. At the bottom, a status line keeps you posted on what's going on, from how many fireflies there are and how full the colony's store of nectar is, to how many flowers are blooming and how fast your machine is rendering it all.
Three rules, that's all it takes
Let's start with the swarm, since that's where it all began. The model behind it is a classic: boids, thought up by Craig Reynolds back in 1986 to simulate flocking birds. Every firefly looks at the other fireflies within 28 units of it, and follows three rules. Separation: fly away from the ones closer than 8 units, harder the closer they get. Alignment: turn towards the average heading of the ones you see. Cohesion: drift towards the centre of the ones you see. And that's really it. There's no leader, no plan, not even a notion of what a flock is. Yet out of those three rules come streams, swirls, flocks that split up around a rock and find each other again behind it, and waves that roll through the whole swarm when something spooks one corner of it.

The sliders let you mess with the balance between the three, from switched off to three times their default, and I'd really encourage you to play around with them. Crank up separation and the swarm spreads out into an even, grainy sheet of light. Crank up alignment and you get long rivers of fireflies all flying the same way. Push cohesion and they clump together into tight balls that bump into each other and merge. Switch alignment off entirely, and the whole thing falls apart into a buzzing, aimless cloud. It's a great way to get a feel for what each rule actually contributes. Here's the same swarm with alignment cranked all the way up, and then with cohesion cranked up and separation turned down:


Three rules make a nice textbook example, but they don't make something that feels alive, so there's a bit more to it. Fireflies within 3 units of each other always bump apart, whatever the separation slider says. Without that personal space, switching separation off would collapse the entire swarm onto a single point, which is funny for about two seconds. Every firefly flies somewhere between 1.8 and 3.2 units per step, so nobody stops dead and nobody races off, and every step it gets a tiny random nudge, so the swarm never freezes into some perfect and very boring pattern. Rocks push away any firefly that comes within 26 units of their edge, harder the closer it gets, which is what makes flocks part around them so nicely. The predator scares everything within 120 units, and that reach grows when the camera zooms out, so it always scares the same patch of your screen. And the edges of the world push back softly over their last 70 units, so the swarm turns before it hits the wall instead of piling up against it.

Who's around?
Here's the catch with boids: every firefly needs to know who's around it. Do that the naive way, with every firefly checking every other firefly, and 20,000 fireflies means 400 million distance checks. Per step. At 60 steps a second. Yeah, that's not going to happen.
The usual way around that is a grid. You chop the meadow up into cells, and a firefly only looks in the cells around it. I took that one step further, and sort the fireflies by their cell every single step, with a counting sort:
/// Sorts the fireflies by cell: a counting sort, two passes over them
First we count how many fireflies each cell holds. Then we turn those counts into the position where each cell starts, by adding up all the counts before it. Finally, every firefly gets dropped into the next free spot of its cell. Two passes over the fireflies, and not a single comparison: no sorting in the quicksort sense at all. The beauty of it is that afterwards, the fireflies of a cell sit right next to each other in memory. Even better, the cells of one row of the grid are next to each other too, so all the cells around a firefly within one row form a single stretch of memory, which you can just walk through from start to end. Before settling on this, I measured it against a grid of linked lists, which is what you'll find in most boids tutorials out there. In WebAssembly, the counting sort won by about 23%. You'll also notice the fireflies aren't stored as a list of structs, but as a structure of arrays: all the x positions in one array, all the y positions in another, and so on. The hot loops read memory in a straight line that way, and your CPU loves that.
Sums instead of neighbours
That takes care of separation, which only cares about fireflies within 8 units. Alignment and cohesion are another story, though. They look 28 units out, and in a dense swarm that easily means hundreds of neighbours per firefly. I was staring at that for a while, until it hit me that alignment and cohesion don't actually care about the individual neighbours at all. All they need are totals: how many neighbours there are, their headings added up, and their positions added up. Divide by the count, and you've got the average heading and the centre.
So the meadow also gets split up into tiny squares of 4 units, and every square adds up its own fireflies. A firefly then only has to add up the squares within its view. And to make adding up squares cheap as well, every row of squares keeps running totals: entry col + 1 holds everything from the start of the row up to and including square col. Any stretch of a row then comes down to a single subtraction:
/// Count, vx, vy, x and y added up over the squares whose centre is within `PERCEPTION`
/// of (x, y), the firefly there included
For every row of squares that falls within the firefly's circle of view, we work out how far left and right that circle reaches at the height of the row, and read the running totals at both ends. Two lookups per row, whether the row holds 3 fireflies or 300. A firefly in the thickest part of the swarm now costs about the same as a loner out on the edge. Rows without any fireflies get skipped altogether, and only the stretch of a row that actually holds fireflies is ever written and cleared, so a sparse swarm doesn't pay for all the empty meadow around it either. Quite simple, and quite powerful.
On the CPU, that gets you up to 20,000 fireflies at 60 steps a second, in your browser. But we're not stopping there, are we?
A million on the GPU
Where your browser supports WebGPU, the swarm moves over to your graphics card, and the slider goes all the way up to a million. I'll be honest: the first time I saw a million little lights flocking across my screen, I sat there grinning like an idiot for a good while. This is what that looks like fullscreen, a few seconds after pulling the slider all the way up, with the newcomers still streaming in and merging into one big sheet of light:

Getting there meant rebuilding the entire step as compute shaders, in WGSL. Luckily, the counting sort from before translates surprisingly well to the GPU. In a first pass, every firefly at the same time does an atomic add on the counter of its cell, which also hands it its place within that cell. Then a parallel prefix sum turns the counts into the position where every cell starts: the cells get scanned in blocks of 1024 inside a workgroup, then the totals of those blocks get scanned, and those get added back onto every block. A last pass scatters every firefly to its sorted spot. After that, every cell adds up its fireflies, just like the squares did on the CPU:
// Each cell of the grid summed up, for steer with cells: its fireflies are next to each other
@compute @workgroup_size(256)
fn sumCells(@builtin(global_invocation_id) id: vec3u) {
if (id.x + 1u >= params.cells) {
return;
}
var sum = vec4f(0.0);
for (var j = starts[id.x]; j < starts[id.x + 1u]; j++) {
sum += sorted[j];
}
rockLight[CELL_SUMS + id.x] = sum;
}
A firefly on the GPU is a single vec4f holding its position and its heading, so adding them up gives you the summed positions and the summed headings in one go. And yes, those sums are stored in the buffer that holds the light the rocks catch, right after the rocks. A shader stage is only guaranteed 8 storage buffers, the steering already used all 8 of them, and I wanted this to keep working on phones. Not my proudest line of code, but it does the job ๐ Steering then takes alignment and cohesion from every cell whose middle the firefly can see:
for (var row = max(home.y - reach, 0); row <= min(home.y + reach, lastCell.y); row++) {
for (var col = max(home.x - reach, 0); col <= min(home.x + reach, lastCell.x); col++) {
let middle = (vec2f(f32(col), f32(row)) + 0.5) * GRID - p;
if (dot(middle, middle) > PERCEPTION * PERCEPTION) {
continue;
}
let cell = u32(row) * params.cols + u32(col);
let n = f32(starts[cell + 1u] - starts[cell]);
let sum = rockLight[CELL_SUMS + cell];
seen += n;
heading += sum.zw;
centre += sum.xy - n * p;
}
}
// Its own cell, always within sight, has itself in it
seen -= 1.0;
heading -= v;
Separation is still done one by one, from the firefly's own cell and the ones right next to it. I did try visiting every single neighbour exactly as well, and compared the two. On my desktop card, a full step at a million fireflies takes 1.61 ms with cells, against 4.25 ms going one by one. That's about 2.6 times faster, and I honestly couldn't tell the difference on screen. Cells it is.
There's a lot more going on in a GPU step, though. When you ask for fewer fireflies, the GPU has to decide who leaves. A histogram of 4096 bins counts how far every firefly is from the nearest edge, and from that the GPU knows exactly which fireflies are closest to an edge, and exactly that many fly out over it. When you ask for more, newcomers fly in as flocks. The glow is done on the GPU as well: every firefly splats its light into a map at a quarter of the screen's resolution, using atomic adds in fixed point (GPUs and floating point atomics don't really mix), and that map gets blurred in four passes and drawn with a tone curve. Every rock gets a workgroup of its own that adds up the fireflies around it, weighed by their distance, so the rock gets a rim of light on the side where the fireflies are. And the shader even counts how aligned every firefly is with the ones it sees, so the music can listen to the swarm, but more on that later. What you see on screen is actually three canvases stacked on top of each other: the meadow on a regular 2D canvas, the swarm on a WebGPU canvas that adds its light onto the meadow with plus-lighter, and the marks of your tools on a third one over that. And when your browser doesn't have WebGPU, or it turns out to be your CPU pretending to be a GPU (which is slower than the CPU swarm itself), or anything at all goes wrong while setting it up, the swarm quietly falls back to the CPU. No error messages, just fewer fireflies.
A world that grows
Now, a million fireflies don't fit on one meadow. Well, they do, but it isn't pretty. So once the meadow gets too crowded (6000 fireflies per meadow on the CPU, 48,000 on the GPU), the world grows along with the count, and the camera glides out to show all of it. That sounds simple enough. It wasn't. The newcomers fly in as flocks of 40, into the land the world just gained, and all of them arrive within a second and a half. My first version had them fly in over the edges instead, which at 300,000 fireflies gave you this weird glowing ring around the swarm. Not great. Shrinking works the other way around: the camera glides in first, the fireflies it leaves behind disappear out of sight, and once it's down, the world shrinks to what it sees.
The ground itself is built from tiles, each grown from a random seed of its own spot in the world. That way you can grow the world, shrink it and grow it again, and the land around the meadow always looks exactly the same. Rocks belong to the land as well, so a rock that got left outside a shrunken world is back when it grows again. Drawing that ground for a world four and a half meadows wide means tens of thousands of grass tufts, though, and doing that all at once froze the frame for a few hundred milliseconds. So these days it gets painted a little bit every frame, from the middle outwards, while the camera rises.
One last thing that surprised me: seen from up high, a swarm looks slow, even when every single firefly flies exactly as fast as before. It's just further away. So on the GPU, the swarm takes 60 steps a second times the square root of how many meadows wide the view is. At a million fireflies, that's about 128 steps a second, and it feels just as lively as a small swarm up close.
The colony
Flocking is pretty, but it isn't really a life. So the second mode is a small ecosystem. It starts with 50 fireflies around a hollow stump in the bottom left corner of the meadow, which holds a store of nectar that starts out 60% full. The colony can grow to 500 fireflies. If the meadow can feed them, that is.

In the colony, light is life. Every firefly glows as brightly as it has energy, so a hungry colony visibly dims. Flying burns energy, and a full firefly lasts about two minutes. Once it drops under 25%, it heads home to eat, and sits on the stump for up to three seconds eating from the store until it's full. A firefly that comes home half full eats half as long and takes half as much. Tired fireflies slowly shift from bright green towards the colours of an ember, and a firefly that runs out of energy completely settles down where it is, and its light fades out over two seconds, like an ember going out. It's a bit sad, honestly.
New fireflies come out of the store. Whenever it's more than 80% full, the store spends 15% of its nectar on a new firefly, which rises out of the hollow full of energy, with at least a second and a half between two births. Once the colony is at its full size and the store is full, it starts behaving like a beehive: carriers still deliver their last sip, but nobody recruits anymore, and foragers that come home to a full store go back to wandering. There's no point bringing food home when there's no room for it. And when every last firefly is gone, one dormant spark stays asleep in the stump. As soon as a flower has nectar again, it wakes up and flies straight to the nearest one. The last of the colony knows where to go. So the colony can come back from extinction, but only if you give it a hand.
Foraging and recruitment
The store only ever runs down by itself. Food comes from flowers, which you can drop on the meadow as long as there are fewer than 3 of them. After that, it's up to the fireflies themselves, but I'll come back to that. A wandering firefly finds a flower by passing within 25 units of it. It lands, sits on the flower for a second to take one of its 90 sips, and carries that sip home, where it adds 1.2% to the store. So one whole flower is worth about a full store.
This is where it starts to look like real swarm intelligence. Only a firefly carrying a sip home can recruit others. When a wanderer comes within 24 units of it, the wanderer joins its flower with a 5% chance, for every step it stays close:
Look at the conditions in there for a second. Nobody recruits when the store is full. Only carriers recruit, and only for a flower that still has nectar left. Fireflies that are eating or dying can't be recruited, and at most 15 fireflies forage for any one flower. Put all of that together and something really nice happens: a rich flower close to home draws a crowd, and that crowd builds a stream of light between the flower and the stump. Exactly like an ant trail, just without the pheromones. Here's one, between a single flower and the stump. The fireflies glowing amber, with a pink drop of nectar at their head, are carrying a sip home:

A forager isn't stupid either. While it flies back and forth, it remembers another flower with nectar it passed along the way, and after a delivery it weighs the two:
let closer = match self.group.and_then ;
if closer && self.group_size < GROUP_CAP
It only switches when the other flower is clearly closer to home, at most 80% of the distance of its own flower. Without that margin, a firefly stuck between two flowers that are about equally far away would keep flipping between them like a confused tourist.
Flashing as one
Real fireflies do something amazing: some species flash in sync, whole trees full of them blinking at the very same moment. Nobody conducts them. Each firefly just nudges its own rhythm a little towards the flashes it sees around it. I really wanted that in. Every firefly flashes at its own pace, once every 70 to 110 steps, and the members of a foraging group pull their flashes together a little bit every step:
If that looks familiar, it's a Kuramoto model, the classic model for coupled oscillators. Think of every firefly's rhythm as a point going round a circle. Per group, we add up all those points as vectors, which gives us the group's average moment in the rhythm, and every member gets pulled a little bit towards it. The sine takes care of the strength of that pull: a firefly that's nearly in sync gets just a tiny nudge, and one that's way off gets a bigger one. Watch a new group form on a flower and you'll see it happen. It starts out blinking all over the place, and over a few seconds it settles into flashing as one. Wanderers keep their own rhythm. Free spirits, the lot of them.
Pollination
Flowers don't last forever, so a colony that wants to survive has to plant new ones. Not that it knows that, of course. Every sip leaves a seed stuck on the firefly with a 10% chance. After 5 to 20 seconds of flight, the seed falls off, floats down for three seconds, and drifts 80 to 300 units on the breeze while it falls, like the fluff of a dandelion. I only added that drift after noticing that seeds falling straight down all landed along the routes the fireflies fly, and those routes all end at the stump. You ended up with a neat ring of flowers around the home. Cute, but wrong. A seed that lands takes root with a 25% chance, but only where there's room for it: not on a rock, not within 60 units of another flower, and not within 150 units of the stump, the trampled ground where the whole colony lands and takes off. The sprout then grows for 30 seconds, from a shoot to a bud to a bloom, and only has nectar once it blooms. Every flower lives about three minutes, whether it gets foraged or not, and wilts over its last 30 seconds. An empty flower disappears as soon as nobody is carrying a sip of it home anymore, and there are never more than 12 flowers blooming at the same time.
You can follow all of it on the meadow if you look closely. A firefly with a seed stuck to it carries a small cream dot at its side. Seeds that came loose drift down as pale specks, and where one takes root, a little green shoot pops up. In this one, a fresh shoot in the top right sprouted next to three flowers in bloom:

Those numbers took a lot of tuning. I ran the colony natively for 10 simulated minutes, over and over with different random seeds, just to see what the meadow does when you leave it alone. With 60 sips per flower, the meadow was bare 41% of the time. With 120, flowers filled up the meadow, and the colony with them, within minutes. 90 sips, with a seed for every 10 sips, turned out to sit right on the edge: three dropped flowers keep a meadow of about 5 going by itself. Sometimes it blooms and the colony booms. Sometimes the flowers run out and the colony starves back down, until you lend it a hand. That edge is exactly where it gets interesting.
And since seeds drift on the breeze, the meadow needed a breeze. It slowly wanders around, a few tens of degrees a minute, and changes strength over a minute or so. It's pulled back towards the east a little, so it mostly blows away from the stump and carries the seeds out over the meadow. Every 20 to 45 seconds a gust swings it 30 to 60 degrees to one side within two seconds, blows a bit harder, and then eases back over six. You can see it too: little motes drift along with the breeze, gusts draw streaks across the meadow, and the flowers sway.
The music
Now turn on the sound with [m], because this is the part I'm proudest of. Everything you hear is generated on the spot with the Web Audio API. No recordings, no audio files, no loops. Just oscillators, noise and filters, driven by whatever happens on the meadow.
The colony sounds like a summer night. There's a bed of crickets, and a breeze made of brown noise that swells with the meadow's wind, getting louder and brighter as it picks up, with a rush for every gust. My first version used white noise for that, and it sounded exactly like a vacuum cleaner. Brown noise, much better. Over that plays a lydian handpan groove in D, and its tempo follows the colony: 75 beats a minute for the 50 fireflies it starts with, up to 100 at its full 500, and slowing down towards 60 as it dies out. You can literally hear the colony thrive or starve. It all comes from this little function:
A bar gets a harmony with a 30% chance, but only while a foraging group of at least 3 fireflies is flashing as one. So the sync on the meadow turns into harmony in the music, and if I had to pick one favourite detail in this whole project, that would be it. The flashes themselves play soft notes in the groove's scale, a lone wanderer now and then gets a soft and rare note of its own, and every event on the meadow has its own sound: births, deaths, deliveries, meals, seeds landing, sprouts, blooms and wilting flowers. Close your eyes and you can still follow what the colony is up to.
The swarm gets a different theme altogether: a mixolydian piece with drums, that builds up once at the start, layer by layer, two loops per layer. Its tempo goes from 95 beats a minute for the smallest swarm up to 130 for the biggest. Here, the chance of a harmony follows how aligned the fireflies are with the ones they see, times how tightly they flock compared to an even spread. A swarm streaming along in long, tight rivers sings in harmony, a scattered cloud doesn't, and fireflies that see nobody at all play the wanderer's note. On the GPU, those numbers come straight out of the shader: every firefly writes down how many others it saw and how aligned it was, the shader adds that up per workgroup, and the page reads it back a frame or two later, without ever waiting on the GPU.
Keeping it smooth
There are a few more things under the hood you'll never notice, which is exactly the point. The swarm steps 60 times a second, whatever the refresh rate of your screen, and between two steps every firefly gets drawn where it is in between, so it looks smooth at 240 Hz as well. A slow frame never makes the next one slower either: when the steps themselves can't keep up anymore, the swarm simply slows down instead of stuttering. The whole scene runs in a Web Worker, drawing on an OffscreenCanvas, so the page itself never has to wait for it. On the CPU, the glow works like on the GPU: the density of the swarm gets counted at a quarter of the screen's resolution, blurred, and run through a tone curve from a lookup table of 4096 entries. A dense swarm turns into a soft cloud of light instead of a white blob, with its hottest spots burning a pale yellow-white, and the fastest fireflies burn whiter, so you can see the panic ripple through the swarm when the predator shows up. The night itself, a meadow seen from above with patches of light and dark grass, about 1700 grass tufts, clover, a pond with lily pads, moonlight from the top right and darker edges, is drawn only once of course, not every frame. And on a phone, the status line tells you how many fireflies your device can keep up with at full speed. Go above that, and the swarm just flies a little slower.
Wrapping up
What looks like a screen full of little lights is really a model of flocking that scales from a hundred fireflies to a million, a small ecosystem with hunger, birth, death, foraging, recruitment, synchronised flashing, pollination and wind, and a soundtrack that listens to all of it. Every number in there got tuned by watching, simulating and measuring: how long a flower lives, how far a seed drifts, how often a gust blows, how hard a group pulls its flashes together.
And the funny thing is, not a single firefly knows any of it. Each one only follows a handful of simple rules about what it sees right around it. The streams, the swirls, the trails between flower and stump, the flashing in sync, the meadow blooming and dying back: it all emerges by itself. That's swarm intelligence for you, and I still think it's one of the coolest things in computing.
There's more to come. Next up is a natural enemy for the colony: a frog you drop on the meadow like a rock, which hops around and eats the fireflies that hang around it for too long. Until then, go drop a flower, turn on the sound, and see if the colony makes it through the night. Or not ๐