Add more support
This commit is contained in:
779
references/fluidsim.html
Normal file
779
references/fluidsim.html
Normal file
@@ -0,0 +1,779 @@
|
|||||||
|
<!--
|
||||||
|
Copyright 2022 Matthias Müller - Ten Minute Physics,
|
||||||
|
www.youtube.com/c/TenMinutePhysics
|
||||||
|
www.matthiasMueller.info/tenMinutePhysics
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Euler Fluid</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: verdana;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
background-color: #606060;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 80px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #d3d3d3;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
-webkit-transition: .2s;
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button class="button" onclick="setupScene(1)">Wind Tunnel</button>
|
||||||
|
<button class="button" onclick="setupScene(3)">Hires Tunnel</button>
|
||||||
|
<button class="button" onclick="setupScene(0)">Tank</button>
|
||||||
|
<button class="button" onclick="setupScene(2)">Paint</button>
|
||||||
|
<input type = "checkbox" id = "streamButton" onclick = "scene.showStreamlines = !scene.showStreamlines">Streamlines
|
||||||
|
<input type = "checkbox" id = "velocityButton" onclick = "scene.showVelocities = !scene.showVelocities">Velocities
|
||||||
|
<input type = "checkbox" name = "field" id = "pressureButton" onclick = "scene.showPressure = !scene.showPressure;"> Pressure
|
||||||
|
<input type = "checkbox" name = "field" id = "smokeButton" onclick = "scene.showSmoke = !scene.showSmoke;" checked>Smoke
|
||||||
|
<input type = "checkbox" id = "overrelaxButton" onclick = "scene.overRelaxation = scene.overRelaxation == 1.0 ? 1.9 : 1.0" checked>Overrelax
|
||||||
|
<br>
|
||||||
|
<canvas id="myCanvas" style="border:2px solid"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var canvas = document.getElementById("myCanvas");
|
||||||
|
var c = canvas.getContext("2d");
|
||||||
|
canvas.width = window.innerWidth - 20;
|
||||||
|
canvas.height = window.innerHeight - 100;
|
||||||
|
|
||||||
|
canvas.focus();
|
||||||
|
|
||||||
|
var simHeight = 1.1;
|
||||||
|
var cScale = canvas.height / simHeight;
|
||||||
|
var simWidth = canvas.width / cScale;
|
||||||
|
|
||||||
|
var U_FIELD = 0;
|
||||||
|
var V_FIELD = 1;
|
||||||
|
var S_FIELD = 2;
|
||||||
|
|
||||||
|
var cnt = 0;
|
||||||
|
|
||||||
|
function cX(x) {
|
||||||
|
return x * cScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cY(y) {
|
||||||
|
return canvas.height - y * cScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------- start of simulator ------------------------------
|
||||||
|
|
||||||
|
class Fluid {
|
||||||
|
constructor(density, numX, numY, h) {
|
||||||
|
this.density = density;
|
||||||
|
this.numX = numX + 2;
|
||||||
|
this.numY = numY + 2;
|
||||||
|
this.numCells = this.numX * this.numY;
|
||||||
|
this.h = h;
|
||||||
|
this.u = new Float32Array(this.numCells);
|
||||||
|
this.v = new Float32Array(this.numCells);
|
||||||
|
this.newU = new Float32Array(this.numCells);
|
||||||
|
this.newV = new Float32Array(this.numCells);
|
||||||
|
this.p = new Float32Array(this.numCells);
|
||||||
|
this.s = new Float32Array(this.numCells);
|
||||||
|
this.m = new Float32Array(this.numCells);
|
||||||
|
this.newM = new Float32Array(this.numCells);
|
||||||
|
this.m.fill(1.0)
|
||||||
|
var num = numX * numY;
|
||||||
|
}
|
||||||
|
|
||||||
|
integrate(dt, gravity) {
|
||||||
|
var n = this.numY;
|
||||||
|
for (var i = 1; i < this.numX; i++) {
|
||||||
|
for (var j = 1; j < this.numY-1; j++) {
|
||||||
|
if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0)
|
||||||
|
this.v[i*n + j] += gravity * dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
solveIncompressibility(numIters, dt) {
|
||||||
|
|
||||||
|
var n = this.numY;
|
||||||
|
var cp = this.density * this.h / dt;
|
||||||
|
|
||||||
|
for (var iter = 0; iter < numIters; iter++) {
|
||||||
|
|
||||||
|
for (var i = 1; i < this.numX-1; i++) {
|
||||||
|
for (var j = 1; j < this.numY-1; j++) {
|
||||||
|
|
||||||
|
if (this.s[i*n + j] == 0.0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var s = this.s[i*n + j];
|
||||||
|
var sx0 = this.s[(i-1)*n + j];
|
||||||
|
var sx1 = this.s[(i+1)*n + j];
|
||||||
|
var sy0 = this.s[i*n + j-1];
|
||||||
|
var sy1 = this.s[i*n + j+1];
|
||||||
|
var s = sx0 + sx1 + sy0 + sy1;
|
||||||
|
if (s == 0.0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var div = this.u[(i+1)*n + j] - this.u[i*n + j] +
|
||||||
|
this.v[i*n + j+1] - this.v[i*n + j];
|
||||||
|
|
||||||
|
var p = -div / s;
|
||||||
|
p *= scene.overRelaxation;
|
||||||
|
this.p[i*n + j] += cp * p;
|
||||||
|
|
||||||
|
this.u[i*n + j] -= sx0 * p;
|
||||||
|
this.u[(i+1)*n + j] += sx1 * p;
|
||||||
|
this.v[i*n + j] -= sy0 * p;
|
||||||
|
this.v[i*n + j+1] += sy1 * p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extrapolate() {
|
||||||
|
var n = this.numY;
|
||||||
|
for (var i = 0; i < this.numX; i++) {
|
||||||
|
this.u[i*n + 0] = this.u[i*n + 1];
|
||||||
|
this.u[i*n + this.numY-1] = this.u[i*n + this.numY-2];
|
||||||
|
}
|
||||||
|
for (var j = 0; j < this.numY; j++) {
|
||||||
|
this.v[0*n + j] = this.v[1*n + j];
|
||||||
|
this.v[(this.numX-1)*n + j] = this.v[(this.numX-2)*n + j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleField(x, y, field) {
|
||||||
|
var n = this.numY;
|
||||||
|
var h = this.h;
|
||||||
|
var h1 = 1.0 / h;
|
||||||
|
var h2 = 0.5 * h;
|
||||||
|
|
||||||
|
x = Math.max(Math.min(x, this.numX * h), h);
|
||||||
|
y = Math.max(Math.min(y, this.numY * h), h);
|
||||||
|
|
||||||
|
var dx = 0.0;
|
||||||
|
var dy = 0.0;
|
||||||
|
|
||||||
|
var f;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case U_FIELD: f = this.u; dy = h2; break;
|
||||||
|
case V_FIELD: f = this.v; dx = h2; break;
|
||||||
|
case S_FIELD: f = this.m; dx = h2; dy = h2; break
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var x0 = Math.min(Math.floor((x-dx)*h1), this.numX-1);
|
||||||
|
var tx = ((x-dx) - x0*h) * h1;
|
||||||
|
var x1 = Math.min(x0 + 1, this.numX-1);
|
||||||
|
|
||||||
|
var y0 = Math.min(Math.floor((y-dy)*h1), this.numY-1);
|
||||||
|
var ty = ((y-dy) - y0*h) * h1;
|
||||||
|
var y1 = Math.min(y0 + 1, this.numY-1);
|
||||||
|
|
||||||
|
var sx = 1.0 - tx;
|
||||||
|
var sy = 1.0 - ty;
|
||||||
|
|
||||||
|
var val = sx*sy * f[x0*n + y0] +
|
||||||
|
tx*sy * f[x1*n + y0] +
|
||||||
|
tx*ty * f[x1*n + y1] +
|
||||||
|
sx*ty * f[x0*n + y1];
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
avgU(i, j) {
|
||||||
|
var n = this.numY;
|
||||||
|
var u = (this.u[i*n + j-1] + this.u[i*n + j] +
|
||||||
|
this.u[(i+1)*n + j-1] + this.u[(i+1)*n + j]) * 0.25;
|
||||||
|
return u;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
avgV(i, j) {
|
||||||
|
var n = this.numY;
|
||||||
|
var v = (this.v[(i-1)*n + j] + this.v[i*n + j] +
|
||||||
|
this.v[(i-1)*n + j+1] + this.v[i*n + j+1]) * 0.25;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
advectVel(dt) {
|
||||||
|
|
||||||
|
this.newU.set(this.u);
|
||||||
|
this.newV.set(this.v);
|
||||||
|
|
||||||
|
var n = this.numY;
|
||||||
|
var h = this.h;
|
||||||
|
var h2 = 0.5 * h;
|
||||||
|
|
||||||
|
for (var i = 1; i < this.numX; i++) {
|
||||||
|
for (var j = 1; j < this.numY; j++) {
|
||||||
|
|
||||||
|
cnt++;
|
||||||
|
|
||||||
|
// u component
|
||||||
|
if (this.s[i*n + j] != 0.0 && this.s[(i-1)*n + j] != 0.0 && j < this.numY - 1) {
|
||||||
|
var x = i*h;
|
||||||
|
var y = j*h + h2;
|
||||||
|
var u = this.u[i*n + j];
|
||||||
|
var v = this.avgV(i, j);
|
||||||
|
// var v = this.sampleField(x,y, V_FIELD);
|
||||||
|
x = x - dt*u;
|
||||||
|
y = y - dt*v;
|
||||||
|
u = this.sampleField(x,y, U_FIELD);
|
||||||
|
this.newU[i*n + j] = u;
|
||||||
|
}
|
||||||
|
// v component
|
||||||
|
if (this.s[i*n + j] != 0.0 && this.s[i*n + j-1] != 0.0 && i < this.numX - 1) {
|
||||||
|
var x = i*h + h2;
|
||||||
|
var y = j*h;
|
||||||
|
var u = this.avgU(i, j);
|
||||||
|
// var u = this.sampleField(x,y, U_FIELD);
|
||||||
|
var v = this.v[i*n + j];
|
||||||
|
x = x - dt*u;
|
||||||
|
y = y - dt*v;
|
||||||
|
v = this.sampleField(x,y, V_FIELD);
|
||||||
|
this.newV[i*n + j] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.u.set(this.newU);
|
||||||
|
this.v.set(this.newV);
|
||||||
|
}
|
||||||
|
|
||||||
|
advectSmoke(dt) {
|
||||||
|
|
||||||
|
this.newM.set(this.m);
|
||||||
|
|
||||||
|
var n = this.numY;
|
||||||
|
var h = this.h;
|
||||||
|
var h2 = 0.5 * h;
|
||||||
|
|
||||||
|
for (var i = 1; i < this.numX-1; i++) {
|
||||||
|
for (var j = 1; j < this.numY-1; j++) {
|
||||||
|
|
||||||
|
if (this.s[i*n + j] != 0.0) {
|
||||||
|
var u = (this.u[i*n + j] + this.u[(i+1)*n + j]) * 0.5;
|
||||||
|
var v = (this.v[i*n + j] + this.v[i*n + j+1]) * 0.5;
|
||||||
|
var x = i*h + h2 - dt*u;
|
||||||
|
var y = j*h + h2 - dt*v;
|
||||||
|
|
||||||
|
this.newM[i*n + j] = this.sampleField(x,y, S_FIELD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.m.set(this.newM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------- end of simulator ------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
simulate(dt, gravity, numIters) {
|
||||||
|
|
||||||
|
this.integrate(dt, gravity);
|
||||||
|
|
||||||
|
this.p.fill(0.0);
|
||||||
|
this.solveIncompressibility(numIters, dt);
|
||||||
|
|
||||||
|
this.extrapolate();
|
||||||
|
this.advectVel(dt);
|
||||||
|
this.advectSmoke(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scene =
|
||||||
|
{
|
||||||
|
gravity : -9.81,
|
||||||
|
dt : 1.0 / 120.0,
|
||||||
|
numIters : 100,
|
||||||
|
frameNr : 0,
|
||||||
|
overRelaxation : 1.9,
|
||||||
|
obstacleX : 0.0,
|
||||||
|
obstacleY : 0.0,
|
||||||
|
obstacleRadius: 0.15,
|
||||||
|
paused: false,
|
||||||
|
sceneNr: 0,
|
||||||
|
showObstacle: false,
|
||||||
|
showStreamlines: false,
|
||||||
|
showVelocities: false,
|
||||||
|
showPressure: false,
|
||||||
|
showSmoke: true,
|
||||||
|
fluid: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupScene(sceneNr = 0)
|
||||||
|
{
|
||||||
|
scene.sceneNr = sceneNr;
|
||||||
|
scene.obstacleRadius = 0.15;
|
||||||
|
scene.overRelaxation = 1.9;
|
||||||
|
|
||||||
|
scene.dt = 1.0 / 60.0;
|
||||||
|
scene.numIters = 40;
|
||||||
|
|
||||||
|
var res = 100;
|
||||||
|
|
||||||
|
if (sceneNr == 0)
|
||||||
|
res = 50;
|
||||||
|
else if (sceneNr == 3)
|
||||||
|
res = 200;
|
||||||
|
|
||||||
|
var domainHeight = 1.0;
|
||||||
|
var domainWidth = domainHeight / simHeight * simWidth;
|
||||||
|
var h = domainHeight / res;
|
||||||
|
|
||||||
|
var numX = Math.floor(domainWidth / h);
|
||||||
|
var numY = Math.floor(domainHeight / h);
|
||||||
|
|
||||||
|
var density = 1000.0;
|
||||||
|
|
||||||
|
f = scene.fluid = new Fluid(density, numX, numY, h);
|
||||||
|
|
||||||
|
var n = f.numY;
|
||||||
|
|
||||||
|
if (sceneNr == 0) { // tank
|
||||||
|
|
||||||
|
for (var i = 0; i < f.numX; i++) {
|
||||||
|
for (var j = 0; j < f.numY; j++) {
|
||||||
|
var s = 1.0; // fluid
|
||||||
|
if (i == 0 || i == f.numX-1 || j == 0)
|
||||||
|
s = 0.0; // solid
|
||||||
|
f.s[i*n + j] = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scene.gravity = -9.81;
|
||||||
|
scene.showPressure = true;
|
||||||
|
scene.showSmoke = false;
|
||||||
|
scene.showStreamlines = false;
|
||||||
|
scene.showVelocities = false;
|
||||||
|
}
|
||||||
|
else if (sceneNr == 1 || sceneNr == 3) { // vortex shedding
|
||||||
|
|
||||||
|
var inVel = 2.0;
|
||||||
|
for (var i = 0; i < f.numX; i++) {
|
||||||
|
for (var j = 0; j < f.numY; j++) {
|
||||||
|
var s = 1.0; // fluid
|
||||||
|
if (i == 0 || j == 0 || j == f.numY-1)
|
||||||
|
s = 0.0; // solid
|
||||||
|
f.s[i*n + j] = s
|
||||||
|
|
||||||
|
if (i == 1) {
|
||||||
|
f.u[i*n + j] = inVel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipeH = 0.1 * f.numY;
|
||||||
|
var minJ = Math.floor(0.5 * f.numY - 0.5*pipeH);
|
||||||
|
var maxJ = Math.floor(0.5 * f.numY + 0.5*pipeH);
|
||||||
|
|
||||||
|
for (var j = minJ; j < maxJ; j++)
|
||||||
|
f.m[j] = 0.0;
|
||||||
|
|
||||||
|
setObstacle(0.4, 0.5, true)
|
||||||
|
|
||||||
|
scene.gravity = 0.0;
|
||||||
|
scene.showPressure = false;
|
||||||
|
scene.showSmoke = true;
|
||||||
|
scene.showStreamlines = false;
|
||||||
|
scene.showVelocities = false;
|
||||||
|
|
||||||
|
if (sceneNr == 3) {
|
||||||
|
scene.dt = 1.0 / 120.0;
|
||||||
|
scene.numIters = 100;
|
||||||
|
scene.showPressure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (sceneNr == 2) { // paint
|
||||||
|
|
||||||
|
scene.gravity = 0.0;
|
||||||
|
scene.overRelaxation = 1.0;
|
||||||
|
scene.showPressure = false;
|
||||||
|
scene.showSmoke = true;
|
||||||
|
scene.showStreamlines = false;
|
||||||
|
scene.showVelocities = false;
|
||||||
|
scene.obstacleRadius = 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("streamButton").checked = scene.showStreamlines;
|
||||||
|
document.getElementById("velocityButton").checked = scene.showVelocities;
|
||||||
|
document.getElementById("pressureButton").checked = scene.showPressure;
|
||||||
|
document.getElementById("smokeButton").checked = scene.showSmoke;
|
||||||
|
document.getElementById("overrelaxButton").checked = scene.overRelaxation > 1.0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// draw -------------------------------------------------------
|
||||||
|
|
||||||
|
function setColor(r,g,b) {
|
||||||
|
c.fillStyle = `rgb(
|
||||||
|
${Math.floor(255*r)},
|
||||||
|
${Math.floor(255*g)},
|
||||||
|
${Math.floor(255*b)})`
|
||||||
|
c.strokeStyle = `rgb(
|
||||||
|
${Math.floor(255*r)},
|
||||||
|
${Math.floor(255*g)},
|
||||||
|
${Math.floor(255*b)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSciColor(val, minVal, maxVal) {
|
||||||
|
val = Math.min(Math.max(val, minVal), maxVal- 0.0001);
|
||||||
|
var d = maxVal - minVal;
|
||||||
|
val = d == 0.0 ? 0.5 : (val - minVal) / d;
|
||||||
|
var m = 0.25;
|
||||||
|
var num = Math.floor(val / m);
|
||||||
|
var s = (val - num * m) / m;
|
||||||
|
var r, g, b;
|
||||||
|
|
||||||
|
switch (num) {
|
||||||
|
case 0 : r = 0.0; g = s; b = 1.0; break;
|
||||||
|
case 1 : r = 0.0; g = 1.0; b = 1.0-s; break;
|
||||||
|
case 2 : r = s; g = 1.0; b = 0.0; break;
|
||||||
|
case 3 : r = 1.0; g = 1.0 - s; b = 0.0; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return[255*r,255*g,255*b, 255]
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw()
|
||||||
|
{
|
||||||
|
c.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
c.fillStyle = "#FF0000";
|
||||||
|
f = scene.fluid;
|
||||||
|
n = f.numY;
|
||||||
|
|
||||||
|
var cellScale = 1.1;
|
||||||
|
|
||||||
|
var h = f.h;
|
||||||
|
|
||||||
|
minP = f.p[0];
|
||||||
|
maxP = f.p[0];
|
||||||
|
|
||||||
|
for (var i = 0; i < f.numCells; i++) {
|
||||||
|
minP = Math.min(minP, f.p[i]);
|
||||||
|
maxP = Math.max(maxP, f.p[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
id = c.getImageData(0,0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
var color = [255, 255, 255, 255]
|
||||||
|
|
||||||
|
for (var i = 0; i < f.numX; i++) {
|
||||||
|
for (var j = 0; j < f.numY; j++) {
|
||||||
|
|
||||||
|
if (scene.showPressure) {
|
||||||
|
var p = f.p[i*n + j];
|
||||||
|
var s = f.m[i*n + j];
|
||||||
|
color = getSciColor(p, minP, maxP);
|
||||||
|
if (scene.showSmoke) {
|
||||||
|
color[0] = Math.max(0.0, color[0] - 255*s);
|
||||||
|
color[1] = Math.max(0.0, color[1] - 255*s);
|
||||||
|
color[2] = Math.max(0.0, color[2] - 255*s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (scene.showSmoke) {
|
||||||
|
var s = f.m[i*n + j];
|
||||||
|
color[0] = 255*s;
|
||||||
|
color[1] = 255*s;
|
||||||
|
color[2] = 255*s;
|
||||||
|
if (scene.sceneNr == 2)
|
||||||
|
color = getSciColor(s, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
else if (f.s[i*n + j] == 0.0) {
|
||||||
|
color[0] = 0;
|
||||||
|
color[1] = 0;
|
||||||
|
color[2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = Math.floor(cX(i * h));
|
||||||
|
var y = Math.floor(cY((j+1) * h));
|
||||||
|
var cx = Math.floor(cScale * cellScale * h) + 1;
|
||||||
|
var cy = Math.floor(cScale * cellScale * h) + 1;
|
||||||
|
|
||||||
|
r = color[0];
|
||||||
|
g = color[1];
|
||||||
|
b = color[2];
|
||||||
|
|
||||||
|
for (var yi = y; yi < y + cy; yi++) {
|
||||||
|
var p = 4 * (yi * canvas.width + x)
|
||||||
|
|
||||||
|
for (var xi = 0; xi < cx; xi++) {
|
||||||
|
id.data[p++] = r;
|
||||||
|
id.data[p++] = g;
|
||||||
|
id.data[p++] = b;
|
||||||
|
id.data[p++] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.putImageData(id, 0, 0);
|
||||||
|
|
||||||
|
if (scene.showVelocities) {
|
||||||
|
|
||||||
|
c.strokeStyle = "#000000";
|
||||||
|
scale = 0.02;
|
||||||
|
|
||||||
|
for (var i = 0; i < f.numX; i++) {
|
||||||
|
for (var j = 0; j < f.numY; j++) {
|
||||||
|
|
||||||
|
var u = f.u[i*n + j];
|
||||||
|
var v = f.v[i*n + j];
|
||||||
|
|
||||||
|
c.beginPath();
|
||||||
|
|
||||||
|
x0 = cX(i * h);
|
||||||
|
x1 = cX(i * h + u * scale);
|
||||||
|
y = cY((j + 0.5) * h );
|
||||||
|
|
||||||
|
c.moveTo(x0, y);
|
||||||
|
c.lineTo(x1, y);
|
||||||
|
c.stroke();
|
||||||
|
|
||||||
|
x = cX((i + 0.5) * h);
|
||||||
|
y0 = cY(j * h );
|
||||||
|
y1 = cY(j * h + v * scale)
|
||||||
|
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(x, y0);
|
||||||
|
c.lineTo(x, y1);
|
||||||
|
c.stroke();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.showStreamlines) {
|
||||||
|
|
||||||
|
var segLen = f.h * 0.2;
|
||||||
|
var numSegs = 15;
|
||||||
|
|
||||||
|
c.strokeStyle = "#000000";
|
||||||
|
|
||||||
|
for (var i = 1; i < f.numX - 1; i += 5) {
|
||||||
|
for (var j = 1; j < f.numY - 1; j += 5) {
|
||||||
|
|
||||||
|
var x = (i + 0.5) * f.h;
|
||||||
|
var y = (j + 0.5) * f.h;
|
||||||
|
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(cX(x), cY(y));
|
||||||
|
|
||||||
|
for (var n = 0; n < numSegs; n++) {
|
||||||
|
var u = f.sampleField(x, y, U_FIELD);
|
||||||
|
var v = f.sampleField(x, y, V_FIELD);
|
||||||
|
l = Math.sqrt(u*u + v*v);
|
||||||
|
// x += u/l * segLen;
|
||||||
|
// y += v/l * segLen;
|
||||||
|
x += u * 0.01;
|
||||||
|
y += v * 0.01;
|
||||||
|
if (x > f.numX * f.h)
|
||||||
|
break;
|
||||||
|
|
||||||
|
c.lineTo(cX(x), cY(y));
|
||||||
|
}
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.showObstacle) {
|
||||||
|
|
||||||
|
c.strokeW
|
||||||
|
r = scene.obstacleRadius + f.h;
|
||||||
|
if (scene.showPressure)
|
||||||
|
c.fillStyle = "#000000";
|
||||||
|
else
|
||||||
|
c.fillStyle = "#DDDDDD";
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(
|
||||||
|
cX(scene.obstacleX), cY(scene.obstacleY), cScale * r, 0.0, 2.0 * Math.PI);
|
||||||
|
c.closePath();
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
c.lineWidth = 3.0;
|
||||||
|
c.strokeStyle = "#000000";
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(
|
||||||
|
cX(scene.obstacleX), cY(scene.obstacleY), cScale * r, 0.0, 2.0 * Math.PI);
|
||||||
|
c.closePath();
|
||||||
|
c.stroke();
|
||||||
|
c.lineWidth = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.showPressure) {
|
||||||
|
var s = "pressure: " + minP.toFixed(0) + " - " + maxP.toFixed(0) + " N/m";
|
||||||
|
c.fillStyle ="#000000";
|
||||||
|
c.font = "16px Arial";
|
||||||
|
c.fillText(s, 10, 35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setObstacle(x, y, reset) {
|
||||||
|
|
||||||
|
var vx = 0.0;
|
||||||
|
var vy = 0.0;
|
||||||
|
|
||||||
|
if (!reset) {
|
||||||
|
vx = (x - scene.obstacleX) / scene.dt;
|
||||||
|
vy = (y - scene.obstacleY) / scene.dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.obstacleX = x;
|
||||||
|
scene.obstacleY = y;
|
||||||
|
var r = scene.obstacleRadius;
|
||||||
|
var f = scene.fluid;
|
||||||
|
var n = f.numY;
|
||||||
|
var cd = Math.sqrt(2) * f.h;
|
||||||
|
|
||||||
|
for (var i = 1; i < f.numX-2; i++) {
|
||||||
|
for (var j = 1; j < f.numY-2; j++) {
|
||||||
|
|
||||||
|
f.s[i*n + j] = 1.0;
|
||||||
|
|
||||||
|
dx = (i + 0.5) * f.h - x;
|
||||||
|
dy = (j + 0.5) * f.h - y;
|
||||||
|
|
||||||
|
if (dx * dx + dy * dy < r * r) {
|
||||||
|
f.s[i*n + j] = 0.0;
|
||||||
|
if (scene.sceneNr == 2)
|
||||||
|
f.m[i*n + j] = 0.5 + 0.5 * Math.sin(0.1 * scene.frameNr)
|
||||||
|
else
|
||||||
|
f.m[i*n + j] = 1.0;
|
||||||
|
f.u[i*n + j] = vx;
|
||||||
|
f.u[(i+1)*n + j] = vx;
|
||||||
|
f.v[i*n + j] = vy;
|
||||||
|
f.v[i*n + j+1] = vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.showObstacle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interaction -------------------------------------------------------
|
||||||
|
|
||||||
|
var mouseDown = false;
|
||||||
|
|
||||||
|
function startDrag(x, y) {
|
||||||
|
let bounds = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
let mx = x - bounds.left - canvas.clientLeft;
|
||||||
|
let my = y - bounds.top - canvas.clientTop;
|
||||||
|
mouseDown = true;
|
||||||
|
|
||||||
|
x = mx / cScale;
|
||||||
|
y = (canvas.height - my) / cScale;
|
||||||
|
|
||||||
|
setObstacle(x,y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(x, y) {
|
||||||
|
if (mouseDown) {
|
||||||
|
let bounds = canvas.getBoundingClientRect();
|
||||||
|
let mx = x - bounds.left - canvas.clientLeft;
|
||||||
|
let my = y - bounds.top - canvas.clientTop;
|
||||||
|
x = mx / cScale;
|
||||||
|
y = (canvas.height - my) / cScale;
|
||||||
|
setObstacle(x,y, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDrag() {
|
||||||
|
mouseDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', event => {
|
||||||
|
startDrag(event.x, event.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseup', event => {
|
||||||
|
endDrag();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mousemove', event => {
|
||||||
|
drag(event.x, event.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchstart', event => {
|
||||||
|
startDrag(event.touches[0].clientX, event.touches[0].clientY)
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchend', event => {
|
||||||
|
endDrag()
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchmove', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
drag(event.touches[0].clientX, event.touches[0].clientY)
|
||||||
|
}, { passive: false});
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
switch(event.key) {
|
||||||
|
case 'p': scene.paused = !scene.paused; break;
|
||||||
|
case 'm': scene.paused = false; simulate(); scene.paused = true; break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleStart()
|
||||||
|
{
|
||||||
|
var button = document.getElementById('startButton');
|
||||||
|
if (scene.paused)
|
||||||
|
button.innerHTML = "Stop";
|
||||||
|
else
|
||||||
|
button.innerHTML = "Start";
|
||||||
|
scene.paused = !scene.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
// main -------------------------------------------------------
|
||||||
|
|
||||||
|
function simulate()
|
||||||
|
{
|
||||||
|
if (!scene.paused)
|
||||||
|
scene.fluid.simulate(scene.dt, scene.gravity, scene.numIters)
|
||||||
|
scene.frameNr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
simulate();
|
||||||
|
draw();
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupScene(1);
|
||||||
|
update();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub mod ten_minute_physics;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Domain {
|
pub struct Domain {
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
|
|||||||
108
src/main.rs
108
src/main.rs
@@ -4,68 +4,58 @@ use ratatui::{text::Text, Frame, widgets::canvas::Canvas, widgets::canvas::Point
|
|||||||
use fluidsim::{Domain, simulate};
|
use fluidsim::{Domain, simulate};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let width = 100;
|
let width = 140;
|
||||||
let height = 60;
|
let height = 60;
|
||||||
|
|
||||||
let mut domain = Domain {
|
let mut t_domain = fluidsim::ten_minute_physics::Domain::new(1000.0, width, height);
|
||||||
width: width + 2,
|
t_domain.set_s_with(|x, y| {
|
||||||
height: height + 2,
|
1.0
|
||||||
cells: core::iter::repeat_n((0.0, 0.0), (width+2)*(height+2)).collect(),
|
});
|
||||||
enabled: core::iter::repeat_n(1.0, (width+2)*(height+2)).collect(),
|
t_domain.set_u_with(|x, y| {
|
||||||
};
|
if x == 0 {
|
||||||
|
2.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let boundary = 10;
|
t_domain.set_s_with(|x, y| {
|
||||||
for y in boundary..domain.height-boundary {
|
if x > 40 && x < 50 && y > 20 && y < 30 {
|
||||||
let cell = domain.cells.get_mut(y*domain.width + 1).unwrap();
|
return 0.0;
|
||||||
cell.0 = 1.0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for y in 0..domain.height {
|
|
||||||
*domain.enabled.get_mut(y*domain.width).unwrap() = 0.0;
|
|
||||||
*domain.enabled.get_mut(y*domain.width + domain.width-1).unwrap() = 0.0;
|
|
||||||
}
|
|
||||||
for x in 0..domain.width {
|
|
||||||
*domain.enabled.get_mut(0*domain.width + x).unwrap() = 0.0;
|
|
||||||
*domain.enabled.get_mut((domain.height-1)*domain.width + x).unwrap() = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (domain_tx, domain_rx) = std::sync::mpsc::channel();
|
1.0
|
||||||
|
});
|
||||||
|
|
||||||
|
let domain_mtx_1 = std::sync::Arc::new(std::sync::Mutex::new(t_domain.clone()));
|
||||||
|
let domain_mtx = domain_mtx_1.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut start = domain;
|
let mut start = t_domain;
|
||||||
|
|
||||||
domain_tx.send(Domain {
|
|
||||||
width: start.width,
|
|
||||||
height: start.height,
|
|
||||||
cells: start.cells.clone(),
|
|
||||||
enabled: start.enabled.clone(),
|
|
||||||
}).unwrap();
|
|
||||||
|
|
||||||
for i in 0.. {
|
for i in 0.. {
|
||||||
// println!("Iteration {}", i);
|
// println!("Iteration {}", i);
|
||||||
|
|
||||||
simulate(&mut start);
|
start.reset_pressure();
|
||||||
|
|
||||||
if i % 100 == 0 {
|
start.solve_incompressibility(100, 0.01);
|
||||||
domain_tx.send(Domain {
|
start.extrapolate();
|
||||||
width: start.width,
|
start.advect_vel(0.01);
|
||||||
height: start.height,
|
start.advect_smoke(0.01);
|
||||||
cells: start.cells.clone(),
|
|
||||||
enabled: start.enabled.clone(),
|
if let Ok(mut mtx) = domain_mtx.try_lock() {
|
||||||
}).unwrap();
|
*mtx = start.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut domain = domain_rx.recv().unwrap();
|
let domain_mtx = domain_mtx_1;
|
||||||
|
let mut domain = domain_mtx.lock().unwrap().clone();
|
||||||
|
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = ratatui::init();
|
||||||
loop {
|
loop {
|
||||||
domain = match domain_rx.try_recv() {
|
domain = domain_mtx.lock().unwrap().clone();
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => domain,
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
draw(frame, &domain)
|
draw(frame, &domain)
|
||||||
@@ -80,33 +70,23 @@ fn main() {
|
|||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(frame: &mut Frame, domain: &Domain) {
|
fn draw(frame: &mut Frame, domain: &fluidsim::ten_minute_physics::Domain) {
|
||||||
let canvas = Canvas::default().x_bounds([0.0, domain.width as f64 - 2.0]).y_bounds([0.0, domain.height as f64 - 2.0]).paint(|ctx| {
|
let (width, height) = domain.inner_dims();
|
||||||
for y in 1..domain.height-1 {
|
|
||||||
for x in 1..domain.width-1 {
|
|
||||||
let own_cell_idx = y * domain.width + x;
|
|
||||||
let top_cell_idx = (y-1) * domain.width + x;
|
|
||||||
let right_cell_idx = y * domain.width + x + 1;
|
|
||||||
|
|
||||||
let own_cell = domain.cells.get(own_cell_idx).unwrap();
|
let canvas = Canvas::default().x_bounds([0.0, width as f64]).y_bounds([0.0, height as f64]).paint(|ctx| {
|
||||||
let top_cell = domain.cells.get(top_cell_idx).unwrap();
|
let (min_v, max_v) = domain.smoke_iter().fold((f32::MAX, f32::MIN), |(mip, map), v| {
|
||||||
let right_cell = domain.cells.get(right_cell_idx).unwrap();
|
(mip.min(v.2), map.max(v.2))
|
||||||
|
});
|
||||||
|
|
||||||
let d = right_cell.0 - own_cell.0 + top_cell.1 - own_cell.1;
|
|
||||||
|
|
||||||
// let text = format!("{:.4}", d);
|
for (x, y, d) in domain.smoke_iter() {
|
||||||
// ctx.print(-0.5 + x as f64, -0.5 + y as f64, text);
|
let inter = ((d - min_v) / (max_v - min_v)).sqrt();
|
||||||
let color = match d.total_cmp(&0.0) {
|
let color = ratatui::style::Color::Rgb((255.0*inter) as u8, (255.0*inter) as u8, 0);
|
||||||
core::cmp::Ordering::Less if d.abs() > 0.0001 => ratatui::style::Color::Rgb(255, 0, 0),
|
|
||||||
core::cmp::Ordering::Greater if d.abs() > 0.0001 => ratatui::style::Color::Rgb(0, 255, 0),
|
|
||||||
_ => ratatui::style::Color::White,
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.draw(&Points {
|
ctx.draw(&Points {
|
||||||
coords: &[(-0.5 + x as f64, -0.5 + y as f64)],
|
coords: &[(-0.5 + x as f64, -0.5 + y as f64)],
|
||||||
color: color,
|
color: color,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
frame.render_widget(canvas, frame.area());
|
frame.render_widget(canvas, frame.area());
|
||||||
|
|||||||
276
src/ten_minute_physics.rs
Normal file
276
src/ten_minute_physics.rs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Domain {
|
||||||
|
density: f32,
|
||||||
|
/// The inner width (without the extra padding cells at the edge)
|
||||||
|
width: usize,
|
||||||
|
/// The inner height (without the extra padding cells at the edge)
|
||||||
|
height: usize,
|
||||||
|
u: Vec<f32>,
|
||||||
|
v: Vec<f32>,
|
||||||
|
s: Vec<f32>,
|
||||||
|
p: Vec<f32>,
|
||||||
|
/// Smoke field
|
||||||
|
m: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Field {
|
||||||
|
U,
|
||||||
|
V,
|
||||||
|
Smoke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Domain {
|
||||||
|
pub fn new(density:f32, width: usize, height: usize) -> Self {
|
||||||
|
let num_cells = (width + 2) * (height + 2);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
density,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
u: vec![0.0; num_cells],
|
||||||
|
v: vec![0.0; num_cells],
|
||||||
|
s: vec![0.0; num_cells],
|
||||||
|
p: vec![0.0; num_cells],
|
||||||
|
m: vec![1.0; num_cells],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_pressure(&mut self) {
|
||||||
|
self.p.fill(0.0);
|
||||||
|
}
|
||||||
|
pub fn set_s_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
|
||||||
|
for i in 1..self.width+1 {
|
||||||
|
for j in 1..self.height+1 {
|
||||||
|
self.s[i*(self.height+2)+j] = func(i-1, j-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_u_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
|
||||||
|
for i in 1..self.width+1 {
|
||||||
|
for j in 1..self.height+1 {
|
||||||
|
self.u[i*(self.height+2)+j] = func(i-1, j-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_v_with<F>(&mut self, func: F) where F: Fn(usize, usize) -> f32 {
|
||||||
|
for i in 1..self.width+1 {
|
||||||
|
for j in 1..self.height+1 {
|
||||||
|
self.v[i*(self.height+2)+j] = func(i-1, j-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner_dims(&self) -> (usize, usize) {
|
||||||
|
(self.width, self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn div_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||||
|
let n = self.height + 2;
|
||||||
|
|
||||||
|
(1..self.width+1)
|
||||||
|
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
|
||||||
|
.map(move |(x, y)| {
|
||||||
|
let div = self.u[(x+1)*n + y] - self.u[x*n + y] + self.v[x*n + y + 1] - self.v[x*n+y];
|
||||||
|
|
||||||
|
(x - 1, y - 1, div)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vel_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||||
|
let n = self.height + 2;
|
||||||
|
|
||||||
|
(1..self.width+1)
|
||||||
|
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
|
||||||
|
.map(move |(x, y)| {
|
||||||
|
let u = self.u[x*n+y];
|
||||||
|
let v = self.v[x*n+y];
|
||||||
|
|
||||||
|
(x - 1, y - 1, (u*u + v*v).sqrt())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pressure_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||||
|
let n = self.height + 2;
|
||||||
|
|
||||||
|
(1..self.width+1)
|
||||||
|
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
|
||||||
|
.map(move |(i, j)| {
|
||||||
|
(i - 1, j - 1, self.p[i*n+j])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn smoke_iter(&self) -> impl Iterator<Item = (usize, usize, f32)> {
|
||||||
|
let n = self.height + 2;
|
||||||
|
|
||||||
|
(1..self.width+1)
|
||||||
|
.flat_map(|x| core::iter::repeat(x).zip(1..self.height+1))
|
||||||
|
.map(move |(i, j)| {
|
||||||
|
(i - 1, j - 1, self.m[i*n+j])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn solve_incompressibility(&mut self, num_iters: usize, dT: f32) {
|
||||||
|
let n = self.height + 2;
|
||||||
|
let cp = self.density * 1.0 / dT;
|
||||||
|
|
||||||
|
for _ in 0..num_iters {
|
||||||
|
for i in 1..self.width+1 {
|
||||||
|
for j in 1..self.height+1 {
|
||||||
|
if self.s[i*n + j] == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = self.s[i*n + j];
|
||||||
|
let sx0 = self.s[(i-1)*n + j];
|
||||||
|
let sx1 = self.s[(i+1)*n + j];
|
||||||
|
let sy0 = self.s[i*n + j - 1];
|
||||||
|
let sy1 = self.s[i*n + j + 1];
|
||||||
|
|
||||||
|
let s = sx0 + sx1 + sy0 + sy1;
|
||||||
|
if s == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let div = self.u[(i+1)*n + j] - self.u[i*n + j] + self.v[i*n + j + 1] - self.v[i*n+j];
|
||||||
|
|
||||||
|
let p = -div / s;
|
||||||
|
let p = p * 1.9;
|
||||||
|
self.p[i*n+j] += cp * p;
|
||||||
|
|
||||||
|
self.u[i*n + j] -= sx0 * p;
|
||||||
|
self.u[(i+1)*n + j] += sx1 * p;
|
||||||
|
self.v[i*n+j] -= sy0 * p;
|
||||||
|
self.v[i*n + j + 1] += sy1 * p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extrapolate(&mut self) {
|
||||||
|
let n = self.height + 2;
|
||||||
|
|
||||||
|
for i in 0..self.width+2 {
|
||||||
|
self.u[i*n + 0] = self.u[i*n + 1];
|
||||||
|
self.u[i*n + self.height+1] = self.u[i*n + self.height];
|
||||||
|
}
|
||||||
|
for j in 0..self.height+2 {
|
||||||
|
self.v[0*n + j] = self.u[1*n + j];
|
||||||
|
self.v[(self.width+1)*n + j] = self.u[self.width*n + j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_field(&self, x: f32, y: f32, field: Field) -> f32 {
|
||||||
|
let n = self.height + 2;
|
||||||
|
let h = 1.0;
|
||||||
|
|
||||||
|
let h1 = 1.0/h;
|
||||||
|
let h2 = 0.5*h;
|
||||||
|
|
||||||
|
let x = (x.min((self.width+2) as f32 * h)).max(h);
|
||||||
|
let y = (y.min((self.height+2) as f32 * h)).max(h);
|
||||||
|
|
||||||
|
let (f, dx, dy) = match field {
|
||||||
|
Field::U => (&self.u, 0.0, h2),
|
||||||
|
Field::V => (&self.v, h2, 0.0),
|
||||||
|
Field::Smoke => (&self.m, h2, h2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let x0 = ((x-dx)*h1).floor().min(self.width as f32 + 1.0);
|
||||||
|
let tx = ((x-dx) - x0*h) * h1;
|
||||||
|
let x1 = (x0 + 1.0).min(self.width as f32 + 1.0);
|
||||||
|
|
||||||
|
let x0 = x0 as usize;
|
||||||
|
let x1 = x1 as usize;
|
||||||
|
|
||||||
|
let y0 = ((y-dy)*h1).floor().min(self.height as f32 + 1.0);
|
||||||
|
let ty = ((y-dy) - y0*h) * h1;
|
||||||
|
let y1 = (y0 + 1.0).min(self.height as f32 + 1.0);
|
||||||
|
|
||||||
|
let y0 = y0 as usize;
|
||||||
|
let y1 = y1 as usize;
|
||||||
|
|
||||||
|
let sx = 1.0 - tx;
|
||||||
|
let sy = 1.0 - ty;
|
||||||
|
|
||||||
|
let val = sx * sy * f[x0*n + y0]
|
||||||
|
+ tx*sy * f[x1*n + y0]
|
||||||
|
+ tx*ty * f[x1*n + y1]
|
||||||
|
+ sx*ty * f[x0*n + y1];
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn avg_u(&self, i: usize, j: usize) -> f32 {
|
||||||
|
let n = self.height + 2;
|
||||||
|
let u = (self.u[i*n + j-1] + self.u[i*n+j] + self.u[(i+1)*n + j-1] + self.u[(i+1)*n + j]) * 0.25;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn avg_v(&self, i: usize, j: usize) -> f32 {
|
||||||
|
let n = self.height + 2;
|
||||||
|
let v = (self.v[(i-1)*n + j] + self.v[i*n+j] + self.v[(i-1)*n + j + 1] + self.v[i*n + j*1]) * 0.25;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advect_vel(&mut self, dt: f32) {
|
||||||
|
let mut newU = self.u.clone();
|
||||||
|
let mut newV = self.v.clone();
|
||||||
|
|
||||||
|
let n = self.height + 2;
|
||||||
|
let h = 1.0;
|
||||||
|
let h2 = 0.5 * h;
|
||||||
|
|
||||||
|
for i in 1..self.width+2 {
|
||||||
|
for j in 1..self.height+2 {
|
||||||
|
// u component
|
||||||
|
if self.s[i*n + j] != 0.0 && self.s[(i-1)*n + j] != 0.0 && j < self.height+1 {
|
||||||
|
let x = i as f32 * h;
|
||||||
|
let y = j as f32 *h + h2;
|
||||||
|
let u = self.u[i*n + j];
|
||||||
|
let v = self.avg_v(i, j);
|
||||||
|
let x = x - dt*u;
|
||||||
|
let y = y - dt*v;
|
||||||
|
let u = self.sample_field(x, y, Field::U);
|
||||||
|
newU[i*n + j] = u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// v component
|
||||||
|
if self.s[i*n + j] != 0.0 && self.s[i*n + j - 1] != 0.0 && i < self.width + 1 {
|
||||||
|
let x = i as f32 * h;
|
||||||
|
let y = j as f32 *h + h2;
|
||||||
|
let u = self.avg_u(i, j);
|
||||||
|
let v = self.v[i*n+j];
|
||||||
|
let x = x - dt*u;
|
||||||
|
let y = y - dt*v;
|
||||||
|
let v = self.sample_field(x, y, Field::V);
|
||||||
|
newV[i*n + j] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.u = newU;
|
||||||
|
self.v = newV;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advect_smoke(&mut self, dt: f32) {
|
||||||
|
let mut newM = self.m.clone();
|
||||||
|
|
||||||
|
let n = self.height + 2;
|
||||||
|
let h = 1.0;
|
||||||
|
let h2 = 0.5*h;
|
||||||
|
|
||||||
|
for i in 1..self.width+1 {
|
||||||
|
for j in 1..self.height+1 {
|
||||||
|
let u = (self.u[i*n+j] + self.u[(i+1)*n + j]) * 0.5;
|
||||||
|
let v = (self.v[i*n+j] + self.v[i*n + j+1]) * 0.5;
|
||||||
|
let x = i as f32 *h + h2 - dt*u;
|
||||||
|
let y = j as f32 *h + h2 - dt*v;
|
||||||
|
|
||||||
|
newM[i*n+j] = self.sample_field(x, y, Field::Smoke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.m = newM;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user