3D Flocking: Boids with Obstacles and a Leader
Reynolds' original three rules were built for a flat plane. Take the same flock into full 3D — through a cave system, around a skyscraper, behind a migrating leader — and every part of the algorithm from neighbour search to orientation needs an extra dimension of care.
1. From 2D to 3D: the extra degree of freedom
The original 1986 Boids demo ran in a flat plane. Adding a third spatial dimension is more than "add a z". Every neighbour query must now search a volume of space instead of an area, meaning a uniform grid checks 27 neighbouring cells (3×3×3) instead of 9, and orientation is no longer a single heading angle θ but a full 3D rotation.
Birds, fish and drones also bank into turns — they roll around their direction of travel, not just yaw toward it. Capturing this requires either Euler angles (simple but suffers from gimbal lock) or quaternions (numerically stable, the standard choice in production 3D engines).
Separation, alignment and cohesion carry over unchanged as 3D vector operations — see our 2D Boids article for the base rules. This article focuses on what is genuinely new in 3D: obstacles, a leader, and orientation.
2. Obstacle avoidance with ray casting
Static geometry — cave walls, buildings, terrain — must deflect the flock without breaking its cohesion. The standard technique casts one or more probe rays forward from each boid:
hit = intersect(ray, obstacles)
if hit: v_avoid = reflect(vel[i], hit.normal) · weight_avoid
- Single forward ray — cheap, but misses obstacles the boid is about to clip sideways.
- Three-ray fan (centre + two offset by a small angle) — catches near-misses at a modest extra cost.
- Sphere-cast — sweeps a sphere of the boid's own radius along the path, guaranteeing no part of the body clips the obstacle.
Obstacles themselves are almost always simplified to spheres or capsules for fast intersection tests, even when the visual mesh is complex — the physics collider and the render mesh are kept separate, exactly as in game engines.
3. Leader-following
To make a flock follow a specific route — migrating over a mountain range, escorting a player character — one or more boids are designated leaders. Leaders move along an independently scripted path (a Bézier or Catmull-Rom spline, or direct player/AI control) and are excluded from the standard flocking forces.
where offset places followers slightly behind and beside the leader (avoiding a "conga line" of boids stacking directly behind it), and w_leader is typically weighted higher than ordinary cohesion.
Followers still apply separation and alignment among themselves, so the flock maintains its organic, breathing shape while tracking the leader's overall trajectory — this is the technique behind most "follow the guide bird" or escort-mission behaviours in games.
4. Combining rules and priorities
With five or six competing forces (separation, alignment, cohesion, obstacle avoidance, leader-following, boundary containment), naive summation can produce a net force that still drives a boid straight into a wall if cohesion is strong enough. Two robust combination strategies:
- Weighted sum with priority weights — obstacle avoidance gets a weight an order of magnitude larger than social rules, so it always dominates when active.
- Priority override — evaluate rules in order; if obstacle avoidance produces a non-zero force above a threshold, use it exclusively and skip the remaining rules for this tick.
Priority override is more predictable and is what most production flocking systems (including game-engine crowd simulation) use in practice, because it guarantees hard constraints (never hit a wall) cannot be diluted by soft preferences (stay near the flock).
5. Banking and quaternion orientation
A boid's visual orientation should track both where it is going (forward vector = normalized velocity) and how sharply it is turning (bank/roll proportional to turn rate). The standard construction:
right = normalize(cross(worldUp, forward))
up = cross(forward, right)
bankAngle = clamp(turnRate · BANK_FACTOR, -MAX_BANK, MAX_BANK)
orientation = lookRotation(forward, up) · rotateAroundForward(bankAngle)
Representing the result as a quaternion and using spherical
linear interpolation (slerp) between frames avoids
the discontinuities and gimbal lock that plague raw Euler-angle
interpolation, giving smooth banking even during sharp
manoeuvres.
6. Spatial partitioning in 3D
The same uniform-grid trick from 2D boids extends directly: cell size ≈ view radius, and each boid checks its 27 (3×3×3) neighbouring cells instead of 9. For very non-uniform flock densities (a tight formation punctuated by scattered stragglers), an octree adapts cell size locally and can outperform a uniform grid, at the cost of more complex insertion/removal bookkeeping each frame.
For 10,000+ boids in WebGL, positions are Morton-coded (Z-order curve interleaving x, y, z bits) and radix-sorted each frame, so spatially close boids end up contiguous in the GPU buffer — this keeps neighbour lookups coalesced and fast even in three dimensions.
7. Pseudocode
function stepBoids3D(boids, obstacles, leader, dt):
for each boid i:
// 1. Priority check: obstacle avoidance first
avoidForce = probeObstacles(boid[i], obstacles, LOOKAHEAD)
if length(avoidForce) > AVOID_THRESHOLD:
a = avoidForce
else:
// 2. Standard flocking (3D vectors)
neighbors = getNeighbors3D(i, VIEW_RADIUS)
v_sep = separation(i, neighbors)
v_align = alignment(i, neighbors)
v_coh = cohesion(i, neighbors)
// 3. Leader-following, if a leader is assigned
v_leader = leader ? followLeader(i, leader) : 0
a = W_SEP*v_sep + W_ALIGN*v_align + W_COH*v_coh + W_LEADER*v_leader
// 4. Integrate motion
vel[i] = clampLength(vel[i] + a * dt, MAX_SPEED)
pos[i] += vel[i] * dt
// 5. Update orientation with banking
orientation[i] = computeBankedOrientation(vel[i], prevVel[i])
Typical constants: VIEW_RADIUS = 12,
LOOKAHEAD = 6,
AVOID_THRESHOLD = 0.1,
W_LEADER = 1.5–2.0 (higher than cohesion so the
flock reliably tracks the leader through turns).
Frequently Asked Questions
What changes when boids move from 2D to 3D?
Position and velocity gain a z-component, neighbour search must scan a volume of cells (27 instead of 9 for a uniform grid), and orientation is no longer a single heading angle but a full 3D rotation, usually a quaternion, that must track both travel direction and banking roll.
How does 3D obstacle avoidance work for boids?
The most common approach casts one or more probe rays from the boid in its direction of travel and tests them against sphere or capsule colliders. If a ray intersects within a look-ahead distance, an avoidance force perpendicular to the obstacle surface is added with a weight high enough to override cohesion and alignment.
How does leader-following differ from normal flocking?
One or more designated boids move along an independently scripted path and are excluded from standard flocking rules. Ordinary flock members add an extra cohesion-like term pulling them toward the leader's recent position, so the whole flock follows the leader's route while still applying separation and alignment among themselves.
Why use quaternions instead of Euler angles for boid orientation?
What is the difference between weighted-sum and priority-override force combination?
Why cast multiple rays instead of just one for obstacle detection?
Why does the leader-following offset avoid a straight-line "conga line"?
How does an octree improve on a uniform grid for 3D flocking?
Can obstacle avoidance and leader-following conflict, and how is that resolved?
How many boids can a 3D flocking simulation handle in real time?
🐦 Try the 3D boids simulation
Watch the flock weave around obstacles and follow a leader in real time — thousands of agents, WebGL-accelerated.
Open simulation →