Created
August 31, 2024 01:50
-
-
Save dgerrells/bbd74265cb45ca5c601d7d0d1590d594 to your computer and use it in GitHub Desktop.
simulate 20m 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" style="user-select: none"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Sabby</title> | |
| <link rel="icon" href="./favicon.ico" type="image/x-icon" /> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/UAParser.js/1.0.37/ua-parser.min.js"></script> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.162.0/build/three.module.js" | |
| } | |
| } | |
| </script> | |
| <style> | |
| body, | |
| html, | |
| canvas { | |
| touch-action: none; | |
| -webkit-touch-callout: none; | |
| -webkit-user-select: none; | |
| -khtml-user-select: none; | |
| -moz-user-select: none; | |
| -ms-user-select: none; | |
| user-select: none; | |
| outline: none; | |
| } | |
| </style> | |
| </head> | |
| <body | |
| style=" | |
| margin: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| background: black; | |
| pointer-events: none; | |
| overflow: hidden; | |
| user-select: none; | |
| " | |
| > | |
| <script type="module"> | |
| import * as THREE from "three"; | |
| const uap = new UAParser(); | |
| const isMobile = /Mobi/.test(navigator.userAgent); | |
| const getQueryParam = (paramName) => | |
| new URL(window.location.href).searchParams.get(paramName); | |
| let cores = ~~getQueryParam("cores"); | |
| if (cores === 0) { | |
| cores = | |
| uap.getResult().device.vendor === "Apple" | |
| ? 4 | |
| : navigator.hardwareConcurrency - 1; | |
| } | |
| const PARTICLE_COUNT = ~~getQueryParam("count") || 1_000_000; | |
| const WORKER_COUNT = cores; | |
| const WORKER_CHUNK_SIZE = Math.floor(PARTICLE_COUNT / WORKER_COUNT); | |
| const simData = { | |
| workerPool: [], | |
| activeWorkers: 0, | |
| width: window.innerWidth, | |
| height: window.innerHeight, | |
| particleGridA: null, | |
| particleGridB: null, | |
| activeParticleGrid: null, | |
| scene: new THREE.Scene(), | |
| camera: new THREE.OrthographicCamera( | |
| window.innerWidth / -2, | |
| window.innerWidth / 2, | |
| window.innerHeight / 2, | |
| window.innerHeight / -2, | |
| 0.1, | |
| 100 | |
| ), | |
| renderer: new THREE.WebGLRenderer(), | |
| texture: null, | |
| quad: null, | |
| }; | |
| const particleStride = 6; // 6 floats x,y,dx,dy,sx,sy; | |
| const particleByteStride = particleStride * 4; // 4 bytes per float | |
| const sabViewParticles = new Float32Array( | |
| new SharedArrayBuffer(PARTICLE_COUNT * particleByteStride) | |
| ); | |
| // dt + screen width + screen height + touch count + mouse x + mouse y | |
| const sabViewSimData = new Float32Array(new SharedArrayBuffer(4 * 64)); | |
| simData.camera.position.set(simData.width / 2, simData.height / 2, 1); | |
| simData.renderer.setSize(simData.width, simData.height); | |
| document.body.appendChild(simData.renderer.domElement); | |
| simData.texture = new THREE.DataTexture( | |
| new Uint8Array(simData.width * simData.height), | |
| simData.width, | |
| simData.height, | |
| THREE.RedFormat, | |
| THREE.UnsignedByteType, | |
| THREE.UVMapping, | |
| THREE.ClampToEdge, | |
| THREE.ClampToEdge, | |
| THREE.NearestFilter, | |
| THREE.NearestFilter, | |
| 0 | |
| ); | |
| const baseC = isMobile ? 5 : 5; | |
| const scaleC = isMobile ? 20 : 30; | |
| const customMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| screen: { value: new THREE.Vector2(simData.width, simData.height) }, | |
| uTexture: { value: simData.texture }, | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform sampler2D uTexture; | |
| uniform vec2 screen; | |
| varying vec2 vUv; | |
| void main() { | |
| float count = texture2D(uTexture, vUv).x; | |
| vec4 col = vec4(0,0,0,1); | |
| col.x = count * (${baseC}. + ${scaleC}.*gl_FragCoord.x / screen.x); | |
| col.y = count * (${baseC}. + ${scaleC}.*gl_FragCoord.y / screen.y); | |
| col.z = count * (${baseC}. + ${scaleC}.*(1.-gl_FragCoord.y / screen.y)); | |
| gl_FragColor = col; | |
| } | |
| `, | |
| blending: THREE.AdditiveBlending, | |
| depthTest: false, | |
| }); | |
| //setup workers | |
| simData.activeWorkers = WORKER_COUNT; | |
| for (let i = 0; i < WORKER_COUNT; i++) { | |
| const worker = new Worker("./sabby/worker.js"); | |
| worker.addEventListener("message", onWorkerMessage); | |
| simData.workerPool.push(worker); | |
| } | |
| function resize() { | |
| const { camera, scene, renderer, workerPool } = simData; | |
| simData.width = window.innerWidth; | |
| simData.height = window.innerHeight; | |
| simData.texture = new THREE.DataTexture( | |
| new Uint8Array(simData.width * simData.height), | |
| simData.width, | |
| simData.height, | |
| THREE.RedFormat, | |
| THREE.UnsignedByteType, | |
| THREE.UVMapping, | |
| THREE.ClampToEdge, | |
| THREE.ClampToEdge, | |
| THREE.NearestFilter, | |
| THREE.NearestFilter, | |
| 0 | |
| ); | |
| customMaterial.uniforms.screen.value.set(simData.width, simData.height); | |
| customMaterial.uniforms.uTexture.value = simData.texture; | |
| scene.remove(simData.quad); | |
| const geometry = new THREE.PlaneGeometry(simData.width, simData.height); | |
| simData.quad = new THREE.Mesh(geometry, customMaterial); | |
| simData.quad.position.set(simData.width / 2, simData.height / 2, 0); | |
| scene.add(simData.quad); | |
| sabViewSimData[1] = simData.width; | |
| sabViewSimData[2] = simData.height; | |
| renderer.setSize(simData.width, simData.height); | |
| camera.position.set(simData.width / 2, simData.height / 2, 10); | |
| camera.left = simData.width / -2; | |
| camera.right = simData.width / 2; | |
| camera.top = simData.height / 2; | |
| camera.bottom = simData.height / -2; | |
| camera.updateProjectionMatrix(); | |
| simData.particleGridA = new Uint8Array( | |
| new SharedArrayBuffer(simData.width * simData.height) | |
| ); | |
| simData.particleGridB = new Uint8Array( | |
| new SharedArrayBuffer(simData.width * simData.height) | |
| ); | |
| //init particles | |
| for (let i = 0; i < PARTICLE_COUNT; i++) { | |
| sabViewParticles[i * particleStride] = Math.random() * simData.width; | |
| sabViewParticles[i * particleStride + 1] = | |
| Math.random() * simData.height; | |
| sabViewParticles[i * particleStride + 2] = | |
| (Math.random() * 2 - 1) * 30; | |
| sabViewParticles[i * particleStride + 3] = | |
| (Math.random() * 2 - 1) * 30; | |
| sabViewParticles[i * particleStride + 4] = | |
| sabViewParticles[i * particleStride]; | |
| sabViewParticles[i * particleStride + 5] = | |
| sabViewParticles[i * particleStride + 1]; | |
| } | |
| // update workers | |
| workerPool.forEach((worker, i) => { | |
| worker.postMessage({ | |
| id: i, | |
| sabViewParticles, | |
| sabViewSimData, | |
| particleOffsetStart: WORKER_CHUNK_SIZE * i, | |
| particleOffsetEnd: WORKER_CHUNK_SIZE * i + WORKER_CHUNK_SIZE, | |
| particleStride, | |
| particleGridA: simData.particleGridA, | |
| particleGridB: simData.particleGridB, | |
| }); | |
| }); | |
| } | |
| resize(); | |
| window.addEventListener("resize", resize); | |
| window.addEventListener("mousemove", (e) => { | |
| sabViewSimData[4] = e.clientX; | |
| sabViewSimData[5] = simData.height - e.clientY; | |
| }); | |
| window.addEventListener("mousedown", (e) => { | |
| sabViewSimData[3] = 1; | |
| sabViewSimData[4] = e.clientX; | |
| sabViewSimData[5] = simData.height - e.clientY; | |
| }); | |
| window.addEventListener("mouseup", (e) => { | |
| sabViewSimData[3] = 0; | |
| }); | |
| window.addEventListener("touchmove", (e) => { | |
| e.preventDefault(); | |
| sabViewSimData[3] = e.targetTouches.length; | |
| for (let i = 0; i < e.targetTouches.length; i++) { | |
| const touch = e.targetTouches[i]; | |
| sabViewSimData[i * 2 + 4] = touch.clientX; | |
| sabViewSimData[i * 2 + 4 + 1] = simData.height - touch.clientY; | |
| } | |
| }); | |
| window.addEventListener("touchstart", (e) => { | |
| e.preventDefault(); | |
| sabViewSimData[3] = e.targetTouches.length; | |
| for (let i = 0; i < e.targetTouches.length; i++) { | |
| const touch = e.targetTouches[i]; | |
| sabViewSimData[i * 2 + 4] = touch.clientX; | |
| sabViewSimData[i * 2 + 4 + 1] = simData.height - touch.clientY; | |
| } | |
| }); | |
| window.addEventListener("touchend", (e) => { | |
| e.preventDefault(); | |
| sabViewSimData[3] = 0; | |
| }); | |
| window.addEventListener("touchcancel", (e) => { | |
| e.preventDefault(); | |
| sabViewSimData[3] = 0; | |
| }); | |
| function onWorkerMessage() { | |
| simData.activeWorkers--; | |
| if (simData.activeWorkers !== 0) { | |
| return; | |
| } | |
| requestAnimationFrame(runSimulation); | |
| } | |
| let lastTime = 1; | |
| function runSimulation(currentTime) { | |
| const dt = Math.min(0.1, (currentTime - lastTime) / 1000); | |
| lastTime = currentTime; | |
| sabViewSimData[0] = dt; | |
| simData.activeWorkers = WORKER_COUNT; | |
| simData.workerPool.forEach((worker, i) => { | |
| worker.postMessage({}); | |
| }); | |
| simData.activeParticleGrid = | |
| simData.activeParticleGrid === simData.particleGridA | |
| ? simData.particleGridB | |
| : simData.particleGridA; | |
| render(simData.activeParticleGrid); | |
| } | |
| function render(grid) { | |
| simData.texture.image.data.set(grid); | |
| simData.texture.needsUpdate = true; | |
| simData.renderer.render(simData.scene, simData.camera); | |
| grid.fill(0); | |
| } | |
| </script> | |
| </body> | |
| </html> |
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
| console.log("worker created"); | |
| const isMobile = /Mobi/.test(navigator.userAgent); | |
| let lastSetupEvent = null; | |
| let activeGrid = null; | |
| // I don't make trash | |
| let cacher = { | |
| x: 0, | |
| y: 0, | |
| }; | |
| const fixedForce = isMobile ? 258300 * 8 : 2583000 * 15; | |
| const simulate = (event) => { | |
| const { | |
| sabViewParticles, | |
| sabViewSimData, | |
| id, | |
| particleOffsetStart, | |
| particleOffsetEnd, | |
| particleStride, | |
| particleGridA, | |
| particleGridB, | |
| } = event.data; | |
| activeGrid = activeGrid === particleGridA ? particleGridB : particleGridA; | |
| const [delta, width, height, touchCount] = [ | |
| sabViewSimData[0], | |
| sabViewSimData[1], | |
| sabViewSimData[2], | |
| sabViewSimData[3], | |
| sabViewSimData[4], | |
| sabViewSimData[5], | |
| ]; | |
| const start = particleOffsetStart; | |
| const end = particleOffsetEnd; | |
| const decay = 1 / (1 + delta * 1); | |
| for (let i = start; i < end; i++) { | |
| const pi = i * particleStride; | |
| let x = sabViewParticles[pi]; | |
| let y = sabViewParticles[pi + 1]; | |
| let dx = sabViewParticles[pi + 2] * decay; | |
| let dy = sabViewParticles[pi + 3] * decay; | |
| let sx = sabViewParticles[pi + 4]; | |
| let sy = sabViewParticles[pi + 5]; | |
| if (touchCount > 0) { | |
| for (let t = 0; t < touchCount; t++) { | |
| const tx = sabViewSimData[4 + t * 2]; | |
| const ty = sabViewSimData[4 + t * 2 + 1]; | |
| forceInvSqr(tx, ty, x, y, fixedForce); | |
| dx += cacher.x * delta * 3; | |
| dy += cacher.y * delta * 3; | |
| } | |
| } | |
| forceSqr(sx, sy, x, y, 0.5); | |
| dx += cacher.x * delta * 1; | |
| dy += cacher.y * delta * 1; | |
| x += dx * delta; | |
| y += dy * delta; | |
| sabViewParticles[pi] = x; | |
| sabViewParticles[pi + 1] = y; | |
| sabViewParticles[pi + 2] = dx; | |
| sabViewParticles[pi + 3] = dy; | |
| if (x < 0 || x >= width) continue; | |
| if (y < 0 || y >= height) continue; | |
| const pCountIndex = (y | 0) * width + (x | 0); | |
| activeGrid[pCountIndex]++; | |
| } | |
| postMessage({}); | |
| }; | |
| function clamp(n) { | |
| n &= -(n >= 0); | |
| return n | ((255 - n) >> 31); | |
| } | |
| function forceInvSqr(x1, y1, x2, y2, m = 25830000) { | |
| const dx = x1 - x2; | |
| const dy = y1 - y2; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| const dirX = dx / dist; | |
| const dirY = dy / dist; | |
| const force = Math.min(1200, m / (dist * dist)); | |
| cacher.x = force * dirX; | |
| cacher.y = force * dirY; | |
| } | |
| function forceInvCube(x1, y1, x2, y2, m = 25830000) { | |
| const dx = x1 - x2; | |
| const dy = y1 - y2; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| const dirX = dx / dist; | |
| const dirY = dy / dist; | |
| const force = Math.min(12000, m / (dist * dist * dist)); | |
| cacher.x = force * dirX; | |
| cacher.y = force * dirY; | |
| } | |
| function forceSqr(x1, y1, x2, y2, d = 999999) { | |
| const dx = x1 - x2; | |
| const dy = y1 - y2; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (d <= dist) { | |
| const dirX = dx / dist; | |
| const dirY = dy / dist; | |
| const force = Math.min(12000, dist * dist); | |
| cacher.x = force * dirX; | |
| cacher.y = force * dirY; | |
| return; | |
| } | |
| cacher.x = 0; | |
| cacher.y = 0; | |
| } | |
| onmessage = (event) => { | |
| if (!event.data.sabViewParticles) { | |
| simulate(lastSetupEvent); | |
| return; | |
| } | |
| lastSetupEvent = event; | |
| postMessage({}); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment