Boids Flocking Simulation
by GameDev.net · WebGPU Compute (WGSL) · 25 Aug 2026
Separation, alignment, and cohesion applied to a small flock of agents. The agents live in a storage buffer, and the compute pass is an all-pairs O(n squared) update, which is fine for a teaching flock of 96.
What it demonstrates
Object-oriented compute: each agent has position, velocity, and a small view radius. The classic Reynolds steering rules. Toroidal wrap-around so the flock never hits a wall.
Common
const BOID_COUNT: u32 = 64u;
const VIEW_RADIUS: f32 = 0.12;
const PROTECTED_RADIUS: f32 = 0.025;
fn hash2(p: vec2<u32>) -> f32 {
var h = p.x * 374761393u + p.y * 668265263u;
h = (h ^ (h >> 13u)) * 1274126177u;
return f32(h ^ (h >> 16u)) / 4294967295.0;
}
fn wrap01(p: vec2<f32>) -> vec2<f32> {
return fract(p);
}
Compute (flock)3
// Boids flocking: separation, alignment, and cohesion. The compute pass is
// O(n²) over the boid list, which is small enough to be a teaching example.
@compute @workgroup_size(1, 1, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let id = gid.x;
if (id >= BOID_COUNT) {
return;
}
var pos: vec2<f32>;
var vel: vec2<f32>;
if (uniforms.frame == 0u) {
pos = vec2<f32>(hash2(vec2<u32>(id, 0u)), hash2(vec2<u32>(id, 1u)));
let angle = hash2(vec2<u32>(id, 2u)) * 6.283185;
let speed = 0.12 + hash2(vec2<u32>(id, 3u)) * 0.08;
vel = vec2<f32>(cos(angle), sin(angle)) * speed;
} else {
pos = output[id].xy;
vel = output[id].zw;
}
var sep = vec2<f32>(0.0);
var align = vec2<f32>(0.0);
var cohere = vec2<f32>(0.0);
var count = 0u;
for (var i = 0u; i < BOID_COUNT; i = i + 1u) {
if (i == id) { continue; }
let otherPos = output[i].xy;
var delta = otherPos - pos;
// Wrap-aware shortest delta on the torus.
delta = delta - round(delta);
let dist = length(delta);
if (dist < VIEW_RADIUS && dist > 0.0001) {
if (dist < PROTECTED_RADIUS) {
sep = sep - normalize(delta) / dist;
}
align = align + output[i].zw;
cohere = cohere + delta;
count = count + 1u;
}
}
if (count > 0u) {
let fCount = f32(count);
sep = sep * 0.0008;
align = (align / fCount - vel) * 0.0004;
cohere = (cohere / fCount) * 0.0002;
}
vel = vel + sep + align + cohere;
// Keep the flock moving at a steady speed.
let speed = length(vel);
let desiredSpeed = 0.12;
if (speed > 0.0001) {
vel = (vel / speed) * clamp(speed, desiredSpeed * 0.5, desiredSpeed * 1.5);
}
pos = pos + vel * uniforms.time_delta;
pos = wrap01(pos);
output[BOID_COUNT + id] = vec4<f32>(pos, vel);
}
Compute (copy)
// Commit the next boid state after every invocation has finished reading the
// current list. Keeping current and next states in separate buffer ranges avoids
// a data race between flocking workgroups.
@compute @workgroup_size(1, 1, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let id = gid.x;
if (id >= BOID_COUNT) {
return;
}
output[id] = output[BOID_COUNT + id];
}
Main Image
// Render each boid as a small arrow-ish glow. The boid list is stored in the
// first BOID_COUNT pixels of the output buffer.
@fragment
fn main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / uniforms.resolution;
var colour = vec3<f32>(0.04, 0.06, 0.08);
for (var i = 0u; i < BOID_COUNT; i = i + 1u) {
let p = output[i].xy;
let vel = output[i].zw;
var delta = uv - p;
delta = delta - round(delta);
let d = length(delta);
if (d < 0.025) {
let dir = vel / (length(vel) + 0.001);
let c = vec3<f32>(dir * 0.5 + 0.5, 1.0);
let a = 1.0 - smoothstep(0.0, 0.025, d);
colour = mix(colour, c, a);
}
}
return vec4<f32>(colour, 1.0);
}
Learn from this shader
How it works
Every boid looks at every other boid within a view radius. Inside a protected radius it steers away, which is separation. It averages nearby velocities, which is alignment, and nearby positions, which is cohesion. The three steering forces are summed, the velocity is clamped to a steady speed, and the position is integrated. The image pass draws each boid as a small coloured dot using its velocity direction for hue.
Try changing
Adjust VIEW_RADIUS or PROTECTED_RADIUS to change flock density. Change the weights of separation, alignment, and cohesion to see different flock types. Add a predator boid that the others flee from. Increase BOID_COUNT and the dispatch size to stress the O(n squared) approach.
Using it in a game
Flocking is used for birds, fish, crowds, and swarm enemies. A production flock uses spatial partitioning, such as a grid or tree, to avoid the O(n squared) cost, but the rules are exactly the same.
Explore the techniques
Continue with curated explanations and progressively related examples.
Discussion