是代码备份
来源
https://github.com/matthias-research/pages/blob/master/tenMinutePhysics/17-fluidSim.html
视频讲解
https://www.bilibili.com/video/BV15h4y1R7Hq/?spm_id_from=333.788.top_right_bar_window_default_collection.content.click&vd_source=6dd875bda9ddfb1fd649643372db5479
<!--
Copyright 2022 Matthias Müller - Ten Minute Physics,
www.youtube.com/c/TenMinutePhysics
www.matthiasMueller.info/tenMinutePhysics
来源
https://github.com/matthias-research/pages/blob/master/tenMinutePhysics/17-fluidSim.html
视频讲解
https://www.bilibili.com/video/BV15h4y1R7Hq/?spm_id_from=333.788.top_right_bar_window_default_collection.content.click&vd_source=6dd875bda9ddfb1fd649643372db5479
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.
-->
<!-- 背景:考研,需要学高数,没有动力,然后想起来大一学高数好奇干啥用 -->
<!-- 国庆去太原理工大学找同学,然后听说了纳维-斯托克斯方程,想起来很久之前在图书馆看到的流体力学 -->
<!-- 微分方程,这个在高数书后面是有提到的,高中帕斯卡方程好像也是流体力学的 -->
<!-- 原来游戏里还有个细胞环流,整进游戏里,现在有个流体方程试试 -->
<!-- b站,请助我一臂之力!搜,大力出奇迹 -->
<!-- 于是,在众多网友帮助之下,我也将成为别人故事中的别人 -->
<!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 {
// 先初始化啥数据,但是不晓得参数意义,对着视频一个个比对试试
// density 密度
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;
// 32 bit 浮点数
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;
}
// 重力,视频的4:09处
// 包装成函数了
// integrate 翻译成中文也没明白啥啥意思
// 参数 : 时间,重量加速度
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;
}
}
}
// 求解不可压缩
// 视频的 5:18处
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++) {
// 盲猜s是散度
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;
//松弛度因子 11:27复刻
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);
// 9:41 提到加权
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;
}
// 0.25是四分之一,视频里有提到但是不能确定
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;
}
// 计算速度,因为发现dt了
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);
}
// 烟雾,盲猜是10:07视频的加权
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) {
// 0.5,视频的10:11提到使用网格中心速度,加权算中心就是一半一半
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
};
// 默认设置 0
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>