Skip to content

Instantly share code, notes, and snippets.

@laurent-h
Created January 7, 2024 14:10
Show Gist options
  • Save laurent-h/bf7d9ba051f9ab8b4911c0346d957fb2 to your computer and use it in GitHub Desktop.
Save laurent-h/bf7d9ba051f9ab8b4911c0346d957fb2 to your computer and use it in GitHub Desktop.
Particles
<!doctype html>
<html lang="en">
<head>
<title>Particles</title>
<meta charset="utf-8">
<meta name="author" content="Laurent Houdard | cables.and.pixels">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<style>
body { margin: 0; overflow: hidden; }
canvas { max-width: 100vw; max-height: 100vh; }
</style>
</head>
<body>
<script type="module">
const PARTICLES = 500_000;
const PSIZE = 6;
let W;
let H;
const xrand = Math.random;
const xrandb =
(a, b = null) => (b === null ? a * xrand() : a + (b - a) * xrand());
const webglProgram = (gl, vert, frag, opts = {}) => {
const p = gl.createProgram();
for (let [t, src] of [
[gl.VERTEX_SHADER, vert],
[gl.FRAGMENT_SHADER, frag],
]) {
const s = gl.createShader(t);
gl.shaderSource(s, src);
gl.compileShader(s);
gl.attachShader(p, s);
}
if ('transformFeedbackVaryings' in opts) {
gl.transformFeedbackVaryings(
p, opts.transformFeedbackVaryings, gl.INTERLEAVED_ATTRIBS
);
}
gl.linkProgram(p);
return p;
};
const webglTexture = (gl, wrap = null, filter = null) => {
const texture = gl.createTexture();
wrap ??= gl.MIRRORED_REPEAT;
filter ??= gl.NEAREST;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
return texture;
};
const c = document.createElement('canvas');
const gl = c.getContext('webgl2', {
preserveDrawingBuffer: true,
});
document.body.append(c);
const vert = `#version 300 es
uniform sampler2D noise;
uniform float uRandom;
uniform float f;
layout(location=0) in float aAge;
layout(location=1) in float aLifespan;
layout(location=2) in vec2 aPosition;
layout(location=3) in vec2 aVelocity;
out float vAge;
out float vLifespan;
out vec2 vPosition;
out vec2 vVelocity;
out float vHealth;
#define PI 3.14159265359
float rand(int n, int k) {
int i = n + 131072 * k;
return mod(uRandom + texelFetch(noise, ivec2(i % 1024, i / 1024), 0).r, 1.);
}
void main() {
float r0 = rand(gl_VertexID, 0);
float r1 = rand(gl_VertexID, 1);
float r2 = rand(gl_VertexID, 2);
float r3 = rand(gl_VertexID, 3);
float r4 = rand(gl_VertexID, 4);
if (aAge >= aLifespan) {
vAge = 0.;
vLifespan = r0;
vPosition.x = 2. * r1 - 1.;
vPosition.y = -1.;
vVelocity = vec2(0, 1) * .0015 * (r2 + .5);
if (vPosition.x > 0.) {
vPosition *= vec2(1, -1);
vVelocity *= vec2(1, -1);
}
}
else {
vAge = aAge;
vAge = min(aLifespan, vAge + .001);
vLifespan = aLifespan;
if (aLifespan < 0.) {
vHealth = 0.;
}
else {
vVelocity = aVelocity;
vPosition = aPosition;
float k =
smoothstep(.0, 1., 1. - abs(vPosition.x)) *
smoothstep(.0, 1., 1. - abs(vPosition.y));
float a = r4 * 2. * PI;
float l = r3 * k * .00003;
vVelocity += l * vec2(sin(a), cos(a));
vPosition += vVelocity;
vHealth = 1.0 - (vAge / vLifespan);
}
}
gl_PointSize = 1. + vHealth;
gl_Position = vec4(vPosition, 0.0, 1.0);
}
`;
const frag = `#version 300 es
precision mediump float;
in float vHealth;
out vec4 fragColor;
void main() {
if (vHealth <= 0.) {
discard;
}
fragColor = vec4(1, 1, 1, vHealth);
}
`;
const p = webglProgram(gl, vert, frag, {
transformFeedbackVaryings: [
'vAge',
'vLifespan',
'vPosition',
'vVelocity'
],
});
const loc = [
'uRandom',
'noise',
].reduce((x, k) => {
x[k] = gl.getUniformLocation(p, k);
return x;
}, {});
const buffers = [];
const vaos = [];
for (let i of [0, 1]) {
const buffer = gl.createBuffer();
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, 4 * PSIZE * PARTICLES, gl.DYNAMIC_COPY);
if (i === 0) {
const initData = new Float32Array(PARTICLES * PSIZE);
for (let j = 0; j < PARTICLES * PSIZE; j += PSIZE) {
const lifespan = -1;
const age = lifespan - 2. * xrand();
initData.set([age, lifespan, 0, 0, 0, 0], j);
}
gl.bufferSubData(gl.ARRAY_BUFFER, 0, initData);
}
let offset = 0;
for (let [i, n] of [
[0, 1],
[1, 1],
[2, 2],
[3, 2],
]) {
gl.enableVertexAttribArray(i);
gl.vertexAttribPointer(i, n, gl.FLOAT, false, 4 * PSIZE, offset);
offset += 4 * n;
}
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
buffers.push(buffer);
vaos.push(vao);
}
let vao = vaos[0];
let buffer = buffers[1];
const noiseData = new Float32Array(
Array.from({ length: 1024 * 1024 }).map(() => xrand())
);
const noiseTexture = webglTexture(gl);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, 1024, 1024, 0,
gl.RED, gl.FLOAT, noiseData);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const draw = () => {
requestAnimationFrame(draw);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(p);
gl.uniform1f(loc.uRandom, xrand());
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, noiseTexture);
gl.uniform1i(loc.noise, 0);
gl.bindVertexArray(vao);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, PARTICLES);
gl.endTransformFeedback();
gl.bindVertexArray(null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
if (vao === vaos[0]) {
vao = vaos[1];
buffer = buffers[0];
}
else {
vao = vaos[0];
buffer = buffers[1];
}
};
const ww = innerWidth;
const wh = innerHeight;
W = Math.round(ww * devicePixelRatio);
H = Math.round(wh * devicePixelRatio);
c.width = W;
c.height = H;
c.style.width = ww + 'px';
c.style.height = wh + 'px';
gl.viewport(0, 0, W, H);
requestAnimationFrame(draw);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment