From 88f47949133fc81305b30881ab38347a9204a083 Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Mon, 13 May 2024 18:21:22 -0400 Subject: [PATCH 1/7] double density relaxation --- run_0.js | 6 ++ sim_0.js | 28 ++++-- sim_1.html | 38 +++++++++ sim_1.js | 243 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 sim_1.html create mode 100644 sim_1.js diff --git a/run_0.js b/run_0.js index 71197e5..fdadd54 100644 --- a/run_0.js +++ b/run_0.js @@ -44,3 +44,9 @@ document.getElementById("numParticles").addEventListener("input", (e) => { numParticles = e.target.value; simulator = new Simulator(canvas.width, canvas.height, numParticles); }); + +window.addEventListener("resize", () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + simulator.resize(canvas.width, canvas.height); +}); diff --git a/sim_0.js b/sim_0.js index f1df0f5..f51768f 100644 --- a/sim_0.js +++ b/sim_0.js @@ -45,9 +45,14 @@ class Simulator { } draw(ctx) { + ctx.save(); + ctx.translate(-2.5, -2.5); + for (let p of this.particles) { ctx.fillRect(p.posX, p.posY, 5, 5); } + + ctx.restore(); } // Algorithm 1: Simulation step @@ -91,19 +96,24 @@ class Simulator { adjustSprings(dt) { } applyViscosity(dt) { } resolveCollisions(dt) { - const boundaryMul = 2; // 1 is no bounce, 2 is full bounce + const boundaryMul = 1.9; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + for (let p of this.particles) { - if (p.posX < 0) { - p.posX += Math.abs(p.velX) * boundaryMul; - } else if (p.posX > this.width) { - p.posX -= Math.abs(p.velX) * boundaryMul; + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); } - if (p.posY < 0) { - p.posY += Math.abs(p.velY) * boundaryMul; - } else if (p.posY > this.height) { - p.posY -= Math.abs(p.velY) * boundaryMul; + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); } } } diff --git a/sim_1.html b/sim_1.html new file mode 100644 index 0000000..91b6233 --- /dev/null +++ b/sim_1.html @@ -0,0 +1,38 @@ + + + + + PVFS 2 - Double Density Relaxation + + + + + + + + + +
+

FPS: 0

+

+ + +

+

+ + + +

