Skip to content

Commit babad94

Browse files
committed
docs: add an example of boids (ping pong FBO)
1 parent aa30c5c commit babad94

File tree

7 files changed

+306
-0
lines changed

7 files changed

+306
-0
lines changed

docs/examples/gpgpu/boids/index.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: ping pong FBO (Boids)
3+
---
4+
5+
::: example-editor {deps=tweakpane@^4.0.5}
6+
7+
<<< ./index.ts
8+
<<< ./positions.frag
9+
<<< ./velocities.frag
10+
<<< ./render.vert
11+
<<< ./render.frag
12+
<<< ./styles.css
13+
<<< @/snippets/default/index.html
14+
15+
:::

docs/examples/gpgpu/boids/index.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useWebGLContext, useLoop, usePingPongFBO, useWebGLCanvas } from "usegl";
2+
import { Pane } from "tweakpane";
3+
import "./styles.css";
4+
import boidsPositions from "./positions.frag?raw";
5+
import boidsVelocities from "./velocities.frag?raw";
6+
import renderPassVertex from "./render.vert?raw";
7+
import renderPassFragment from "./render.frag?raw";
8+
9+
const { gl, canvas } = useWebGLContext("#glCanvas");
10+
11+
const count = 300;
12+
13+
const velocities = usePingPongFBO(gl, {
14+
fragment: boidsVelocities,
15+
uniforms: {
16+
uDeltaTime: 0,
17+
uPerceptionRadius: 0.1,
18+
uMaxSpeed: 0.4,
19+
uSeparationWeight: 1.5,
20+
uAlignmentWeight: 1.0,
21+
uCohesionWeight: 0.8,
22+
uBorderForce: 1,
23+
uBorderDistance: 0.8,
24+
uPredatorRepulsionStrength: 2.5,
25+
uPredatorRepulsionRadius: 1,
26+
tPositions: () => positions.texture,
27+
},
28+
dataTexture: {
29+
name: "tVelocities",
30+
initialData: Array.from({ length: count }).flatMap(() => [
31+
Math.random() * 0.2 - 0.1,
32+
Math.random() * 0.2 - 0.1,
33+
0,
34+
0,
35+
]),
36+
},
37+
});
38+
39+
const positions = usePingPongFBO(gl, {
40+
fragment: boidsPositions,
41+
uniforms: {
42+
uDeltaTime: 0,
43+
tVelocities: () => velocities.texture,
44+
},
45+
dataTexture: {
46+
name: "tPositions",
47+
initialData: Array.from({ length: count }).flatMap(() => [
48+
Math.random() * 2 - 1,
49+
Math.random() * 2 - 1,
50+
0,
51+
0,
52+
]),
53+
},
54+
});
55+
56+
const renderPass = useWebGLCanvas({
57+
canvas,
58+
vertex: renderPassVertex,
59+
fragment: renderPassFragment,
60+
uniforms: {
61+
tPositions: () => positions.texture,
62+
tVelocities: () => velocities.texture,
63+
},
64+
attributes: {
65+
aCoords: positions.coords,
66+
},
67+
transparent: true,
68+
});
69+
70+
renderPass.onCanvasReady(() => {
71+
useLoop(({ deltaTime }) => {
72+
velocities.uniforms.uDeltaTime = deltaTime / 500;
73+
velocities.render();
74+
75+
positions.uniforms.uDeltaTime = deltaTime / 500;
76+
positions.render();
77+
78+
renderPass.render();
79+
});
80+
});
81+
82+
const pane = new Pane({ title: "Uniforms", expanded: false });
83+
pane.addBinding(velocities.uniforms, "uMaxSpeed", { min: 0.2, max: 0.8 });
84+
85+
const flocking = pane.addFolder({ title: "Flocking" });
86+
flocking.addBinding(velocities.uniforms, "uAlignmentWeight", { min: 0.5, max: 1.5 });
87+
flocking.addBinding(velocities.uniforms, "uSeparationWeight", { min: 1, max: 2 });
88+
flocking.addBinding(velocities.uniforms, "uCohesionWeight", { min: 0.5, max: 1.5 });
89+
90+
const predator = pane.addFolder({ title: "Predator" });
91+
predator.addBinding(velocities.uniforms, "uPredatorRepulsionStrength", { min: 1, max: 3 });
92+
predator.addBinding(velocities.uniforms, "uPredatorRepulsionRadius", { min: 0.5, max: 1.5 });
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
uniform sampler2D tPositions;
2+
uniform sampler2D tVelocities;
3+
uniform float uDeltaTime;
4+
5+
varying vec2 uv;
6+
7+
void main() {
8+
vec2 position = texture(tPositions, uv).xy;
9+
vec2 velocity = texture(tVelocities, uv).xy;
10+
11+
position += velocity * uDeltaTime;
12+
13+
gl_FragColor = vec4(position, 0.0, 1.0);
14+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
varying vec4 vColor;
2+
varying mat2 vRotation;
3+
4+
// https://iquilezles.org/articles/distfunctions2d/
5+
float sdUnevenCapsule( vec2 p, float r1, float r2, float h ) {
6+
p.x = abs(p.x);
7+
float b = (r1-r2)/h;
8+
float a = sqrt(1.0-b*b);
9+
float k = dot(p,vec2(-b,a));
10+
if( k < 0.0 ) return length(p) - r1;
11+
if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
12+
return dot(p, vec2(a,b) ) - r1;
13+
}
14+
15+
void main() {
16+
vec2 uv = vRotation * (gl_PointCoord.xy - .5);
17+
18+
float h = .4;
19+
float r1 = .12;
20+
float r2 = .28;
21+
float offset = (h + r1) / 2.;
22+
float capsuleDist = sdUnevenCapsule(uv + vec2(0., offset), r1, r2, h);
23+
24+
vec4 color = vColor;
25+
color.a *= 1. - step(0., capsuleDist);
26+
27+
gl_FragColor = color;
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
uniform sampler2D tPositions;
2+
uniform sampler2D tVelocities;
3+
attribute vec2 aCoords;
4+
varying vec4 vColor;
5+
varying mat2 vRotation;
6+
7+
#define PI acos(-1.)
8+
9+
void main() {
10+
vec2 position = texture2D(tPositions, aCoords).xy;
11+
vec2 velocity = texture2D(tVelocities, aCoords).xy;
12+
13+
vec2 orientation = normalize(velocity.xy);
14+
float angle = atan(orientation.y, orientation.x) - PI / 2.;
15+
vRotation = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
16+
17+
if(aCoords == vec2(0)) {
18+
// predator
19+
vColor = vec4(1, 0, 0, 1);
20+
gl_PointSize = 40.0;
21+
} else {
22+
// boid
23+
vec2 predatorPosition = texture2D(tPositions, vec2(0)).xy;
24+
float distance = length(position - predatorPosition);
25+
vColor = mix(vec4(1, .5, 0, 1), vec4(0, .8, 1, 1), smoothstep(0.2, .8, distance));
26+
vColor = mix(vColor, vec4(0), smoothstep(.6, 2., distance));
27+
gl_PointSize = 30.0;
28+
}
29+
30+
gl_Position = vec4(position, 0, 1);
31+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
body {
2+
margin: 0;
3+
}
4+
5+
canvas {
6+
width: 100svw;
7+
height: 100svh;
8+
display: block;
9+
background: black;
10+
}
11+
12+
.tp-dfwv {
13+
width: 370px !important;
14+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
uniform sampler2D tPositions;
2+
uniform sampler2D tVelocities;
3+
uniform float uDeltaTime;
4+
5+
uniform float uMaxSpeed;
6+
uniform float uPerceptionRadius;
7+
8+
uniform float uSeparationWeight;
9+
uniform float uAlignmentWeight;
10+
uniform float uCohesionWeight;
11+
12+
uniform float uBorderForce;
13+
uniform float uBorderDistance;
14+
15+
uniform float uPredatorRepulsionStrength;
16+
uniform float uPredatorRepulsionRadius;
17+
18+
varying vec2 uv;
19+
20+
float random(vec2 st) {
21+
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
22+
}
23+
24+
vec2 flockingForces(vec2 position, vec2 velocity) {
25+
vec2 texSize = vec2(textureSize(tVelocities, 0));
26+
vec2 separation = vec2(0.0);
27+
vec2 alignment = vec2(0.0);
28+
vec2 cohesion = vec2(0.0);
29+
int count = 0;
30+
31+
// Accumulate forces from neighbors
32+
for(float y = 1.0; y < texSize.y; y++) {
33+
for(float x = 1.0; x < texSize.x; x++) {
34+
vec2 neighborUV = vec2(x, y) / texSize;
35+
if(neighborUV == uv) continue;
36+
37+
vec2 neighborPos = texture2D(tPositions, neighborUV).xy;
38+
vec2 neighborVel = texture2D(tVelocities, neighborUV).xy;
39+
float dist = distance(position, neighborPos);
40+
41+
if(dist < uPerceptionRadius && dist > 0.0) {
42+
separation += normalize(position - neighborPos) * (1.0 - dist/uPerceptionRadius);
43+
alignment += neighborVel;
44+
cohesion += neighborPos;
45+
count++;
46+
}
47+
}
48+
}
49+
50+
if(count > 0) {
51+
separation = normalize(separation) * uSeparationWeight;
52+
alignment = normalize(alignment/float(count)) * uAlignmentWeight;
53+
cohesion = normalize((cohesion/float(count)) - position) * uCohesionWeight;
54+
}
55+
return separation + alignment + cohesion;
56+
}
57+
58+
vec2 borderRepulsion(vec2 position) {
59+
vec2 distToBorder = vec2(1.0 - abs(position.x), 1.0 - abs(position.y));
60+
vec2 force = vec2(0.0);
61+
62+
if(distToBorder.x < uBorderDistance) {
63+
force.x = (uBorderDistance - distToBorder.x) / uBorderDistance * -sign(position.x);
64+
}
65+
if(distToBorder.y < uBorderDistance) {
66+
force.y = (uBorderDistance - distToBorder.y) / uBorderDistance * -sign(position.y);
67+
}
68+
return force * uBorderForce;
69+
}
70+
71+
vec2 randomWandering(vec2 position, vec2 velocity) {
72+
float rand = random(position.xy) * 2.0 - 1.0;
73+
float angle = rand * radians(10.0);
74+
mat2 rotationMatrix = mat2(
75+
cos(angle), -sin(angle),
76+
sin(angle), cos(angle)
77+
);
78+
return normalize(rotationMatrix * velocity) * 5.;
79+
}
80+
81+
vec2 predatorRepulsion(vec2 position, vec2 velocity) {
82+
vec2 predatorPos = texture2D(tPositions, vec2(0.0)).xy;
83+
vec2 toPredator = predatorPos - position;
84+
float predatorDist = length(toPredator);
85+
86+
if (predatorDist < uPredatorRepulsionRadius) {
87+
return normalize(-1. * toPredator) * uPredatorRepulsionStrength * (1.0 - predatorDist / uPredatorRepulsionRadius);
88+
}
89+
return vec2(0.0);
90+
}
91+
92+
void main() {
93+
vec2 position = texture2D(tPositions, uv).xy;
94+
vec2 velocity = texture2D(tVelocities, uv).xy;
95+
vec2 acceleration = borderRepulsion(position);
96+
97+
bool isPredator = floor(gl_FragCoord.xy) == vec2(0.0);
98+
99+
if (isPredator) {
100+
acceleration += randomWandering(position, velocity);
101+
} else {
102+
acceleration += flockingForces(position, velocity) + predatorRepulsion(position, velocity);
103+
}
104+
velocity += acceleration * uDeltaTime;
105+
106+
float speedLimit = isPredator ? uMaxSpeed / 2.0 : uMaxSpeed;
107+
if(length(velocity) > speedLimit) {
108+
velocity = normalize(velocity) * speedLimit;
109+
}
110+
111+
gl_FragColor = vec4(velocity, 0.0, 1.0);
112+
}

0 commit comments

Comments
 (0)