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 @@
-
+
+
+
+
+
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/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"
+
+
+
+
+
+
+