---
url: /examples/gpgpu/boids.md
description: >-
  Simulate flocking boids on the GPU with ping-pong framebuffers and interactive
  behavior controls.
---

::: example-editor {deps=tweakpane@^4.0.5}

```ts
import { glCanvas, glContext, loop, pingPongFBO } from "@radiancejs/gl";
import { Pane } from "tweakpane";
import "./styles.css";
import boidsPositions from "./positions.frag?raw";
import boidsVelocities from "./velocities.frag?raw";
import renderPassVertex from "./render.vert?raw";
import renderPassFragment from "./render.frag?raw";

const { gl, canvas } = glContext({ canvas: "#glCanvas" });

const count = 300;

const velocities = pingPongFBO({
  gl,
  fragment: boidsVelocities,
  uniforms: {
    uDeltaTime: 0,
    uPerceptionRadius: 0.1,
    uMaxSpeed: 0.4,
    uSeparationWeight: 1.5,
    uAlignmentWeight: 1.0,
    uCohesionWeight: 0.8,
    uBorderForce: 1,
    uBorderDistance: 0.8,
    uPredatorRepulsionStrength: 2.5,
    uPredatorRepulsionRadius: 1,
    tPositions: () => positions.texture,
  },
  dataTexture: {
    name: "tVelocities",
    initialData: Array.from({ length: count }).flatMap(() => [
      /* R */ Math.random() * 0.2 - 0.1,
      /* G */ Math.random() * 0.2 - 0.1,
      /* B */ 0,
      /* A */ 0,
    ]),
  },
});

const positions = pingPongFBO({
  gl,
  fragment: boidsPositions,
  uniforms: {
    uDeltaTime: 0,
    tVelocities: () => velocities.texture,
  },
  dataTexture: {
    name: "tPositions",
    initialData: Array.from({ length: count }).flatMap(() => [
      /* R */ Math.random() * 2 - 1,
      /* G */ Math.random() * 2 - 1,
      /* B */ 0,
      /* A */ 0,
    ]),
  },
});

const renderPass = glCanvas({
  canvas,
  vertex: renderPassVertex,
  fragment: renderPassFragment,
  uniforms: {
    tPositions: () => positions.texture,
    tVelocities: () => velocities.texture,
  },
  attributes: {
    aCoords: positions.coords,
  },
  blending: "normal",
});

loop(({ deltaTime }) => {
  velocities.uniforms.uDeltaTime = deltaTime / 500;
  velocities.render();

  positions.uniforms.uDeltaTime = deltaTime / 500;
  positions.render();

  renderPass.render();
});

const pane = new Pane({ title: "Uniforms", expanded: false });
pane.addBinding(velocities.uniforms, "uMaxSpeed", { min: 0.2, max: 0.8 });

const flocking = pane.addFolder({ title: "Flocking" });
flocking.addBinding(velocities.uniforms, "uAlignmentWeight", { min: 0.5, max: 1.5 });
flocking.addBinding(velocities.uniforms, "uSeparationWeight", { min: 1, max: 2 });
flocking.addBinding(velocities.uniforms, "uCohesionWeight", { min: 0.5, max: 1.5 });

const predator = pane.addFolder({ title: "Predator" });
predator.addBinding(velocities.uniforms, "uPredatorRepulsionStrength", { min: 1, max: 3 });
predator.addBinding(velocities.uniforms, "uPredatorRepulsionRadius", { min: 0.5, max: 1.5 });

```

```frag
uniform sampler2D tPositions;
uniform sampler2D tVelocities;
uniform float uDeltaTime;

varying vec2 uv;

void main() {
  vec2 position = texture(tPositions, uv).xy;
  vec2 velocity = texture(tVelocities, uv).xy;

  position += velocity * uDeltaTime;

  gl_FragColor = vec4(position, 0.0, 1.0);
}

```

