Created
January 7, 2024 14:10
-
-
Save laurent-h/bf7d9ba051f9ab8b4911c0346d957fb2 to your computer and use it in GitHub Desktop.
Particles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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