+
+ + + + + + + diff --git a/sim_1.js b/sim_1.js new file mode 100644 index 0000000..99bc644 --- /dev/null +++ b/sim_1.js @@ -0,0 +1,243 @@ +class Particle { + constructor(posX, posY, velX, velY) { + this.posX = posX; + this.posY = posY; + + this.prevX = posX; + this.prevY = posY; + + this.velX = velX; + this.velY = velY; + } +} + +class Simulator { + constructor(width, height, numParticles) { + this.running = false; + + this.width = width; + this.height = height; + + this.gravX = 0.0; + this.gravY = 0.5; + + this.particles = []; + this.addParticles(numParticles); + + this.screenX = window.screenX; + this.screenY = window.screenY; + } + + start() { this.running = true; } + pause() { this.running = false; } + + resize(width, height) { + this.width = width; + this.height = height; + + for (let p of this.particles) { + p.posX += deltaX; + p.posY += deltaY; + } + } + + addParticles(count) { + for (let i = 0; i < count; i++) { + const posX = Math.random() * this.width; + const posY = Math.random() * this.height; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + draw(ctx) { + ctx.save(); + ctx.translate(-5, -5); + + for (let p of this.particles) { + ctx.fillRect(p.posX, p.posY, 10, 10); + } + + ctx.restore(); + } + + // Algorithm 1: Simulation step + update(dt = 1) { + if (!this.running) { + return; + } + + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + + this.screenX = window.screenX; + this.screenY = window.screenY; + + for (let p of this.particles) { + // apply gravity + p.velX += this.gravX * dt; + p.velY += this.gravY * dt; + + p.posX -= screenMoveX; + p.posY -= screenMoveY; + } + + this.applyViscosity(dt); + + for (let p of this.particles) { + // save previous position + p.prevX = p.posX; + p.prevY = p.posY; + + // advance to predicted position + p.posX += p.velX * dt; + p.posY += p.velY * dt; + } + + this.adjustSprings(dt); + this.applySpringDisplacements(dt); + this.doubleDensityRelaxation(dt); + this.resolveCollisions(dt); + + for (let p of this.particles) { + // use previous position to calculate new velocity + p.velX = (p.posX - p.prevX) / dt; + p.velY = (p.posY - p.prevY) / dt; + } + } + + doubleDensityRelaxation(dt) { + const numParticles = this.particles.length; + const kernelRadius = 40; // h + const kernelRadiusSq = kernelRadius * kernelRadius; + + const restDensity = 2; + const stiffness = 1.0; + const nearStiffness = .5; + + // Neighbor cache + const neighborIndices = []; + const neighborUnitX = []; + const neighborUnitY = []; + const neighborCloseness = []; + + for (let i = 0; i < numParticles; i++) { + let p0 = this.particles[i]; + + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + + // Compute density and near-density + for (let j = i + 1; j < numParticles; j++) { + let p1 = this.particles[j]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = j; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + } + + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); + + if (closestX < kernelRadius) { + const q = closestX / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + if (closestY < kernelRadius) { + const q = closestY / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; + + let dispX = 0; + let dispY = 0; + + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; + + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; + + p1.posX += DX; + p1.posY += DY; + + dispX -= DX; + dispY -= DY; + } + + p0.posX += dispX; + p0.posY += dispY; + } + } + + applySpringDisplacements(dt) { } + adjustSprings(dt) { } + applyViscosity(dt) { } + resolveCollisions(dt) { + const boundaryMul = 1.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + + for (let p of this.particles) { + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); + } + + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); + } + } + } +} From 952d7c31bb8deed21c1ea39bdb838bb182dc3b87 Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Mon, 13 May 2024 20:23:26 -0400 Subject: [PATCH 2/7] optimize neighbor search --- sim_1.js | 5 - sim_2.html | 38 ++++++ sim_2.js | 330 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 368 insertions(+), 5 deletions(-) create mode 100644 sim_2.html create mode 100644 sim_2.js diff --git a/sim_1.js b/sim_1.js index 99bc644..9ebf8bf 100644 --- a/sim_1.js +++ b/sim_1.js @@ -34,11 +34,6 @@ class Simulator { resize(width, height) { this.width = width; this.height = height; - - for (let p of this.particles) { - p.posX += deltaX; - p.posY += deltaY; - } } addParticles(count) { diff --git a/sim_2.html b/sim_2.html new file mode 100644 index 0000000..4828016 --- /dev/null +++ b/sim_2.html @@ -0,0 +1,38 @@ + + + + + PVFS 2.1 - Optimize Neighbor Search + + + + + + + + + +
+

FPS: 0

+

+ + +

+

+ + + +

+
+ + + + + + + diff --git a/sim_2.js b/sim_2.js new file mode 100644 index 0000000..7400f20 --- /dev/null +++ b/sim_2.js @@ -0,0 +1,330 @@ +class Particle { + constructor(posX, posY, velX, velY) { + this.posX = posX; + this.posY = posY; + + this.prevX = posX; + this.prevY = posY; + + this.velX = velX; + this.velY = velY; + } +} + +class Simulator { + constructor(width, height, numParticles) { + this.running = false; + + this.width = width; + this.height = height; + + this.gravX = 0.0; + this.gravY = 0.2; + + this.particles = []; + this.addParticles(numParticles); + + this.screenX = window.screenX; + this.screenY = window.screenY; + + this.numHashBuckets = 20000; + this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list + this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + } + + start() { this.running = true; } + pause() { this.running = false; } + + resize(width, height) { + this.width = width; + this.height = height; + } + + addParticles(count) { + for (let i = 0; i < count; i++) { + const posX = Math.random() * this.width; + const posY = Math.random() * this.height; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + draw(ctx) { + ctx.save(); + ctx.translate(-5, -5); + + for (let p of this.particles) { + ctx.fillRect(p.posX, p.posY, 10, 10); + } + + ctx.restore(); + } + + // Algorithm 1: Simulation step + update(dt = 1) { + if (!this.running) { + return; + } + + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + + this.screenX = window.screenX; + this.screenY = window.screenY; + + for (let p of this.particles) { + // apply gravity + p.velX += this.gravX * dt; + p.velY += this.gravY * dt; + + p.posX -= screenMoveX; + p.posY -= screenMoveY; + } + + this.applyViscosity(dt); + + for (let p of this.particles) { + // save previous position + p.prevX = p.posX; + p.prevY = p.posY; + + // advance to predicted position + p.posX += p.velX * dt; + p.posY += p.velY * dt; + } + + this.populateHashGrid(); + + this.adjustSprings(dt); + this.applySpringDisplacements(dt); + this.doubleDensityRelaxation(dt); + this.resolveCollisions(dt); + + for (let p of this.particles) { + // use previous position to calculate new velocity + p.velX = (p.posX - p.prevX) / dt; + p.velY = (p.posY - p.prevY) / dt; + } + } + + doubleDensityRelaxation(dt) { + const numParticles = this.particles.length; + const kernelRadius = 40; // h + const kernelRadiusSq = kernelRadius * kernelRadius; + const kernelRadiusInv = 1.0 / kernelRadius; + + const restDensity = 4; + const stiffness = 1.; + const nearStiffness = 1.; + + // Neighbor cache + const neighborIndices = []; + const neighborUnitX = []; + const neighborUnitY = []; + const neighborCloseness = []; + + for (let i = 0; i < numParticles; i++) { + let p0 = this.particles[i]; + + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + + // Compute density and near-density + const bucketX = Math.floor(p0.posX * kernelRadiusInv); + const bucketY = Math.floor(p0.posY * kernelRadiusInv); + + for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { + for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { + const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + + let neighborIdx = this.particleListHeads[bucketIdx]; + + while (neighborIdx != -1) { + if (neighborIdx <= i) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + let p1 = this.particles[neighborIdx]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r * kernelRadiusInv; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = neighborIdx; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + + neighborIdx = this.particleListNextIdx[neighborIdx]; + } + } + } + + // The old n^2 way + // for (let j = i + 1; j < numParticles; j++) { + // let p1 = this.particles[j]; + + // const diffX = p1.posX - p0.posX; + + // if (diffX > kernelRadius || diffX < -kernelRadius) { + // continue; + // } + + // const diffY = p1.posY - p0.posY; + + // if (diffY > kernelRadius || diffY < -kernelRadius) { + // continue; + // } + + // const rSq = diffX * diffX + diffY * diffY; + + // if (rSq < kernelRadiusSq) { + // const r = Math.sqrt(rSq); + // const q = r / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; + + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + + // neighborIndices[numNeighbors] = j; + // neighborUnitX[numNeighbors] = diffX / r; + // neighborUnitY[numNeighbors] = diffY / r; + // neighborCloseness[numNeighbors] = closeness; + // numNeighbors++; + // } + // } + + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); + + if (closestX < kernelRadius) { + const q = closestX / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + if (closestY < kernelRadius) { + const q = closestY / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; + + let dispX = 0; + let dispY = 0; + + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; + + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; + + p1.posX += DX; + p1.posY += DY; + + dispX -= DX; + dispY -= DY; + } + + p0.posX += dispX; + p0.posY += dispY; + } + } + + // Mueller 10 minute physics + getHashBucketIdx(bucketX, bucketY) { + const h = ((bucketX * 92837111) ^ (bucketY * 689287499)); + return Math.abs(h) % this.numHashBuckets; + } + + populateHashGrid() { + // Clear the hash grid + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads[i] = -1; + } + + // Populate the hash grid + const numParticles = this.particles.length; + const bucketSize = 40; // Same as kernel radius + const bucketSizeInv = 1.0 / bucketSize; + + for (let i = 0; i < numParticles; i++) { + let p = this.particles[i]; + + const bucketX = Math.floor(p.posX * bucketSizeInv); + const bucketY = Math.floor(p.posY * bucketSizeInv); + + const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); + + this.particleListNextIdx[i] = this.particleListHeads[bucketIdx]; + this.particleListHeads[bucketIdx] = i; + } + } + + applySpringDisplacements(dt) { } + adjustSprings(dt) { } + applyViscosity(dt) { } + resolveCollisions(dt) { + const boundaryMul = 1.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + + for (let p of this.particles) { + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); + } + + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); + } + } + } +} From e36bf7a0ab70f278ee1c83979814a12af3db4aac Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Mon, 13 May 2024 20:29:42 -0400 Subject: [PATCH 3/7] Use spatial hash --- run_0.js | 4 ++ sim_2.html | 2 + sim_2.js | 143 +++++++++++++++++++++++++++-------------------------- 3 files changed, 80 insertions(+), 69 deletions(-) diff --git a/run_0.js b/run_0.js index fdadd54..15fd7f9 100644 --- a/run_0.js +++ b/run_0.js @@ -45,6 +45,10 @@ document.getElementById("numParticles").addEventListener("input", (e) => { simulator = new Simulator(canvas.width, canvas.height, numParticles); }); +document.getElementById("useSpatialHash").addEventListener("change", (e) => { + simulator.useSpatialHash = e.target.checked; +}); + window.addEventListener("resize", () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; diff --git a/sim_2.html b/sim_2.html index 4828016..c7e51b9 100644 --- a/sim_2.html +++ b/sim_2.html @@ -22,6 +22,8 @@ + +

diff --git a/sim_2.js b/sim_2.js index 7400f20..87b1511 100644 --- a/sim_2.js +++ b/sim_2.js @@ -27,7 +27,8 @@ class Simulator { this.screenX = window.screenX; this.screenY = window.screenY; - this.numHashBuckets = 20000; + this.useSpatialHash = true; + this.numHashBuckets = 1000; this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list } @@ -115,9 +116,9 @@ class Simulator { const kernelRadiusSq = kernelRadius * kernelRadius; const kernelRadiusInv = 1.0 / kernelRadius; - const restDensity = 4; - const stiffness = 1.; - const nearStiffness = 1.; + const restDensity = 2; + const stiffness = .5; + const nearStiffness = 0.5; // Neighbor cache const neighborIndices = []; @@ -133,95 +134,99 @@ class Simulator { let numNeighbors = 0; - // Compute density and near-density - const bucketX = Math.floor(p0.posX * kernelRadiusInv); - const bucketY = Math.floor(p0.posY * kernelRadiusInv); + if (this.useSpatialHash) { - for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { - for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { - const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + // Compute density and near-density + const bucketX = Math.floor(p0.posX * kernelRadiusInv); + const bucketY = Math.floor(p0.posY * kernelRadiusInv); - let neighborIdx = this.particleListHeads[bucketIdx]; + for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { + for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { + const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); - while (neighborIdx != -1) { - if (neighborIdx <= i) { - neighborIdx = this.particleListNextIdx[neighborIdx]; - continue; - } + let neighborIdx = this.particleListHeads[bucketIdx]; - let p1 = this.particles[neighborIdx]; + while (neighborIdx != -1) { + if (neighborIdx <= i) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } - const diffX = p1.posX - p0.posX; + let p1 = this.particles[neighborIdx]; - if (diffX > kernelRadius || diffX < -kernelRadius) { - neighborIdx = this.particleListNextIdx[neighborIdx]; - continue; - } + const diffX = p1.posX - p0.posX; - const diffY = p1.posY - p0.posY; + if (diffX > kernelRadius || diffX < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } - if (diffY > kernelRadius || diffY < -kernelRadius) { - neighborIdx = this.particleListNextIdx[neighborIdx]; - continue; - } + const diffY = p1.posY - p0.posY; - const rSq = diffX * diffX + diffY * diffY; + if (diffY > kernelRadius || diffY < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } - if (rSq < kernelRadiusSq) { - const r = Math.sqrt(rSq); - const q = r * kernelRadiusInv; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + const rSq = diffX * diffX + diffY * diffY; - density += closeness * closeness; - nearDensity += closeness * closenessSq; + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r * kernelRadiusInv; + const closeness = 1 - q; + const closenessSq = closeness * closeness; - neighborIndices[numNeighbors] = neighborIdx; - neighborUnitX[numNeighbors] = diffX / r; - neighborUnitY[numNeighbors] = diffY / r; - neighborCloseness[numNeighbors] = closeness; - numNeighbors++; - } + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = neighborIdx; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } - neighborIdx = this.particleListNextIdx[neighborIdx]; + neighborIdx = this.particleListNextIdx[neighborIdx]; + } } } - } + } else { + // The old n^2 way - // The old n^2 way - // for (let j = i + 1; j < numParticles; j++) { - // let p1 = this.particles[j]; + for (let j = i + 1; j < numParticles; j++) { + let p1 = this.particles[j]; - // const diffX = p1.posX - p0.posX; + const diffX = p1.posX - p0.posX; - // if (diffX > kernelRadius || diffX < -kernelRadius) { - // continue; - // } + if (diffX > kernelRadius || diffX < -kernelRadius) { + continue; + } - // const diffY = p1.posY - p0.posY; + const diffY = p1.posY - p0.posY; - // if (diffY > kernelRadius || diffY < -kernelRadius) { - // continue; - // } + if (diffY > kernelRadius || diffY < -kernelRadius) { + continue; + } - // const rSq = diffX * diffX + diffY * diffY; + const rSq = diffX * diffX + diffY * diffY; - // if (rSq < kernelRadiusSq) { - // const r = Math.sqrt(rSq); - // const q = r / kernelRadius; - // const closeness = 1 - q; - // const closenessSq = closeness * closeness; + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; - // density += closeness * closeness; - // nearDensity += closeness * closenessSq; + density += closeness * closeness; + nearDensity += closeness * closenessSq; - // neighborIndices[numNeighbors] = j; - // neighborUnitX[numNeighbors] = diffX / r; - // neighborUnitY[numNeighbors] = diffY / r; - // neighborCloseness[numNeighbors] = closeness; - // numNeighbors++; - // } - // } + neighborIndices[numNeighbors] = j; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + } + } // Add wall density const closestX = Math.min(p0.posX, this.width - p0.posX); From 4ce3e637e24a6aafaa2f136cc561c5f6dae2ccc1 Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Mon, 13 May 2024 21:15:35 -0400 Subject: [PATCH 4/7] Fixed ij bug --- run_0.js | 11 +- sim_1.js | 6 +- sim_2.html | 4 +- sim_2.js | 57 +++++--- sim_3.html | 42 ++++++ sim_3.js | 372 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 469 insertions(+), 23 deletions(-) create mode 100644 sim_3.html create mode 100644 sim_3.js diff --git a/run_0.js b/run_0.js index 15fd7f9..d0f5f32 100644 --- a/run_0.js +++ b/run_0.js @@ -45,9 +45,14 @@ document.getElementById("numParticles").addEventListener("input", (e) => { simulator = new Simulator(canvas.width, canvas.height, numParticles); }); -document.getElementById("useSpatialHash").addEventListener("change", (e) => { - simulator.useSpatialHash = e.target.checked; -}); + +let useSpatialHash = document.getElementById("useSpatialHash") + +if (useSpatialHash) { + useSpatialHash.addEventListener("change", (e) => { + simulator.useSpatialHash = e.target.checked; + }); +} window.addEventListener("resize", () => { canvas.width = window.innerWidth; diff --git a/sim_1.js b/sim_1.js index 9ebf8bf..17be3ec 100644 --- a/sim_1.js +++ b/sim_1.js @@ -127,7 +127,11 @@ class Simulator { let numNeighbors = 0; // Compute density and near-density - for (let j = i + 1; j < numParticles; j++) { + for (let j = 0; j < numParticles; j++) { + if (i === j) { + continue; + } + let p1 = this.particles[j]; const diffX = p1.posX - p0.posX; diff --git a/sim_2.html b/sim_2.html index c7e51b9..fb4a243 100644 --- a/sim_2.html +++ b/sim_2.html @@ -22,8 +22,10 @@ +

+

- +

diff --git a/sim_2.js b/sim_2.js index 87b1511..a096af5 100644 --- a/sim_2.js +++ b/sim_2.js @@ -125,6 +125,7 @@ class Simulator { const neighborUnitX = []; const neighborUnitY = []; const neighborCloseness = []; + const visitedBuckets = []; for (let i = 0; i < numParticles; i++) { let p0 = this.particles[i]; @@ -133,9 +134,9 @@ class Simulator { let nearDensity = 0; let numNeighbors = 0; + let numVisitedBuckets = 0; if (this.useSpatialHash) { - // Compute density and near-density const bucketX = Math.floor(p0.posX * kernelRadiusInv); const bucketY = Math.floor(p0.posY * kernelRadiusInv); @@ -144,10 +145,26 @@ class Simulator { for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + // Check hash collision + let found = false; + for (let k = 0; k < numVisitedBuckets; k++) { + if (visitedBuckets[k] === bucketIdx) { + found = true; + break; + } + } + + if (found) { + continue; + } + + visitedBuckets[numVisitedBuckets] = bucketIdx; + numVisitedBuckets++; + let neighborIdx = this.particleListHeads[bucketIdx]; while (neighborIdx != -1) { - if (neighborIdx <= i) { + if (neighborIdx === i) { neighborIdx = this.particleListNextIdx[neighborIdx]; continue; } @@ -193,7 +210,11 @@ class Simulator { } else { // The old n^2 way - for (let j = i + 1; j < numParticles; j++) { + for (let j = 0; j < numParticles; j++) { + if (i === j) { + continue; + } + let p1 = this.particles[j]; const diffX = p1.posX - p0.posX; @@ -232,23 +253,23 @@ class Simulator { const closestX = Math.min(p0.posX, this.width - p0.posX); const closestY = Math.min(p0.posY, this.height - p0.posY); - if (closestX < kernelRadius) { - const q = closestX / kernelRadius; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + // if (closestX < kernelRadius) { + // const q = closestX / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; - density += closeness * closeness; - nearDensity += closeness * closenessSq; - } + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } - if (closestY < kernelRadius) { - const q = closestY / kernelRadius; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + // if (closestY < kernelRadius) { + // const q = closestY / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; - density += closeness * closeness; - nearDensity += closeness * closenessSq; - } + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } // Compute pressure and near-pressure const pressure = stiffness * (density - restDensity); @@ -311,7 +332,7 @@ class Simulator { adjustSprings(dt) { } applyViscosity(dt) { } resolveCollisions(dt) { - const boundaryMul = 1.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce const boundaryMinX = 5; const boundaryMaxX = this.width - 5; const boundaryMinY = 5; diff --git a/sim_3.html b/sim_3.html new file mode 100644 index 0000000..0176f85 --- /dev/null +++ b/sim_3.html @@ -0,0 +1,42 @@ + + + + + PVFS 2.1 - Optimize Neighbor Search + + + + + + + + + +

+

FPS: 0

+

+ + +

+

+ + +

+

+ + + +

+
+ + + + + + + diff --git a/sim_3.js b/sim_3.js new file mode 100644 index 0000000..2006600 --- /dev/null +++ b/sim_3.js @@ -0,0 +1,372 @@ +class Particle { + constructor(posX, posY, velX, velY) { + this.posX = posX; + this.posY = posY; + + this.prevX = posX; + this.prevY = posY; + + this.velX = velX; + this.velY = velY; + + this.dispX = 0; + this.dispY = 0; + } +} + +class Simulator { + constructor(width, height, numParticles) { + this.running = false; + + this.width = width; + this.height = height; + + this.gravX = 0.0; + this.gravY = 0.2; + + this.particles = []; + this.addParticles(numParticles); + + this.screenX = window.screenX; + this.screenY = window.screenY; + + this.useSpatialHash = true; + this.numHashBuckets = 1000; + this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list + this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + } + + start() { this.running = true; } + pause() { this.running = false; } + + resize(width, height) { + this.width = width; + this.height = height; + } + + addParticles(count) { + for (let i = 0; i < count; i++) { + const posX = Math.random() * this.width; + const posY = Math.random() * this.height; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + draw(ctx) { + ctx.save(); + ctx.translate(-5, -5); + + for (let p of this.particles) { + ctx.fillRect(p.posX, p.posY, 10, 10); + } + + ctx.restore(); + } + + // Algorithm 1: Simulation step + update(dt = 1) { + if (!this.running) { + return; + } + + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + + this.screenX = window.screenX; + this.screenY = window.screenY; + + for (let p of this.particles) { + // apply gravity + p.velX += this.gravX * dt; + p.velY += this.gravY * dt; + + p.posX -= screenMoveX; + p.posY -= screenMoveY; + } + + this.applyViscosity(dt); + + for (let p of this.particles) { + // save previous position + p.prevX = p.posX; + p.prevY = p.posY; + + // advance to predicted position + p.posX += p.velX * dt; + p.posY += p.velY * dt; + } + + this.populateHashGrid(); + + this.adjustSprings(dt); + this.applySpringDisplacements(dt); + this.doubleDensityRelaxation(dt); + + this.applyPressureDisplacements(dt); + + this.resolveCollisions(dt); + + for (let p of this.particles) { + // use previous position to calculate new velocity + p.velX = (p.posX - p.prevX) / dt; + p.velY = (p.posY - p.prevY) / dt; + } + } + + doubleDensityRelaxation(dt) { + const numParticles = this.particles.length; + const kernelRadius = 40; // h + const kernelRadiusSq = kernelRadius * kernelRadius; + const kernelRadiusInv = 1.0 / kernelRadius; + + const restDensity = 2; + const stiffness = .5; + const nearStiffness = 0.5; + + // Neighbor cache + const neighborIndices = []; + const neighborUnitX = []; + const neighborUnitY = []; + const neighborCloseness = []; + const visitedBuckets = []; + + for (let i = 0; i < numParticles; i++) { + let p0 = this.particles[i]; + + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + let numVisitedBuckets = 0; + + if (this.useSpatialHash) { + // Compute density and near-density + const bucketX = Math.floor(p0.posX * kernelRadiusInv); + const bucketY = Math.floor(p0.posY * kernelRadiusInv); + + for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { + for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { + const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + + // Check hash collision + let found = false; + for (let k = 0; k < numVisitedBuckets; k++) { + if (visitedBuckets[k] === bucketIdx) { + found = true; + break; + } + } + + if (found) { + continue; + } + + visitedBuckets[numVisitedBuckets] = bucketIdx; + numVisitedBuckets++; + + let neighborIdx = this.particleListHeads[bucketIdx]; + + while (neighborIdx != -1) { + if (neighborIdx === i) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + let p1 = this.particles[neighborIdx]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r * kernelRadiusInv; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = neighborIdx; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + + neighborIdx = this.particleListNextIdx[neighborIdx]; + } + } + } + } else { + // The old n^2 way + + for (let j = 0; j < numParticles; j++) { + if (i === j) { + continue; + } + + let p1 = this.particles[j]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = j; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + } + } + + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); + + if (closestX < kernelRadius) { + const q = closestX / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + if (closestY < kernelRadius) { + const q = closestY / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; + + let dispX = 0; + let dispY = 0; + + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; + + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; + + p1.dispX += DX; + p1.dispY += DY; + + dispX -= DX; + dispY -= DY; + } + + p0.dispX += dispX; + p0.dispY += dispY; + } + } + + // Mueller 10 minute physics + getHashBucketIdx(bucketX, bucketY) { + const h = ((bucketX * 92837111) ^ (bucketY * 689287499)); + return Math.abs(h) % this.numHashBuckets; + } + + populateHashGrid() { + // Clear the hash grid + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads[i] = -1; + } + + // Populate the hash grid + const numParticles = this.particles.length; + const bucketSize = 40; // Same as kernel radius + const bucketSizeInv = 1.0 / bucketSize; + + for (let i = 0; i < numParticles; i++) { + let p = this.particles[i]; + + const bucketX = Math.floor(p.posX * bucketSizeInv); + const bucketY = Math.floor(p.posY * bucketSizeInv); + + const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); + + this.particleListNextIdx[i] = this.particleListHeads[bucketIdx]; + this.particleListHeads[bucketIdx] = i; + } + } + + applyPressureDisplacements(dt) { + for (let p of this.particles) { + p.posX += p.dispX * .5; + p.posY += p.dispY * .5; + + p.dispX = 0; + p.dispY = 0; + } + } + + applySpringDisplacements(dt) { } + adjustSprings(dt) { } + applyViscosity(dt) { } + resolveCollisions(dt) { + const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + + for (let p of this.particles) { + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); + } + + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); + } + } + } +} From f8eda21794864d0507b2241d283f939678ccd153 Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Tue, 14 May 2024 01:57:41 -0400 Subject: [PATCH 5/7] iterate by bucket --- scrap/sim_3.js | 372 +++++++++++++++++++++++++++++++++++++++++++ scrap/symmetric.html | 42 +++++ sim_3.js | 166 ++++++++----------- 3 files changed, 483 insertions(+), 97 deletions(-) create mode 100644 scrap/sim_3.js create mode 100644 scrap/symmetric.html diff --git a/scrap/sim_3.js b/scrap/sim_3.js new file mode 100644 index 0000000..2006600 --- /dev/null +++ b/scrap/sim_3.js @@ -0,0 +1,372 @@ +class Particle { + constructor(posX, posY, velX, velY) { + this.posX = posX; + this.posY = posY; + + this.prevX = posX; + this.prevY = posY; + + this.velX = velX; + this.velY = velY; + + this.dispX = 0; + this.dispY = 0; + } +} + +class Simulator { + constructor(width, height, numParticles) { + this.running = false; + + this.width = width; + this.height = height; + + this.gravX = 0.0; + this.gravY = 0.2; + + this.particles = []; + this.addParticles(numParticles); + + this.screenX = window.screenX; + this.screenY = window.screenY; + + this.useSpatialHash = true; + this.numHashBuckets = 1000; + this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list + this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + } + + start() { this.running = true; } + pause() { this.running = false; } + + resize(width, height) { + this.width = width; + this.height = height; + } + + addParticles(count) { + for (let i = 0; i < count; i++) { + const posX = Math.random() * this.width; + const posY = Math.random() * this.height; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + draw(ctx) { + ctx.save(); + ctx.translate(-5, -5); + + for (let p of this.particles) { + ctx.fillRect(p.posX, p.posY, 10, 10); + } + + ctx.restore(); + } + + // Algorithm 1: Simulation step + update(dt = 1) { + if (!this.running) { + return; + } + + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + + this.screenX = window.screenX; + this.screenY = window.screenY; + + for (let p of this.particles) { + // apply gravity + p.velX += this.gravX * dt; + p.velY += this.gravY * dt; + + p.posX -= screenMoveX; + p.posY -= screenMoveY; + } + + this.applyViscosity(dt); + + for (let p of this.particles) { + // save previous position + p.prevX = p.posX; + p.prevY = p.posY; + + // advance to predicted position + p.posX += p.velX * dt; + p.posY += p.velY * dt; + } + + this.populateHashGrid(); + + this.adjustSprings(dt); + this.applySpringDisplacements(dt); + this.doubleDensityRelaxation(dt); + + this.applyPressureDisplacements(dt); + + this.resolveCollisions(dt); + + for (let p of this.particles) { + // use previous position to calculate new velocity + p.velX = (p.posX - p.prevX) / dt; + p.velY = (p.posY - p.prevY) / dt; + } + } + + doubleDensityRelaxation(dt) { + const numParticles = this.particles.length; + const kernelRadius = 40; // h + const kernelRadiusSq = kernelRadius * kernelRadius; + const kernelRadiusInv = 1.0 / kernelRadius; + + const restDensity = 2; + const stiffness = .5; + const nearStiffness = 0.5; + + // Neighbor cache + const neighborIndices = []; + const neighborUnitX = []; + const neighborUnitY = []; + const neighborCloseness = []; + const visitedBuckets = []; + + for (let i = 0; i < numParticles; i++) { + let p0 = this.particles[i]; + + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + let numVisitedBuckets = 0; + + if (this.useSpatialHash) { + // Compute density and near-density + const bucketX = Math.floor(p0.posX * kernelRadiusInv); + const bucketY = Math.floor(p0.posY * kernelRadiusInv); + + for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { + for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { + const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + + // Check hash collision + let found = false; + for (let k = 0; k < numVisitedBuckets; k++) { + if (visitedBuckets[k] === bucketIdx) { + found = true; + break; + } + } + + if (found) { + continue; + } + + visitedBuckets[numVisitedBuckets] = bucketIdx; + numVisitedBuckets++; + + let neighborIdx = this.particleListHeads[bucketIdx]; + + while (neighborIdx != -1) { + if (neighborIdx === i) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + let p1 = this.particles[neighborIdx]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r * kernelRadiusInv; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = neighborIdx; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + + neighborIdx = this.particleListNextIdx[neighborIdx]; + } + } + } + } else { + // The old n^2 way + + for (let j = 0; j < numParticles; j++) { + if (i === j) { + continue; + } + + let p1 = this.particles[j]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = j; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + } + } + + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); + + if (closestX < kernelRadius) { + const q = closestX / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + if (closestY < kernelRadius) { + const q = closestY / kernelRadius; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + } + + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; + + let dispX = 0; + let dispY = 0; + + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; + + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; + + p1.dispX += DX; + p1.dispY += DY; + + dispX -= DX; + dispY -= DY; + } + + p0.dispX += dispX; + p0.dispY += dispY; + } + } + + // Mueller 10 minute physics + getHashBucketIdx(bucketX, bucketY) { + const h = ((bucketX * 92837111) ^ (bucketY * 689287499)); + return Math.abs(h) % this.numHashBuckets; + } + + populateHashGrid() { + // Clear the hash grid + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads[i] = -1; + } + + // Populate the hash grid + const numParticles = this.particles.length; + const bucketSize = 40; // Same as kernel radius + const bucketSizeInv = 1.0 / bucketSize; + + for (let i = 0; i < numParticles; i++) { + let p = this.particles[i]; + + const bucketX = Math.floor(p.posX * bucketSizeInv); + const bucketY = Math.floor(p.posY * bucketSizeInv); + + const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); + + this.particleListNextIdx[i] = this.particleListHeads[bucketIdx]; + this.particleListHeads[bucketIdx] = i; + } + } + + applyPressureDisplacements(dt) { + for (let p of this.particles) { + p.posX += p.dispX * .5; + p.posY += p.dispY * .5; + + p.dispX = 0; + p.dispY = 0; + } + } + + applySpringDisplacements(dt) { } + adjustSprings(dt) { } + applyViscosity(dt) { } + resolveCollisions(dt) { + const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + + for (let p of this.particles) { + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); + } + + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); + } + } + } +} diff --git a/scrap/symmetric.html b/scrap/symmetric.html new file mode 100644 index 0000000..0176f85 --- /dev/null +++ b/scrap/symmetric.html @@ -0,0 +1,42 @@ + + + + + PVFS 2.1 - Optimize Neighbor Search + + + + + + + + + +
+

FPS: 0

+

+ + +

+

+ + +

+

+ + + +

+
+ + + + + + + diff --git a/sim_3.js b/sim_3.js index 2006600..c145ce2 100644 --- a/sim_3.js +++ b/sim_3.js @@ -8,9 +8,6 @@ class Particle { this.velX = velX; this.velY = velY; - - this.dispX = 0; - this.dispY = 0; } } @@ -32,7 +29,15 @@ class Simulator { this.useSpatialHash = true; this.numHashBuckets = 1000; + this.numActiveBuckets = 0; + this.activeBuckets = []; this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list + + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads.push(-1); + this.activeBuckets.push(0); + } + this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list } @@ -104,9 +109,6 @@ class Simulator { this.adjustSprings(dt); this.applySpringDisplacements(dt); this.doubleDensityRelaxation(dt); - - this.applyPressureDisplacements(dt); - this.resolveCollisions(dt); for (let p of this.particles) { @@ -133,16 +135,20 @@ class Simulator { const neighborCloseness = []; const visitedBuckets = []; - for (let i = 0; i < numParticles; i++) { - let p0 = this.particles[i]; + const numActiveBuckets = this.numActiveBuckets; + + for (let abIdx = 0; abIdx < numActiveBuckets; abIdx++) { + let selfIdx = this.particleListHeads[this.activeBuckets[abIdx]]; - let density = 0; - let nearDensity = 0; + while (selfIdx != -1) { + let p0 = this.particles[selfIdx]; - let numNeighbors = 0; - let numVisitedBuckets = 0; + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + let numVisitedBuckets = 0; - if (this.useSpatialHash) { // Compute density and near-density const bucketX = Math.floor(p0.posX * kernelRadiusInv); const bucketY = Math.floor(p0.posY * kernelRadiusInv); @@ -170,7 +176,7 @@ class Simulator { let neighborIdx = this.particleListHeads[bucketIdx]; while (neighborIdx != -1) { - if (neighborIdx === i) { + if (neighborIdx === selfIdx) { neighborIdx = this.particleListNextIdx[neighborIdx]; continue; } @@ -213,94 +219,57 @@ class Simulator { } } } - } else { - // The old n^2 way - - for (let j = 0; j < numParticles; j++) { - if (i === j) { - continue; - } - - let p1 = this.particles[j]; - - const diffX = p1.posX - p0.posX; - - if (diffX > kernelRadius || diffX < -kernelRadius) { - continue; - } - - const diffY = p1.posY - p0.posY; - - if (diffY > kernelRadius || diffY < -kernelRadius) { - continue; - } - const rSq = diffX * diffX + diffY * diffY; - if (rSq < kernelRadiusSq) { - const r = Math.sqrt(rSq); - const q = r / kernelRadius; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); - density += closeness * closeness; - nearDensity += closeness * closenessSq; + // if (closestX < kernelRadius) { + // const q = closestX / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; - neighborIndices[numNeighbors] = j; - neighborUnitX[numNeighbors] = diffX / r; - neighborUnitY[numNeighbors] = diffY / r; - neighborCloseness[numNeighbors] = closeness; - numNeighbors++; - } - } - } + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } - // Add wall density - const closestX = Math.min(p0.posX, this.width - p0.posX); - const closestY = Math.min(p0.posY, this.height - p0.posY); + // if (closestY < kernelRadius) { + // const q = closestY / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; - if (closestX < kernelRadius) { - const q = closestX / kernelRadius; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } - density += closeness * closeness; - nearDensity += closeness * closenessSq; - } + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; - if (closestY < kernelRadius) { - const q = closestY / kernelRadius; - const closeness = 1 - q; - const closenessSq = closeness * closeness; + let dispX = 0; + let dispY = 0; - density += closeness * closeness; - nearDensity += closeness * closenessSq; - } - - // Compute pressure and near-pressure - const pressure = stiffness * (density - restDensity); - const nearPressure = nearStiffness * nearDensity; + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; - let dispX = 0; - let dispY = 0; + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; - for (let j = 0; j < numNeighbors; j++) { - let p1 = this.particles[neighborIndices[j]]; + p1.posX += DX; + p1.posY += DY; - const closeness = neighborCloseness[j]; - const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; - const DX = D * neighborUnitX[j]; - const DY = D * neighborUnitY[j]; + dispX -= DX; + dispY -= DY; + } - p1.dispX += DX; - p1.dispY += DY; + p0.posX += dispX; + p0.posY += dispY; - dispX -= DX; - dispY -= DY; + selfIdx = this.particleListNextIdx[selfIdx]; } - - p0.dispX += dispX; - p0.dispY += dispY; } } @@ -312,10 +281,16 @@ class Simulator { populateHashGrid() { // Clear the hash grid + for (let i = 0; i < this.numActiveBuckets; i++) { + this.particleListHeads[this.activeBuckets[i]] = -1; + } + for (let i = 0; i < this.numHashBuckets; i++) { this.particleListHeads[i] = -1; } + this.numActiveBuckets = 0; + // Populate the hash grid const numParticles = this.particles.length; const bucketSize = 40; // Same as kernel radius @@ -329,18 +304,15 @@ class Simulator { const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); - this.particleListNextIdx[i] = this.particleListHeads[bucketIdx]; - this.particleListHeads[bucketIdx] = i; - } - } + const head = this.particleListHeads[bucketIdx]; - applyPressureDisplacements(dt) { - for (let p of this.particles) { - p.posX += p.dispX * .5; - p.posY += p.dispY * .5; + if (head === -1) { + this.activeBuckets[this.numActiveBuckets] = bucketIdx; + this.numActiveBuckets++; + } - p.dispX = 0; - p.dispY = 0; + this.particleListNextIdx[i] = head; + this.particleListHeads[bucketIdx] = i; } } From c13e21c61dc3a7003b3c5932276daf0e9bfd5a76 Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Tue, 14 May 2024 09:24:31 -0400 Subject: [PATCH 6/7] before adding wall particles --- scrap/symmetric.html => scrap_0.html | 2 +- scrap/sim_3.js => scrap_0.js | 0 sim_2.html | 2 +- sim_2.js | 61 ++-- sim_3.html | 2 +- sim_3.js | 125 +++++++-- sim_4.html | 42 +++ sim_4.js | 403 +++++++++++++++++++++++++++ 8 files changed, 577 insertions(+), 60 deletions(-) rename scrap/symmetric.html => scrap_0.html (96%) rename scrap/sim_3.js => scrap_0.js (100%) create mode 100644 sim_4.html create mode 100644 sim_4.js diff --git a/scrap/symmetric.html b/scrap_0.html similarity index 96% rename from scrap/symmetric.html rename to scrap_0.html index 0176f85..7a8eff4 100644 --- a/scrap/symmetric.html +++ b/scrap_0.html @@ -35,7 +35,7 @@ - + diff --git a/scrap/sim_3.js b/scrap_0.js similarity index 100% rename from scrap/sim_3.js rename to scrap_0.js diff --git a/sim_2.html b/sim_2.html index fb4a243..00ad864 100644 --- a/sim_2.html +++ b/sim_2.html @@ -2,7 +2,7 @@ - PVFS 2.1 - Optimize Neighbor Search + PVFS 2.1 - Spatial Hash Neighbor Search diff --git a/sim_2.js b/sim_2.js index a096af5..43e90c1 100644 --- a/sim_2.js +++ b/sim_2.js @@ -11,6 +11,18 @@ class Particle { } } +class Material { + constructor(name, restDensity, stiffness, nearStiffness, kernelRadius) { + this.name = name; + this.restDensity = restDensity; + this.stiffness = stiffness; + this.nearStiffness = nearStiffness; + this.kernelRadius = kernelRadius; + + this.maxPressure = 1; + } +} + class Simulator { constructor(width, height, numParticles) { this.running = false; @@ -28,9 +40,11 @@ class Simulator { this.screenY = window.screenY; this.useSpatialHash = true; - this.numHashBuckets = 1000; + this.numHashBuckets = 5000; this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + + this.material = new Material("water", 2, 0.5, 0.5, 40); } start() { this.running = true; } @@ -112,13 +126,13 @@ class Simulator { doubleDensityRelaxation(dt) { const numParticles = this.particles.length; - const kernelRadius = 40; // h + const kernelRadius = this.material.kernelRadius; // h const kernelRadiusSq = kernelRadius * kernelRadius; const kernelRadiusInv = 1.0 / kernelRadius; - const restDensity = 2; - const stiffness = .5; - const nearStiffness = 0.5; + const restDensity = this.material.restDensity; + const stiffness = this.material.stiffness; + const nearStiffness = this.material.nearStiffness; // Neighbor cache const neighborIndices = []; @@ -249,31 +263,17 @@ class Simulator { } } - // Add wall density - const closestX = Math.min(p0.posX, this.width - p0.posX); - const closestY = Math.min(p0.posY, this.height - p0.posY); - - // if (closestX < kernelRadius) { - // const q = closestX / kernelRadius; - // const closeness = 1 - q; - // const closenessSq = closeness * closeness; - - // density += closeness * closeness; - // nearDensity += closeness * closenessSq; - // } - - // if (closestY < kernelRadius) { - // const q = closestY / kernelRadius; - // const closeness = 1 - q; - // const closenessSq = closeness * closeness; + // Compute pressure and near-pressure + let pressure = stiffness * (density - restDensity); + let nearPressure = nearStiffness * nearDensity; - // density += closeness * closeness; - // nearDensity += closeness * closenessSq; - // } + if (pressure > 1) { + pressure = 1; + } - // Compute pressure and near-pressure - const pressure = stiffness * (density - restDensity); - const nearPressure = nearStiffness * nearDensity; + if (nearPressure > 1) { + nearPressure = 1; + } let dispX = 0; let dispY = 0; @@ -291,6 +291,9 @@ class Simulator { dispX -= DX; dispY -= DY; + + // p0.posX -= DX; + // p0.posY -= DY; } p0.posX += dispX; @@ -312,7 +315,7 @@ class Simulator { // Populate the hash grid const numParticles = this.particles.length; - const bucketSize = 40; // Same as kernel radius + const bucketSize = this.material.kernelRadius; // Same as kernel radius const bucketSizeInv = 1.0 / bucketSize; for (let i = 0; i < numParticles; i++) { diff --git a/sim_3.html b/sim_3.html index 0176f85..9fc4b63 100644 --- a/sim_3.html +++ b/sim_3.html @@ -2,7 +2,7 @@ - PVFS 2.1 - Optimize Neighbor Search + PVFS 2.2 - Iterate by Bucket Instead of Particle diff --git a/sim_3.js b/sim_3.js index c145ce2..6d429cb 100644 --- a/sim_3.js +++ b/sim_3.js @@ -11,6 +11,18 @@ class Particle { } } +class Material { + constructor(name, restDensity, stiffness, nearStiffness, kernelRadius) { + this.name = name; + this.restDensity = restDensity; + this.stiffness = stiffness; + this.nearStiffness = nearStiffness; + this.kernelRadius = kernelRadius; + + this.maxPressure = 1; + } +} + class Simulator { constructor(width, height, numParticles) { this.running = false; @@ -28,7 +40,7 @@ class Simulator { this.screenY = window.screenY; this.useSpatialHash = true; - this.numHashBuckets = 1000; + this.numHashBuckets = 5000; this.numActiveBuckets = 0; this.activeBuckets = []; this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list @@ -39,6 +51,8 @@ class Simulator { } this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + + this.material = new Material("water", 2, 0.5, 0.5, 40); } start() { this.running = true; } @@ -120,13 +134,13 @@ class Simulator { doubleDensityRelaxation(dt) { const numParticles = this.particles.length; - const kernelRadius = 40; // h + const kernelRadius = this.material.kernelRadius; // h const kernelRadiusSq = kernelRadius * kernelRadius; const kernelRadiusInv = 1.0 / kernelRadius; - const restDensity = 2; - const stiffness = .5; - const nearStiffness = 0.5; + const restDensity = this.material.restDensity; + const stiffness = this.material.stiffness; + const nearStiffness = this.material.nearStiffness; // Neighbor cache const neighborIndices = []; @@ -137,6 +151,20 @@ class Simulator { const numActiveBuckets = this.numActiveBuckets; + const wallCloseness = [0, 0]; // x, y + const wallDirection = [0, 0]; // x, y + + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + const softMinX = boundaryMinX + kernelRadius; + const softMaxX = boundaryMaxX - kernelRadius; + const softMinY = boundaryMinY + kernelRadius; + const softMaxY = boundaryMaxY - kernelRadius; + + for (let abIdx = 0; abIdx < numActiveBuckets; abIdx++) { let selfIdx = this.particleListHeads[this.activeBuckets[abIdx]]; @@ -220,32 +248,59 @@ class Simulator { } } - // Add wall density - const closestX = Math.min(p0.posX, this.width - p0.posX); - const closestY = Math.min(p0.posY, this.height - p0.posY); + if (p0.posX < softMinX) { + wallCloseness[0] = 1 - (softMinX - Math.max(boundaryMinX, p0.posX)) * kernelRadiusInv; + wallDirection[0] = 1; + } else if (p0.posX > softMaxX) { + wallCloseness[0] = 1 - (Math.min(boundaryMaxX, p0.posX) - softMaxX) * kernelRadiusInv; + wallDirection[0] = -1; + } else { + wallCloseness[0] = 0; + } - // if (closestX < kernelRadius) { - // const q = closestX / kernelRadius; - // const closeness = 1 - q; - // const closenessSq = closeness * closeness; + if (p0.posY < softMinY) { + wallCloseness[1] = 1 - (softMinY - Math.max(boundaryMinY, p0.posY)) * kernelRadiusInv; + wallDirection[1] = 1; + } else if (p0.posY > softMaxY) { + wallCloseness[1] = 1 - (Math.min(boundaryMaxY, p0.posY) - softMaxY) * kernelRadiusInv; + wallDirection[1] = -1; + } else { + wallCloseness[1] = 0; + } - // density += closeness * closeness; - // nearDensity += closeness * closenessSq; - // } + if (wallCloseness[0] > 0) { + density += wallCloseness[0] * wallCloseness[0]; + nearDensity += wallCloseness[0] * wallCloseness[0] * wallCloseness[0]; + } + + if (wallCloseness[1] > 0) { + density += wallCloseness[1] * wallCloseness[1]; + nearDensity += wallCloseness[1] * wallCloseness[1] * wallCloseness[1]; + } + + // Compute pressure and near-pressure + let pressure = stiffness * (density - restDensity); + let nearPressure = nearStiffness * nearDensity; + let immisciblePressure = stiffness * (density - 1); + + // Clamp pressure for stability + const pressureSum = pressure + nearPressure; + + if (pressureSum > 1) { + const pressureMul = 1 / pressureSum; + pressure *= pressureMul; + nearPressure *= pressureMul; + } - // if (closestY < kernelRadius) { - // const q = closestY / kernelRadius; - // const closeness = 1 - q; - // const closenessSq = closeness * closeness; - // density += closeness * closeness; - // nearDensity += closeness * closenessSq; + // if (pressure > 1) { + // pressure = 1; // } - // Compute pressure and near-pressure - const pressure = stiffness * (density - restDensity); - const nearPressure = nearStiffness * nearDensity; + // if (nearPressure > 1) { + // nearPressure = 1; + // } let dispX = 0; let dispY = 0; @@ -263,8 +318,22 @@ class Simulator { dispX -= DX; dispY -= DY; + + // p0.posX -= DX; + // p0.posY -= DY; + } + + if (wallCloseness[0] > 0) { + const D = dt * dt * (0 * wallCloseness[0] + nearPressure * wallCloseness[0] * wallCloseness[0]) / 2; + p0.posX += D * wallDirection[0]; + } + + if (wallCloseness[1] > 0) { + const D = dt * dt * (0 * wallCloseness[1] + nearPressure * wallCloseness[1] * wallCloseness[1]) / 2; + p0.posY += D * wallDirection[1]; } + p0.posX += dispX; p0.posY += dispY; @@ -293,7 +362,7 @@ class Simulator { // Populate the hash grid const numParticles = this.particles.length; - const bucketSize = 40; // Same as kernel radius + const bucketSize = this.material.kernelRadius; // Same as kernel radius const bucketSizeInv = 1.0 / bucketSize; for (let i = 0; i < numParticles; i++) { @@ -304,14 +373,14 @@ class Simulator { const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); - const head = this.particleListHeads[bucketIdx]; + const headIdx = this.particleListHeads[bucketIdx]; - if (head === -1) { + if (headIdx === -1) { this.activeBuckets[this.numActiveBuckets] = bucketIdx; this.numActiveBuckets++; } - this.particleListNextIdx[i] = head; + this.particleListNextIdx[i] = headIdx; this.particleListHeads[bucketIdx] = i; } } diff --git a/sim_4.html b/sim_4.html new file mode 100644 index 0000000..5d2b5c2 --- /dev/null +++ b/sim_4.html @@ -0,0 +1,42 @@ + + + + + PVFS 2.3 - Local Bucket "Cache" + + + + + + + + + +
+

FPS: 0

+

+ + +

+

+ + +

+

+ + + +

+
+ + + + + + + diff --git a/sim_4.js b/sim_4.js new file mode 100644 index 0000000..0753884 --- /dev/null +++ b/sim_4.js @@ -0,0 +1,403 @@ +class Particle { + constructor(posX, posY, velX, velY) { + this.posX = posX; + this.posY = posY; + + this.prevX = posX; + this.prevY = posY; + + this.velX = velX; + this.velY = velY; + } +} + +class Simulator { + constructor(width, height, numParticles) { + this.running = false; + + this.width = width; + this.height = height; + + this.gravX = 0.0; + this.gravY = 0.2; + + this.particles = []; + this.addParticles(numParticles); + + this.screenX = window.screenX; + this.screenY = window.screenY; + + this.useSpatialHash = true; + this.numHashBuckets = 1000; + this.numActiveBuckets = 0; + this.activeBuckets = []; + this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list + + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads.push(-1); + this.activeBuckets.push(0); + } + + this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list + } + + start() { this.running = true; } + pause() { this.running = false; } + + resize(width, height) { + this.width = width; + this.height = height; + } + + addParticles(count) { + for (let i = 0; i < count; i++) { + const posX = Math.random() * this.width; + const posY = Math.random() * this.height; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + draw(ctx) { + ctx.save(); + ctx.translate(-5, -5); + + for (let p of this.particles) { + ctx.fillRect(p.posX, p.posY, 10, 10); + } + + ctx.restore(); + } + + // Algorithm 1: Simulation step + update(dt = 1) { + if (!this.running) { + return; + } + + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + + this.screenX = window.screenX; + this.screenY = window.screenY; + + for (let p of this.particles) { + // apply gravity + p.velX += this.gravX * dt; + p.velY += this.gravY * dt; + + p.posX -= screenMoveX; + p.posY -= screenMoveY; + } + + this.applyViscosity(dt); + + for (let p of this.particles) { + // save previous position + p.prevX = p.posX; + p.prevY = p.posY; + + // advance to predicted position + p.posX += p.velX * dt; + p.posY += p.velY * dt; + } + + this.populateHashGrid(); + + this.adjustSprings(dt); + this.applySpringDisplacements(dt); + this.doubleDensityRelaxation(dt); + this.resolveCollisions(dt); + + for (let p of this.particles) { + // use previous position to calculate new velocity + p.velX = (p.posX - p.prevX) / dt; + p.velY = (p.posY - p.prevY) / dt; + } + } + + doubleDensityRelaxation(dt) { + const numParticles = this.particles.length; + const kernelRadius = 40; // h + const kernelRadiusSq = kernelRadius * kernelRadius; + const kernelRadiusInv = 1.0 / kernelRadius; + + const restDensity = 2; + const stiffness = .5; + const nearStiffness = 0.5; + + // Neighbor cache + const neighborIndices = []; + const neighborUnitX = []; + const neighborUnitY = []; + const neighborCloseness = []; + const visitedBuckets = []; + + const numActiveBuckets = this.numActiveBuckets; + + const centerBucketParticles = []; + const centerBucketParticleVisited = []; + const centerBucketX = []; + const centerBucketY = []; + + + for (let abIdx = 0; abIdx < numActiveBuckets; abIdx++) { + let selfIdx = this.particleListHeads[this.activeBuckets[abIdx]]; + + let numBucketParticles = 0; + + while (selfIdx != -1) { + const p = this.particles[selfIdx]; + centerBucketParticles[numBucketParticles] = p; + centerBucketParticleVisited[numBucketParticles] = false; + centerBucketX[numBucketParticles] = Math.floor(p.posX * kernelRadiusInv); + centerBucketY[numBucketParticles] = Math.floor(p.posY * kernelRadiusInv); + + numBucketParticles++; + + selfIdx = this.particleListNextIdx[selfIdx]; + } + + let numVisited = 0; + let firstUnvisitedIdx = 0; + + while (numVisited < numBucketParticles) { + let bucketX = centerBucketX[firstUnvisitedIdx]; + let bucketY = centerBucketY[firstUnvisitedIdx]; + + let mismatchFound = false; + + for (let visitIdx = firstUnvisitedIdx; visitIdx < numBucketParticles; visitIdx++) { + if (centerBucketParticleVisited[visitIdx]) { + continue; + } + + if (centerBucketX[visitIdx] !== bucketX || centerBucketY[visitIdx] !== bucketY) { + if (!mismatchFound) { + firstUnvisitedIdx = visitIdx; + mismatchFound = true; + } + + continue; + } + + + + centerBucketParticleVisited[visitIdx] = true; + numVisited++; + } + } + + + for (let i = 0; i < numBucketParticles; i++) { + + } + } + + for (let abIdx = 0; abIdx < numActiveBuckets; abIdx++) { + let selfIdx = this.particleListHeads[this.activeBuckets[abIdx]]; + + while (selfIdx != -1) { + let p0 = this.particles[selfIdx]; + + let density = 0; + let nearDensity = 0; + + let numNeighbors = 0; + let numVisitedBuckets = 0; + + // Compute density and near-density + const bucketX = Math.floor(p0.posX * kernelRadiusInv); + const bucketY = Math.floor(p0.posY * kernelRadiusInv); + + for (let bucketDX = -1; bucketDX <= 1; bucketDX++) { + for (let bucketDY = -1; bucketDY <= 1; bucketDY++) { + const bucketIdx = this.getHashBucketIdx(Math.floor(bucketX + bucketDX), Math.floor(bucketY + bucketDY)); + + // Check hash collision + let found = false; + for (let k = 0; k < numVisitedBuckets; k++) { + if (visitedBuckets[k] === bucketIdx) { + found = true; + break; + } + } + + if (found) { + continue; + } + + visitedBuckets[numVisitedBuckets] = bucketIdx; + numVisitedBuckets++; + + let neighborIdx = this.particleListHeads[bucketIdx]; + + while (neighborIdx != -1) { + if (neighborIdx === selfIdx) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + let p1 = this.particles[neighborIdx]; + + const diffX = p1.posX - p0.posX; + + if (diffX > kernelRadius || diffX < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const diffY = p1.posY - p0.posY; + + if (diffY > kernelRadius || diffY < -kernelRadius) { + neighborIdx = this.particleListNextIdx[neighborIdx]; + continue; + } + + const rSq = diffX * diffX + diffY * diffY; + + if (rSq < kernelRadiusSq) { + const r = Math.sqrt(rSq); + const q = r * kernelRadiusInv; + const closeness = 1 - q; + const closenessSq = closeness * closeness; + + density += closeness * closeness; + nearDensity += closeness * closenessSq; + + neighborIndices[numNeighbors] = neighborIdx; + neighborUnitX[numNeighbors] = diffX / r; + neighborUnitY[numNeighbors] = diffY / r; + neighborCloseness[numNeighbors] = closeness; + numNeighbors++; + } + + neighborIdx = this.particleListNextIdx[neighborIdx]; + } + } + } + + + // Add wall density + const closestX = Math.min(p0.posX, this.width - p0.posX); + const closestY = Math.min(p0.posY, this.height - p0.posY); + + // if (closestX < kernelRadius) { + // const q = closestX / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; + + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } + + // if (closestY < kernelRadius) { + // const q = closestY / kernelRadius; + // const closeness = 1 - q; + // const closenessSq = closeness * closeness; + + // density += closeness * closeness; + // nearDensity += closeness * closenessSq; + // } + + // Compute pressure and near-pressure + const pressure = stiffness * (density - restDensity); + const nearPressure = nearStiffness * nearDensity; + + let dispX = 0; + let dispY = 0; + + for (let j = 0; j < numNeighbors; j++) { + let p1 = this.particles[neighborIndices[j]]; + + const closeness = neighborCloseness[j]; + const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const DX = D * neighborUnitX[j]; + const DY = D * neighborUnitY[j]; + + p1.posX += DX; + p1.posY += DY; + + dispX -= DX; + dispY -= DY; + } + + p0.posX += dispX; + p0.posY += dispY; + + selfIdx = this.particleListNextIdx[selfIdx]; + } + } + } + + // Mueller 10 minute physics + getHashBucketIdx(bucketX, bucketY) { + const h = ((bucketX * 92837111) ^ (bucketY * 689287499)); + return Math.abs(h) % this.numHashBuckets; + } + + populateHashGrid() { + // Clear the hash grid + for (let i = 0; i < this.numActiveBuckets; i++) { + this.particleListHeads[this.activeBuckets[i]] = -1; + } + + for (let i = 0; i < this.numHashBuckets; i++) { + this.particleListHeads[i] = -1; + } + + this.numActiveBuckets = 0; + + // Populate the hash grid + const numParticles = this.particles.length; + const bucketSize = 40; // Same as kernel radius + const bucketSizeInv = 1.0 / bucketSize; + + for (let i = 0; i < numParticles; i++) { + let p = this.particles[i]; + + const bucketX = Math.floor(p.posX * bucketSizeInv); + const bucketY = Math.floor(p.posY * bucketSizeInv); + + const bucketIdx = this.getHashBucketIdx(bucketX, bucketY); + + const head = this.particleListHeads[bucketIdx]; + + if (head === -1) { + this.activeBuckets[this.numActiveBuckets] = bucketIdx; + this.numActiveBuckets++; + } + + this.particleListNextIdx[i] = head; + this.particleListHeads[bucketIdx] = i; + } + } + + applySpringDisplacements(dt) { } + adjustSprings(dt) { } + applyViscosity(dt) { } + resolveCollisions(dt) { + const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + + + for (let p of this.particles) { + if (p.posX < boundaryMinX) { + p.posX += boundaryMul * (boundaryMinX - p.posX); + } else if (p.posX > boundaryMaxX) { + p.posX += boundaryMul * (boundaryMaxX - p.posX); + } + + if (p.posY < boundaryMinY) { + p.posY += boundaryMul * (boundaryMinY - p.posY); + } else if (p.posY > boundaryMaxY) { + p.posY += boundaryMul * (boundaryMaxY - p.posY); + } + } + } +} From f923fd773dfd8c839354c6dfc89ef947e7137b7a Mon Sep 17 00:00:00 2001 From: Grant Kot Date: Tue, 14 May 2024 12:12:43 -0400 Subject: [PATCH 7/7] add parameters --- index.html | 47 +++++++++- run_0.js | 72 +++++++++++++-- sim_3.html | 39 +++++++- sim_3.js | 259 +++++++++++++++++++++++++++++++++++++++-------------- style.css | 5 +- 5 files changed, 341 insertions(+), 81 deletions(-) diff --git a/index.html b/index.html index b55b2bf..82979db 100644 --- a/index.html +++ b/index.html @@ -18,11 +18,50 @@

+

+ + +

+

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+

@@ -31,8 +70,8 @@ - - + + diff --git a/run_0.js b/run_0.js index d0f5f32..3399780 100644 --- a/run_0.js +++ b/run_0.js @@ -24,7 +24,27 @@ function loop() { loop(); // Event listeners +const materialSliders = ["restDensity", "stiffness", "nearStiffness", "kernelRadius", "pointSize", "gravX", "gravY", "dt"]; + +for (let sliderId of materialSliders) { + let slider = document.getElementById(sliderId); + + if (slider) { + slider.addEventListener("input", (e) => { + simulator.material[sliderId] = e.target.value; + }); + } +} + document.getElementById("startButton").addEventListener("click", () => { + for (let sliderId of materialSliders) { + let slider = document.getElementById(sliderId); + + if (slider) { + simulator.material[sliderId] = slider.value; + } + } + simulator.start(); }); @@ -45,13 +65,14 @@ document.getElementById("numParticles").addEventListener("input", (e) => { simulator = new Simulator(canvas.width, canvas.height, numParticles); }); +{ + let useSpatialHash = document.getElementById("useSpatialHash") -let useSpatialHash = document.getElementById("useSpatialHash") - -if (useSpatialHash) { - useSpatialHash.addEventListener("change", (e) => { - simulator.useSpatialHash = e.target.checked; - }); + if (useSpatialHash) { + useSpatialHash.addEventListener("change", (e) => { + simulator.useSpatialHash = e.target.checked; + }); + } } window.addEventListener("resize", () => { @@ -59,3 +80,42 @@ window.addEventListener("resize", () => { canvas.height = window.innerHeight; simulator.resize(canvas.width, canvas.height); }); + +window.addEventListener("mousemove", (e) => { + simulator.mouseX = e.clientX; + simulator.mouseY = e.clientY; +}); + +window.addEventListener("mousedown", (e) => { + if (e.button == 0) { + simulator.drag = true; + } +}); + +window.addEventListener("mouseup", (e) => { + if (e.button == 0) { + simulator.drag = false; + } +}); + +const actionKeys = { "e": "emit", "d": "drain", "a": "attract", "r": "repel" }; + +window.addEventListener("keydown", (e) => { + if (actionKeys[e.key]) { + simulator[actionKeys[e.key]] = true; + } +}); + +window.addEventListener("keyup", (e) => { + if (actionKeys[e.key]) { + simulator[actionKeys[e.key]] = false; + } +}); + +window.addEventListener("blur", () => { + for (let key in actionKeys) { + simulator[actionKeys[key]] = false; + } + + simulator.drag = false; +}); diff --git a/sim_3.html b/sim_3.html index 9fc4b63..49bc796 100644 --- a/sim_3.html +++ b/sim_3.html @@ -24,9 +24,44 @@

- - + +

+

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ + +

+

diff --git a/sim_3.js b/sim_3.js index 6d429cb..0c24ca0 100644 --- a/sim_3.js +++ b/sim_3.js @@ -11,6 +11,15 @@ class Particle { } } +function moveParticleData(dst, src) { + dst.posX = src.posX; + dst.posY = src.posY; + dst.prevX = src.prevX; + dst.prevY = src.prevY; + dst.velX = src.velX; + dst.velY = src.velY; +} + class Material { constructor(name, restDensity, stiffness, nearStiffness, kernelRadius) { this.name = name; @@ -18,6 +27,10 @@ class Material { this.stiffness = stiffness; this.nearStiffness = nearStiffness; this.kernelRadius = kernelRadius; + this.pointSize = 5; + this.gravX = 0.0; + this.gravY = 0.5; + this.dt = 1; this.maxPressure = 1; } @@ -30,17 +43,25 @@ class Simulator { this.width = width; this.height = height; - this.gravX = 0.0; - this.gravY = 0.2; - this.particles = []; this.addParticles(numParticles); this.screenX = window.screenX; this.screenY = window.screenY; + this.mouseX = width / 2; + this.mouseY = height / 2; + this.attract = false; + this.repel = false; + this.emit = false; + this.drain = false; + this.drag = false; + + this.mousePrevX = this.mouseX; + this.mousePrevY = this.mouseY; + this.useSpatialHash = true; - this.numHashBuckets = 5000; + this.numHashBuckets = 10000; this.numActiveBuckets = 0; this.activeBuckets = []; this.particleListHeads = []; // Same size as numHashBuckets, each points to first particle in bucket list @@ -52,7 +73,7 @@ class Simulator { this.particleListNextIdx = []; // Same size as particles list, each points to next particle in bucket list - this.material = new Material("water", 2, 0.5, 0.5, 40); + this.material = new Material("water", .5, 0.5, 0.5, 40); } start() { this.running = true; } @@ -74,33 +95,123 @@ class Simulator { } } + emitParticles() { + const emitRate = 10; + + for (let i = 0; i < emitRate; i++) { + const posX = this.mouseX + Math.random() * 10 - 5; + const posY = this.mouseY + Math.random() * 10 - 5; + const velX = Math.random() * 2 - 1; + const velY = Math.random() * 2 - 1; + + this.particles.push(new Particle(posX, posY, velX, velY)); + } + } + + drainParticles() { + let numParticles = this.particles.length; + + for (let i = 0; i < numParticles; i++) { + let p = this.particles[i]; + + const dx = p.posX - this.mouseX; + const dy = p.posY - this.mouseY; + const distSq = dx * dx + dy * dy; + + if (distSq < 10000) { + moveParticleData(p, this.particles[numParticles - 1]); + numParticles--; + } + } + + this.particles.length = numParticles; + } + draw(ctx) { ctx.save(); - ctx.translate(-5, -5); + + const pointSize = this.material.pointSize; + + ctx.translate(-.5 * pointSize, -.5 * pointSize); for (let p of this.particles) { - ctx.fillRect(p.posX, p.posY, 10, 10); + const speed = (p.velX * p.velX + p.velY * p.velY); + ctx.fillStyle = `rgb(${speed}, ${speed * 0.4 + 153}, 255)`; + + ctx.fillRect(p.posX, p.posY, pointSize, pointSize); } ctx.restore(); } // Algorithm 1: Simulation step - update(dt = 1) { + update() { + const screenMoveX = window.screenX - this.screenX; + const screenMoveY = window.screenY - this.screenY; + this.screenX = window.screenX; + this.screenY = window.screenY; + + const dragX = this.mouseX - this.mousePrevX; + const dragY = this.mouseY - this.mousePrevY; + this.mousePrevX = this.mouseX; + this.mousePrevY = this.mouseY; + if (!this.running) { return; } - const screenMoveX = window.screenX - this.screenX; - const screenMoveY = window.screenY - this.screenY; + if (this.emit) { + this.emitParticles(); + } - this.screenX = window.screenX; - this.screenY = window.screenY; + if (this.drain) { + this.drainParticles(); + } + + const dt = this.material.dt; + + const gravX = this.material.gravX * dt; + const gravY = this.material.gravY * dt; + + let attractRepel = this.attract ? 0.3 : 0; + attractRepel -= this.repel ? 0.3 : 0; + const arNonZero = attractRepel !== 0; for (let p of this.particles) { // apply gravity - p.velX += this.gravX * dt; - p.velY += this.gravY * dt; + p.velX += gravX; + p.velY += gravY; + + if (arNonZero) { + let dx = p.posX - this.mouseX; + let dy = p.posY - this.mouseY; + const distSq = dx * dx + dy * dy; + + if (distSq < 100000 && distSq > 0.1) { + const dist = Math.sqrt(distSq); + const invDist = 1 / dist; + + dx *= invDist; + dy *= invDist; + + p.velX -= attractRepel * dx; + p.velY -= attractRepel * dy; + } + } + + if (this.drag) { + let dx = p.posX - this.mouseX; + let dy = p.posY - this.mouseY; + const distSq = dx * dx + dy * dy; + + if (distSq < 10000 && distSq > 0.1) { + const dist = Math.sqrt(distSq); + const invDist = 1 / dist; + + p.velX = dragX; + p.velY = dragY; + } + } p.posX -= screenMoveX; p.posY -= screenMoveY; @@ -108,6 +219,12 @@ class Simulator { this.applyViscosity(dt); + const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMinX = 5; + const boundaryMaxX = this.width - 5; + const boundaryMinY = 5; + const boundaryMaxY = this.height - 5; + for (let p of this.particles) { // save previous position p.prevX = p.posX; @@ -116,6 +233,19 @@ class Simulator { // advance to predicted position p.posX += p.velX * dt; p.posY += p.velY * dt; + + // Could do boundary both before and after density relaxation + // if (p.posX < boundaryMinX) { + // p.posX += boundaryMul * (boundaryMinX - p.posX); + // } else if (p.posX > boundaryMaxX) { + // p.posX += boundaryMul * (boundaryMaxX - p.posX); + // } + + // if (p.posY < boundaryMinY) { + // p.posY += boundaryMul * (boundaryMinY - p.posY); + // } else if (p.posY > boundaryMaxY) { + // p.posY += boundaryMul * (boundaryMaxY - p.posY); + // } } this.populateHashGrid(); @@ -125,10 +255,12 @@ class Simulator { this.doubleDensityRelaxation(dt); this.resolveCollisions(dt); + const dtInv = 1 / dt; + for (let p of this.particles) { // use previous position to calculate new velocity - p.velX = (p.posX - p.prevX) / dt; - p.velY = (p.posY - p.prevY) / dt; + p.velX = (p.posX - p.prevX) * dtInv; + p.velY = (p.posY - p.prevY) * dtInv; } } @@ -139,11 +271,11 @@ class Simulator { const kernelRadiusInv = 1.0 / kernelRadius; const restDensity = this.material.restDensity; - const stiffness = this.material.stiffness; - const nearStiffness = this.material.nearStiffness; + const stiffness = this.material.stiffness * dt * dt; + const nearStiffness = this.material.nearStiffness * dt * dt; // Neighbor cache - const neighborIndices = []; + const neighbors = []; const neighborUnitX = []; const neighborUnitY = []; const neighborCloseness = []; @@ -236,7 +368,7 @@ class Simulator { density += closeness * closeness; nearDensity += closeness * closenessSq; - neighborIndices[numNeighbors] = neighborIdx; + neighbors[numNeighbors] = this.particles[neighborIdx]; neighborUnitX[numNeighbors] = diffX / r; neighborUnitY[numNeighbors] = diffY / r; neighborCloseness[numNeighbors] = closeness; @@ -249,49 +381,51 @@ class Simulator { } // Add wall density - if (p0.posX < softMinX) { - wallCloseness[0] = 1 - (softMinX - Math.max(boundaryMinX, p0.posX)) * kernelRadiusInv; - wallDirection[0] = 1; - } else if (p0.posX > softMaxX) { - wallCloseness[0] = 1 - (Math.min(boundaryMaxX, p0.posX) - softMaxX) * kernelRadiusInv; - wallDirection[0] = -1; - } else { - wallCloseness[0] = 0; - } + // if (p0.posX < softMinX) { + // wallCloseness[0] = 1 - (softMinX - Math.max(boundaryMinX, p0.posX)) * kernelRadiusInv; + // wallDirection[0] = 1; + // } else if (p0.posX > softMaxX) { + // wallCloseness[0] = 1 - (Math.min(boundaryMaxX, p0.posX) - softMaxX) * kernelRadiusInv; + // wallDirection[0] = -1; + // } else { + // wallCloseness[0] = 0; + // } - if (p0.posY < softMinY) { - wallCloseness[1] = 1 - (softMinY - Math.max(boundaryMinY, p0.posY)) * kernelRadiusInv; - wallDirection[1] = 1; - } else if (p0.posY > softMaxY) { - wallCloseness[1] = 1 - (Math.min(boundaryMaxY, p0.posY) - softMaxY) * kernelRadiusInv; - wallDirection[1] = -1; - } else { - wallCloseness[1] = 0; - } + // if (p0.posY < softMinY) { + // wallCloseness[1] = 1 - (softMinY - Math.max(boundaryMinY, p0.posY)) * kernelRadiusInv; + // wallDirection[1] = 1; + // } else if (p0.posY > softMaxY) { + // wallCloseness[1] = 1 - (Math.min(boundaryMaxY, p0.posY) - softMaxY) * kernelRadiusInv; + // wallDirection[1] = -1; + // } else { + // wallCloseness[1] = 0; + // } - if (wallCloseness[0] > 0) { - density += wallCloseness[0] * wallCloseness[0]; - nearDensity += wallCloseness[0] * wallCloseness[0] * wallCloseness[0]; - } + // const wallMul = 1; - if (wallCloseness[1] > 0) { - density += wallCloseness[1] * wallCloseness[1]; - nearDensity += wallCloseness[1] * wallCloseness[1] * wallCloseness[1]; - } + // if (wallCloseness[0] > 0) { + // density += wallMul * wallCloseness[0] * wallCloseness[0]; + // nearDensity += wallMul * wallCloseness[0] * wallCloseness[0] * wallCloseness[0]; + // } + + // if (wallCloseness[1] > 0) { + // density += wallMul * wallCloseness[1] * wallCloseness[1]; + // nearDensity += wallMul * wallCloseness[1] * wallCloseness[1] * wallCloseness[1]; + // } // Compute pressure and near-pressure let pressure = stiffness * (density - restDensity); let nearPressure = nearStiffness * nearDensity; - let immisciblePressure = stiffness * (density - 1); + let immisciblePressure = stiffness * (density - 0); - // Clamp pressure for stability - const pressureSum = pressure + nearPressure; + // Optional: Clamp pressure for stability + // const pressureSum = pressure + nearPressure; - if (pressureSum > 1) { - const pressureMul = 1 / pressureSum; - pressure *= pressureMul; - nearPressure *= pressureMul; - } + // if (pressureSum > 1) { + // const pressureMul = 1 / pressureSum; + // pressure *= pressureMul; + // nearPressure *= pressureMul; + // } // if (pressure > 1) { @@ -306,10 +440,10 @@ class Simulator { let dispY = 0; for (let j = 0; j < numNeighbors; j++) { - let p1 = this.particles[neighborIndices[j]]; + let p1 = neighbors[j]; const closeness = neighborCloseness[j]; - const D = dt * dt * (pressure * closeness + nearPressure * closeness * closeness) / 2; + const D = (pressure * closeness + nearPressure * closeness * closeness) / 2; const DX = D * neighborUnitX[j]; const DY = D * neighborUnitY[j]; @@ -323,17 +457,6 @@ class Simulator { // p0.posY -= DY; } - if (wallCloseness[0] > 0) { - const D = dt * dt * (0 * wallCloseness[0] + nearPressure * wallCloseness[0] * wallCloseness[0]) / 2; - p0.posX += D * wallDirection[0]; - } - - if (wallCloseness[1] > 0) { - const D = dt * dt * (0 * wallCloseness[1] + nearPressure * wallCloseness[1] * wallCloseness[1]) / 2; - p0.posY += D * wallDirection[1]; - } - - p0.posX += dispX; p0.posY += dispY; @@ -389,7 +512,7 @@ class Simulator { adjustSprings(dt) { } applyViscosity(dt) { } resolveCollisions(dt) { - const boundaryMul = 0.5 * dt; // 1 is no bounce, 2 is full bounce + const boundaryMul = 0.5 * dt * dt; const boundaryMinX = 5; const boundaryMaxX = this.width - 5; const boundaryMinY = 5; diff --git a/style.css b/style.css index 3d1edb6..6619cf4 100644 --- a/style.css +++ b/style.css @@ -2,6 +2,7 @@ body { margin: 0; padding: 0; overflow: hidden; + background-color: #111; } canvas { @@ -13,6 +14,8 @@ canvas { position: fixed; top: 0; left: 0; - background-color: rgba(200, 200, 200, 1.0); + background-color: rgba(0, 100, 200, 0.25); + color: white; padding: 0 1em; + font-size: large; }