```frag
uniform sampler2D tPositions;
uniform sampler2D tVelocities;
uniform float uDeltaTime;

uniform float uMaxSpeed;
uniform float uPerceptionRadius;

uniform float uSeparationWeight;
uniform float uAlignmentWeight;
uniform float uCohesionWeight;

uniform float uBorderForce;
uniform float uBorderDistance;

uniform float uPredatorRepulsionStrength;
uniform float uPredatorRepulsionRadius;

varying vec2 uv;

float random(vec2 st) {
  return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

vec2 flockingForces(vec2 position, vec2 velocity) {
  vec2 texSize = vec2(textureSize(tVelocities, 0));
  vec2 separation = vec2(0.0);
  vec2 alignment = vec2(0.0);
  vec2 cohesion = vec2(0.0);
  int count = 0;

  // Accumulate forces from neighbors
  for(float y = 1.0; y < texSize.y; y++) {
    for(float x = 1.0; x < texSize.x; x++) {
      vec2 neighborUV = vec2(x, y) / texSize;
      if(neighborUV == uv) continue;

      vec2 neighborPos = texture2D(tPositions, neighborUV).xy;
      vec2 neighborVel = texture2D(tVelocities, neighborUV).xy;
      float dist = distance(position, neighborPos);

      if(dist < uPerceptionRadius && dist > 0.0) {
        separation += normalize(position - neighborPos) * (1.0 - dist/uPerceptionRadius);
        alignment += neighborVel;
        cohesion += neighborPos;
        count++;
      }
    }
  }

  if(count > 0) {
    separation = normalize(separation) * uSeparationWeight;
    alignment = normalize(alignment/float(count)) * uAlignmentWeight;
    cohesion = normalize((cohesion/float(count)) - position) * uCohesionWeight;
  }
  return separation + alignment + cohesion;
}

vec2 borderRepulsion(vec2 position) {
  vec2 distToBorder = vec2(1.0 - abs(position.x), 1.0 - abs(position.y));
  vec2 force = vec2(0.0);

  if(distToBorder.x < uBorderDistance) {
    force.x = (uBorderDistance - distToBorder.x) / uBorderDistance * -sign(position.x);
  }
  if(distToBorder.y < uBorderDistance) {
    force.y = (uBorderDistance - distToBorder.y) / uBorderDistance * -sign(position.y);
  }
  return force * uBorderForce;
}

vec2 randomWandering(vec2 position, vec2 velocity) {
  float rand = random(position.xy) * 2.0 - 1.0;
  float angle = rand * radians(10.0);
  mat2 rotationMatrix = mat2(
    cos(angle), -sin(angle),
    sin(angle), cos(angle)
  );
  return normalize(rotationMatrix * velocity) * 5.;
}

vec2 predatorRepulsion(vec2 position, vec2 velocity) {
  vec2 predatorPos = texture2D(tPositions, vec2(0.0)).xy;
  vec2 toPredator = predatorPos - position;
  float predatorDist = length(toPredator);

  if (predatorDist < uPredatorRepulsionRadius) {
    return normalize(-1. * toPredator) * uPredatorRepulsionStrength * (1.0 - predatorDist / uPredatorRepulsionRadius);
  }
  return vec2(0.0);
}

void main() {
  vec2 position = texture2D(tPositions, uv).xy;
  vec2 velocity = texture2D(tVelocities, uv).xy;
  vec2 acceleration = borderRepulsion(position);

  bool isPredator = floor(gl_FragCoord.xy) == vec2(0.0);

  if (isPredator) {
    acceleration += randomWandering(position, velocity);
  } else {
    acceleration += flockingForces(position, velocity) + predatorRepulsion(position, velocity);
  }
  velocity += acceleration * uDeltaTime;

  float speedLimit = isPredator ? uMaxSpeed / 2.0 : uMaxSpeed;
  if(length(velocity) > speedLimit) {
    velocity = normalize(velocity) * speedLimit;
  }

  gl_FragColor = vec4(velocity, 0.0, 1.0);
}

```

```vert
uniform sampler2D tPositions;
uniform sampler2D tVelocities;
attribute vec2 aCoords;
varying vec4 vColor;
varying mat2 vRotation;

#define PI acos(-1.)

void main() {
  vec2 position = texture2D(tPositions, aCoords).xy;
  vec2 velocity = texture2D(tVelocities, aCoords).xy;

  vec2 orientation = normalize(velocity.xy);
  float angle = atan(orientation.y, orientation.x) - PI / 2.;
  vRotation = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));

  if(aCoords == vec2(0)) {
    // predator
    vColor = vec4(1, 0, 0, 1);
    gl_PointSize = 40.0;
  } else {
    // boid
    vec2 predatorPosition = texture2D(tPositions, vec2(0)).xy;
    float distance = length(position - predatorPosition);
    vColor = mix(vec4(1, .5, 0, 1), vec4(0, .8, 1, 1), smoothstep(0.2, .8, distance));
    vColor.rgb = mix(vColor.rgb, vec3(0), smoothstep(.6, 2., distance));
    gl_PointSize = 30.0;
  }

  gl_Position = vec4(position, 0, 1);
}

```

```frag
varying vec4 vColor;
varying mat2 vRotation;

// https://iquilezles.org/articles/distfunctions2d/
float sdUnevenCapsule( vec2 p, float r1, float r2, float h ) {
  p.x = abs(p.x);
  float b = (r1-r2)/h;
  float a = sqrt(1.0-b*b);
  float k = dot(p,vec2(-b,a));
  if( k < 0.0 ) return length(p) - r1;
  if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
  return dot(p, vec2(a,b) ) - r1;
}

void main() {
  vec2 uv = vRotation * (gl_PointCoord.xy - .5);

  float h = .4;
  float r1 = .12;
  float r2 = .28;
  float offset = (h + r1) / 2.;
  float capsuleDist = sdUnevenCapsule(uv + vec2(0., offset), r1, r2, h);

  vec4 color = vColor;
  color.a *= 1. - step(0., capsuleDist);
  color.rgb *= color.a;

  gl_FragColor = color;
}

```

```css
body {
  margin: 0;
}

canvas {
  width: 100svw;
  height: 100svh;
  display: block;
  background: black;
}

.tp-dfwv {
  width: 370px !important;
}

```

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>RadianceJS Example</title>
  </head>
  <body>
    <canvas id="glCanvas"></canvas>
    <script src="/index.ts"></script>
  </body>
</html>

```

:::
