Boids
Explanation
This demo was a little different from the others in that it does not use any custom shaders, the whole thing is made with ThreeJS primitives. In particular, a buffer geometry and a points material. Boids would be a good candidate problem for GPU-acceleration via compute shaders, and in fact the first version of this demo I wrote used a ThreeJS plugin to provide the traditional shoehorned OpenGL compute shader stuff, but I decided to go without it to get rid of the additional dependency. WebGPU offers built in support for compute shaders, but browser support for WebGPU is still lacking, so I decided to stick with WebGL/ThreeJS. Hence all the velocity calculations and position updates are being done on the CPU.
Source
import * as THREE from "three";
const WIDTH = 16;
const NUM_BOIDS = WIDTH * WIDTH;
const MAX_SPEED = 0.005;
const EDGE_MIN = 0.01;
const EDGE_MAX = 0.99;
const EDGE_REPULSION = 0.03;
const COHESION = 0.00015;
const ALIGNMENT = 0.0125;
const SEPARATION = 0.05;
const MOUSE_PULL = 0.0001;
const VELOCITY_DAMPING = 0.995;
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
let width = window.innerWidth;
let height = window.innerHeight;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(width, height);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1410);
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const geometry = new THREE.BufferGeometry();
// this seems redundant but the ThreeJS points class requires 3D positions in our buffer geometry
// so I have opted to maintain two separate data structures, one for the points in two dimensions
// and one for the points in three dimensions even though the boids are technically only moving in
// two dimensions
const positions = new Float32Array(NUM_BOIDS * 3);
const positionBuffer = new Float32Array(NUM_BOIDS * 2);
const velocities = new Float32Array(NUM_BOIDS * 2);
// randomize positions and velocities
for (let i = 0; i < NUM_BOIDS; i++) {
const x = Math.random();
const y = Math.random();
positionBuffer[i * 2] = x;
positionBuffer[i * 2 + 1] = y;
positions[i * 3] = x * 2 - 1;
positions[i * 3 + 1] = y * 2 - 1;
positions[i * 3 + 2] = 0;
velocities[i * 2] = (Math.random() - 0.5) * 2.0 * MAX_SPEED;
velocities[i * 2 + 1] = (Math.random() - 0.5) * 2.0 * MAX_SPEED;
}
geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage),
);
const material = new THREE.PointsMaterial({ color: 0xffffff, size: 2.0 });
const boids = new THREE.Points(geometry, material);
scene.add(boids);
const mouse = { x: 0.5, y: 0.5, active: false };
let lastMouseMove = 0;
const handleResize = () => {
width = window.innerWidth;
height = window.innerHeight;
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(width, height);
camera.updateProjectionMatrix();
};
const handleMouseMove = (event) => {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = clamp01((event.clientX - rect.left) / rect.width);
mouse.y = clamp01(1 - (event.clientY - rect.top) / rect.height);
mouse.active = true;
lastMouseMove = performance.now();
};
const handleMouseLeave = () => {
mouse.active = false;
mouse.x = 0.5;
mouse.y = 0.5;
lastMouseMove = 0;
};
window.addEventListener("resize", handleResize);
window.addEventListener("mousemove", handleMouseMove);
renderer.domElement.addEventListener("mouseleave", handleMouseLeave);
document.body.appendChild(renderer.domElement);
const animate = () => {
requestAnimationFrame(animate);
// iterate over boids...
for (let i = 0; i < NUM_BOIDS; i++) {
const idxPos = i * 2;
const idxVel = i * 2;
const boidPosX = positionBuffer[idxPos];
const boidPosY = positionBuffer[idxPos + 1];
let boidVelX = velocities[idxVel];
let boidVelY = velocities[idxVel + 1];
let sumPosX = 0;
let sumPosY = 0;
let sumVelX = 0;
let sumVelY = 0;
let sepX = 0;
let sepY = 0;
// compute the average position and velocity of the flock as well as the deviation for
// the current boid
for (let j = 0; j < NUM_BOIDS; j++) {
if (i === j) continue;
const nPosX = positionBuffer[j * 2];
const nPosY = positionBuffer[j * 2 + 1];
const nVelX = velocities[j * 2];
const nVelY = velocities[j * 2 + 1];
sumPosX += nPosX;
sumPosY += nPosY;
sumVelX += nVelX;
sumVelY += nVelY;
const diffX = nPosX - boidPosX;
const diffY = nPosY - boidPosY;
const distanceSq = diffX * diffX + diffY * diffY;
if (distanceSq < 0.00005 && distanceSq > 0) {
sepX -= diffX;
sepY -= diffY;
}
}
const avgPosX = sumPosX / (NUM_BOIDS - 1);
const avgPosY = sumPosY / (NUM_BOIDS - 1);
const avgVelX = sumVelX / (NUM_BOIDS - 1);
const avgVelY = sumVelY / (NUM_BOIDS - 1);
// apply the boid rules
const v1x = (avgPosX - boidPosX) * COHESION;
const v1y = (avgPosY - boidPosY) * COHESION;
const v2x = (avgVelX - boidVelX) * ALIGNMENT;
const v2y = (avgVelY - boidVelY) * ALIGNMENT;
const v3x = sepX * SEPARATION;
const v3y = sepY * SEPARATION;
// add in an adjustment for edge repulsion
let v4x = 0;
let v4y = 0;
if (boidPosX < EDGE_MIN) v4x += (EDGE_MIN - boidPosX) * EDGE_REPULSION;
if (boidPosX > EDGE_MAX) v4x += (EDGE_MAX - boidPosX) * EDGE_REPULSION;
if (boidPosY < EDGE_MIN) v4y += (EDGE_MIN - boidPosY) * EDGE_REPULSION;
if (boidPosY > EDGE_MAX) v4y += (EDGE_MAX - boidPosY) * EDGE_REPULSION;
// add in an adjustment to follow the mouse
let v5x = 0;
let v5y = 0;
if (mouse.active) {
// ignore the mouse if it is not present or standing still for >3 secs
if (lastMouseMove && performance.now() - lastMouseMove > 3000) {
mouse.active = false;
mouse.x = 0.5;
mouse.y = 0.5;
lastMouseMove = 0;
} else {
const mouseDirX = mouse.x - boidPosX;
const mouseDirY = mouse.y - boidPosY;
const mouseMag =
Math.sqrt(mouseDirX * mouseDirX + mouseDirY * mouseDirY) || 1;
v5x = (mouseDirX / mouseMag) * MOUSE_PULL;
v5y = (mouseDirY / mouseMag) * MOUSE_PULL;
}
}
boidVelX = boidVelX * VELOCITY_DAMPING + v1x + v2x + v3x + v4x + v5x;
boidVelY = boidVelY * VELOCITY_DAMPING + v1y + v2y + v3y + v4y + v5y;
// clamp max speed to keep the simulation well behaved
const speed = Math.sqrt(boidVelX * boidVelX + boidVelY * boidVelY);
if (speed > MAX_SPEED) {
boidVelX = (boidVelX / speed) * MAX_SPEED;
boidVelY = (boidVelY / speed) * MAX_SPEED;
}
let newPosX = boidPosX + boidVelX;
let newPosY = boidPosY + boidVelY;
if (newPosX < 0) newPosX = 0;
if (newPosX > 1) newPosX = 1;
if (newPosY < 0) newPosY = 0;
if (newPosY > 1) newPosY = 1;
positionBuffer[idxPos] = newPosX;
positionBuffer[idxPos + 1] = newPosY;
velocities[idxVel] = boidVelX;
velocities[idxVel + 1] = boidVelY;
positions[i * 3] = newPosX * 2 - 1;
positions[i * 3 + 1] = newPosY * 2 - 1;
}
geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
};
animate();
- ← Previous
Matrix (-like) Effect - Next →
Water Effect