Skip to main content
GameDev.net gamedev.net

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.

Source Revision 4

Author notes are linked to specific lines.

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
1 // Boids flocking: separation, alignment, and cohesion. The compute pass is
2 // O(n²) over the boid list, which is small enough to be a teaching example.
3
4 @compute @workgroup_size(1, 1, 1)
5 fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
6 let id = gid.x;
7 if (id >= BOID_COUNT) {
8 return;
9 }
10
11 var pos: vec2<f32>;
12 var vel: vec2<f32>;
13
14 if (uniforms.frame == 0u) {
15 pos = vec2<f32>(hash2(vec2<u32>(id, 0u)), hash2(vec2<u32>(id, 1u)));
16 let angle = hash2(vec2<u32>(id, 2u)) * 6.283185;
17 let speed = 0.12 + hash2(vec2<u32>(id, 3u)) * 0.08;
18 vel = vec2<f32>(cos(angle), sin(angle)) * speed;
19 } else {
20 pos = output[id].xy;
21 vel = output[id].zw;
22 }
23
24 var sep = vec2<f32>(0.0);
25 var align = vec2<f32>(0.0);
26 var cohere = vec2<f32>(0.0);
27 var count = 0u;
28
29 for (var i = 0u; i < BOID_COUNT; i = i + 1u) {
30 if (i == id) { continue; }
31 let otherPos = output[i].xy;
32 var delta = otherPos - pos;
33 // Wrap-aware shortest delta on the torus.
34 delta = delta - round(delta);
35 let dist = length(delta);
36 if (dist < VIEW_RADIUS && dist > 0.0001) {
37 if (dist < PROTECTED_RADIUS) {
38 sep = sep - normalize(delta) / dist;
39 }
40 align = align + output[i].zw;
41 cohere = cohere + delta;
42 count = count + 1u;
43 }
44 }
45
46 if (count > 0u) {
47 let fCount = f32(count);
48 sep = sep * 0.0008;
49 align = (align / fCount - vel) * 0.0004;
50 cohere = (cohere / fCount) * 0.0002;
51 }
52
53 vel = vel + sep + align + cohere;
54
55 // Keep the flock moving at a steady speed.
56 let speed = length(vel);
57 let desiredSpeed = 0.12;
58 if (speed > 0.0001) {
59 vel = (vel / speed) * clamp(speed, desiredSpeed * 0.5, desiredSpeed * 1.5);
60 }
61
62 pos = pos + vel * uniforms.time_delta;
63 pos = wrap01(pos);
64
65 output[BOID_COUNT + id] = vec4<f32>(pos, vel);
66 }
67
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.

LicenseMIT
Views23
Forks0

Discussion

Loading comments...