Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 570 lines (434 sloc) 12.5 KB
<!-- Matthias Müller -->
<!DOCTYPE html>
<html>
<style>
body {setup
padding: 10px 50px;
font-family: verdana;
line-height: 1.5;
font-size: 15px;
}
.button {
background-color: #555555;
border: none;
color: white;
padding: 8px 8px;
border-radius: 5px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
.slider {
-webkit-appearance: none;
width: 80px;
height: 6px;
border-radius: 5px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: #202020;
cursor: pointer;
}
</style>
<title>2D Particle Fluid</title>
<body>
<h1>2D Particle Fluid</h1>
<span id = "ms">0.000</span> ms per frame
<br>
<br>
<button onclick="setup(10, 1000)" class="button">1000 x 10</button>
<button onclick="setup(10, 200)" class="button">200 x 10</button>
<button onclick="setup(40, 100)" class="button">100 x 50</button>
<button onclick="setup(100, 100)" class="button">100 x 100</button>
<span style = "margin: 0px 0px 0px 20px;">Visconsity</span> <td><input type = "range" min = "0" max = "10" value = "0" id = "viscositySlider" class = "slider"></td>
<br><br>
<canvas id="myCanvas" width="800" height="600" style="border:3px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var canvas = document.getElementById("myCanvas");
var c = canvas.getContext("2d");
var drawOrig = { x : canvas.width / 2, y : canvas.height - 20};
var drawScale = 200;
// global params
var gravity = -10;
var particleRadius = 0.01;
var unilateral = true;
var viscosity = 0.0;
var timeStep = 0.01;
var numIters = 1;
var numSubSteps = 10;
var maxParticles = 10000;
var numX = 10;
var numY = 1000;
var numParticles = numX * numY;
// boundary
var width = 1.0;
var height = 2.0;
var boundaries = [
{ left : -width * 0.5 - 0.1, right : -width * 0.5, bottom : -0.01, top : height },
{ left : width * 0.5, right : width * 0.5 + 0.1, bottom : -0.01, top : height },
];
var fluidOrig = { left : - 0.3, bottom : 1.8 };
// derived params
var particleDiameter = 2 * particleRadius;
var restDensity = 1.0 / (particleDiameter * particleDiameter);
var kernelRadius = 3.0 * particleRadius;
var h2 = kernelRadius * kernelRadius;
var kernelScale = 4.0 / (Math.PI * h2 * h2 * h2 * h2);
// 2d poly6 (SPH based shallow water simulation
var gridSpacing = kernelRadius * 1.5;
var invGridSpacing = 1.0 / gridSpacing;
var maxVel = 0.4 * particleRadius;
var particles = {
pos : new Float32Array(2 * maxParticles),
prev : new Float32Array(2 * maxParticles),
vel : new Float32Array(2 * maxParticles)
}
var i, j;
class Vector {
constructor(size) {
this.vals = new Int32Array(size);
this.maxSize = size;
this.size = 0;
}
clear() {
this.size = 0;
}
pushBack(val) {
if (this.size >= this.maxSize) {
this.maxSize *= 2;
var old = this.vals;
this.vals = new Int32Array(this.maxSize);
for (i = 0; i < old.length; i++)
this.vals[i] = old[i];
}
this.vals[this.size++] = val;
}
}
function setup(initNumX, initNumY)
{
if (initNumX * initNumY > maxParticles)
return;
numX = initNumX;
numY = initNumY;
numParticles = numX * numY;
var nr = 0;
for (j = 0; j < numY; j++) {
for (i = 0; i < numX; i++) {
particles.pos[nr] = fluidOrig.left + i * particleDiameter;
particles.pos[nr] += 0.00001 * (j % 2);
particles.pos[nr + 1] = fluidOrig.bottom + j * particleDiameter;
particles.vel[nr] = 0.0;
particles.vel[nr + 1] = 0.0;
nr += 2;
}
}
for (i = 0; i < hashSize; i++) {
hash.first[i] = -1;
hash.marks[i] = 0;
}
}
function solveBoundaries()
{
var minX = canvas.width * 0.5 / drawScale;
for (i = 0; i < numParticles; i++) {
var px = particles.pos[2 * i];
var py = particles.pos[2 * i + 1];
if (py < 0.0) { // ground
particles.pos[2 * i + 1] = 0.0;
// particles.pos[2 * i] = 0.0;
}
if (px < -minX)
particles.pos[2 * i] = -minX;
if (px > minX)
particles.pos[2 * i] = minX;
for (j = 0; j < boundaries.length; j++) {
b = boundaries[j];
if (px < b.left || px > b.right || py < b.bottom || py > b.top)
continue;
var dx, dy;
if (px < (b.left + b.right) * 0.5)
dx = b.left - px;
else
dx = b.right - px;
if (py < (b.bottom + b.top) * 0.5)
dy = b.bottom - py;
else
dy = b.top - py;
if (Math.abs(dx) < Math.abs(dy))
particles.pos[2 * i] += dx;
else
particles.pos[2 * i + 1] += dy;
}
}
}
// -----------------------------------------------------------------------------------
var hashSize = 370111;
var hash = {
size : hashSize,
first : new Int32Array(hashSize),
marks : new Int32Array(hashSize),
currentMark : 0,
next : new Int32Array(maxParticles),
orig : { left : -100.0, bottom : -1.0 }
}
var firstNeighbor = new Int32Array(maxParticles + 1);
var neighbors = new Vector(10 * maxParticles);
function findNeighbors()
{
// hash particles
hash.currentMark++;
for (i = 0; i < numParticles; i++) {
var px = particles.pos[2 * i];
var py = particles.pos[2 * i + 1];
var gx = Math.floor((px - hash.orig.left) * invGridSpacing);
var gy = Math.floor((py - hash.orig.bottom) * invGridSpacing);
var h = (Math.abs((gx * 92837111) ^ (gy * 689287499))) % hash.size;
if (hash.marks[h] != hash.currentMark) {
hash.marks[h] = hash.currentMark;
hash.first[h] = -1;
}
hash.next[i] = hash.first[h];
hash.first[h] = i;
}
// collect neighbors
neighbors.clear();
var h2 = gridSpacing * gridSpacing;
for (i = 0; i < numParticles; i++) {
firstNeighbor[i] = neighbors.size;
var px = particles.pos[2 * i];
var py = particles.pos[2 * i + 1];
var gx = Math.floor((px - hash.orig.left) * invGridSpacing);
var gy = Math.floor((py - hash.orig.bottom) * invGridSpacing);
var x, y;
for (x = gx - 1; x <= gx + 1; x++) {
for (y = gy - 1; y <= gy + 1; y++) {
var h = (Math.abs((x * 92837111) ^ (y * 689287499))) % hash.size;
if (hash.marks[h] != hash.currentMark)
continue;
var id = hash.first[h];
while (id >= 0)
{
var dx = particles.pos[2 * id] - px;
var dy = particles.pos[2 * id + 1] - py;
if (dx * dx + dy * dy < h2)
neighbors.pushBack(id);
id = hash.next[id];
}
}
}
}
firstNeighbor[numParticles] = neighbors.size;
}
// -----------------------------------------------------------------------------------
var grads = new Float32Array(1000);
var sand = false;
function solveFluid()
{
var h = kernelRadius;
var h2 = h * h;
var avgRho = 0.0;
for (i = 0; i < numParticles; i++) {
var px = particles.pos[2 * i];
var py = particles.pos[2 * i + 1];
var first = firstNeighbor[i];
var num = firstNeighbor[i + 1] - first;
var rho = 0.0;
var sumGrad2 = 0.0;
var gradix = 0.0;
var gradiy = 0.0;
for (j = 0; j < num; j++) {
var id = neighbors.vals[first + j];
var nx = particles.pos[2 * id] - px;
var ny = particles.pos[2 * id + 1] - py;
var r = Math.sqrt(nx * nx + ny * ny);
if (r > 0) {
nx /= r;
ny /= r;
}
if (sand) {
if (r < 2 * particleRadius) {
var d = 0.5 * (2 * particleRadius - r);
particles.pos[2 * i] -= nx * d;
particles.pos[2 * i + 1] -= ny * d;
particles.pos[2 * id] += nx * d;
particles.pos[2 * id + 1] += ny * d;
/*
var tx = ny;
var ty = -nx;
var vx0 = particles.pos[2 * id] - paricles.prev[2 * id];
var vy0 = particles.pos[2 * id + 1] - paricles.prev[2 * id + 1];
*/
}
continue;
}
if (r > h) {
grads[2 * j] = 0.0;
grads[2 * j + 1] = 0.0;
}
else {
var r2 = r * r;
var w = (h2 - r2);
rho += kernelScale * w * w * w;
var grad = (kernelScale * 3.0 * w * w * (-2.0 * r)) / restDensity;
grads[2 * j] = nx * grad;
grads[2 * j + 1] = ny * grad;
gradix -= nx * grad;
gradiy -= ny * grad;
sumGrad2 += grad * grad;
}
}
sumGrad2 += (gradix * gradix + gradiy * gradiy);
avgRho += rho;
var C = rho / restDensity - 1.0;
if (unilateral && C < 0.0)
continue;
var lambda = -C / (sumGrad2 + 0.0001);
for (j = 0; j < num; j++) {
var id = neighbors.vals[first + j];
if (id == i) {
particles.pos[2 * id] += lambda * gradix;
particles.pos[2 * id + 1] += lambda * gradiy;
}
else {
particles.pos[2 * id] += lambda * grads[2 * j];
particles.pos[2 * id + 1] += lambda * grads[2 * j + 1];
}
}
}
}
// -----------------------------------------------------------------------------------
function applyViscosity(pnr, dt)
{
var first = firstNeighbor[i];
var num = firstNeighbor[i + 1] - first;
if (num == 0)
return;
var avgVelX = 0.0;
var avgVelY = 0.0;
for (j = 0; j < num; j++) {
var id = neighbors.vals[first + j];
avgVelX += particles.vel[2 * id];
avgVelY += particles.vel[2 * id + 1];
}
avgVelX /= num;
avgVelY /= num;
var deltaX = avgVelX - particles.vel[2 * pnr];
var deltaY = avgVelY - particles.vel[2 * pnr + 1];
particles.vel[2 * pnr] += viscosity * deltaX;
particles.vel[2 * pnr + 1] += viscosity * deltaY;
}
// -----------------------------------------------------------------------------------
function simulate()
{
findNeighbors();
var dt = timeStep / numSubSteps;
var step;
for (step = 0; step < numSubSteps; step ++) {
// predict
for (i = 0; i < numParticles; i++) {
particles.vel[2 * i + 1] += gravity * dt;
particles.prev[2 * i] = particles.pos[2 * i];
particles.prev[2 * i + 1] = particles.pos[2 * i + 1];
particles.pos[2 * i] += particles.vel[2 * i] * dt;
particles.pos[2 * i + 1] += particles.vel[2 * i + 1] * dt;
}
// solve
solveBoundaries();
solveFluid();
// derive velocities
for (i = 0; i < numParticles; i++) {
var vx = particles.pos[2 * i] - particles.prev[2 * i];
var vy = particles.pos[2 * i + 1] - particles.prev[2 * i + 1];
// CFL
var v = Math.sqrt(vx * vx + vy * vy);
if (v > maxVel) {
vx *= maxVel / v;
vy *= maxVel / v;
particles.pos[2 * i] = particles.prev[2 * i] + vx;
particles.pos[2 * i + 1] = particles.prev[2 * i + 1] + vy;
}
particles.vel[2 * i] = vx / dt;
particles.vel[2 * i + 1] = vy / dt;
applyViscosity(i, dt);
}
}
}
// -----------------------------------------------------------------------------------
function draw()
{
c.clearRect(0, 0, canvas.width, canvas.height);
// particles
var nr = 0;
for (i = 0; i < numParticles; i++) {
if ((Math.floor(i / 1000) % 2) == 0)
c.fillStyle = "#0000FF";
else
c.fillStyle = "#FF0000";
var px = drawOrig.x + particles.pos[nr] * drawScale;
var py = drawOrig.y - particles.pos[nr + 1] * drawScale;
nr += 2;
c.beginPath();
c.arc(
px, py, particleRadius * drawScale, 0, Math.PI*2, true);
c.closePath();
c.fill();
}
// boundaries
c.fillStyle = "#eeeeee";
for (i = 0; i < boundaries.length; i++) {
var b = boundaries[i];
var left = drawOrig.x + b.left * drawScale;
var width = (b.right - b.left) * drawScale;
var top = drawOrig.y - b.top * drawScale;
var height = (b.top - b.bottom) * drawScale;
c.beginPath();
c.rect(left, top, width, height);
c.stroke();
}
c.beginPath();
c.moveTo(0, drawOrig.y); c.lineTo(canvas.width, drawOrig.y);
c.stroke();
}
// -----------------------------------------------------------------------------------
var timeFrames = 0;
var timeSum = 0;
function step()
{
var startTime = performance.now();
simulate();
var endTime = performance.now();
timeSum += endTime - startTime;
timeFrames++;
if (timeFrames > 10) {
timeSum /= timeFrames;
document.getElementById("ms").innerHTML = timeSum.toFixed(3);
timeFrames = 0;
timeSum = 0;
}
draw();
window.requestAnimationFrame(step);
}
document.getElementById("viscositySlider").oninput = function() {
viscosity = this.value / 15;
// document.getElementById("viscosity").innerHTML = viscosity.toString();
}
// main
setup(10, 1000);
step();
</script>
</body>
</html>
You can’t perform that action at this time.