前回は粒子の初期条件と境界条件について解説しました。
今回は粒子の可視化部分を作り、全体を整理してプログラムを完成させたいと思います。
目次
粒子の可視化
【5-3】章で説明したとおり、粒子の可視化はp5.jsのdraw関数で行います。運動方程式を解いて得られた時々刻々変化する粒子の位置に、円を描いていくことによって、アニメーションが表示されます。draw関数内に直接書いてもよいですが、可視化部分も関数にしておきましょう。関数名はdrawParticleとしておきます。
function drawParticle() {
background(220);
// time
fill("black");
textSize(32);
text('time = ' + time.toFixed(2), 10, 50);
// fluid particle
const drawEllipse = function(p, scale, d) {
for (let i = 0, n = p.length; i < n; i++) {
if (!p[i].active) continue;
const x = (p[i].position.x - regionAll.left) * scale;
const y = canvasHeight - (p[i].position.y - regionAll.bottom) * scale;
ellipse(x, y, d);
}
};
const scale = canvasWidth / regionAll.width
const d = particleSize * scale;
noStroke();
fill("blue");
drawEllipse(p, scale, d);
// wall particle
fill("red");
drawEllipse(pWall, scale, d);
}
最初のbackgroundは前に説明したとおり、背景を灰色に塗りつぶす命令です。
// time
fill("black");
textSize(32);
text('time = ' + time.toFixed(2), 10, 50);
textはp5.jsの命令でテキスト文字を画面に出力します。fillは色指定で黒に、textSizeは文字の大きさの設定です。ここで、timeは時刻を表すグローバル変数です。toFixed(2)は数値を小数点以下2桁までとして表します。textの10, 50はキャンバスのx=10px, y=50pxの位置に出力するという意味です。
※p5.js特有の関数とJavaScriptの命令が混じっているので注意してください。ここでは、fill, textSize, text関数はp5.js、.toFixedはJavaScriptの命令です。
// fluid particle
const drawEllipse = function(p, scale, d) {
for (let i = 0, n = p.length; i < n; i++) {
if (!p[i].active) continue;
const x = (p[i].position.x - regionAll.left) * scale;
const y = canvasHeight - (p[i].position.y - regionAll.bottom) * scale;
ellipse(x, y, d);
}
};
drawEllipseという関数は、粒子配列pに対して円を描いています。scaleは計算上のサイズを画面上のピクセルサイズに変換する係数です。dは描く円の直径を表しています。
ここで、画面のキャンバスの位置座標x、yと計算上の座標が異なるので変換しています。キャンバスは、左上角が原点で下向きにy軸が伸びるような座標になっています。
const scale = canvasWidth / regionAll.width
const d = particleSize * scale;
noStroke();
fill("blue");
drawEllipse(p, scale, d);
noStrokeは円の外形線を表示しない設定です。流体粒子pは青色として、先程定義したdrawEllipse関数で表示しています。
// wall particle
fill("red");
drawEllipse(pWall, scale, d);
壁粒子pWallは赤色で、同じくdrawEllipse関数で表示します。
さてこれで、可視化のメイン部分ができたので、draw関数の中に書いてやります。
function draw() {
if (isRun) {
time += timeDelta;
motionUpdate();
drawParticle()
}
}
isRunのif文ですが、isRunというのは別途定義するグローバル変数で、trueかfalseの値をとります。isRunがtrueならば計算を実行し、falseなら計算しないというスイッチになります(後述)。
draw関数はp5.jsで繰り返し呼ばれるので、粒子の運動方程式を解くmotionUpdate関数と粒子を可視化するdrawParticle関数を実行しています。時間timeは時間刻みtimeDeltaずつ増分してやります。
ボタンの作成
画面には[再生/停止]と[リセット]という2つのボタンが配置されています。このボタンを作りましょう。p5.jsにボタンを簡単に作る機能があるので、これを使って作ることにします。
まず、ボタンを押したときの挙動を決めておきます。
今回のプログラムでは以下のような挙動をするものとします。
- index.htmlが読み込まれたときに、初期位置の粒子を表示します。
- [再生/停止]ボタンを押すと、計算がスタートし、粒子のアニメーションが始まります。
- もう一度、[再生/停止]を押すと、計算が停止します。
- さらに、[再生/停止]を押すと、計算を再開します。
- [リセット]ボタンを押すと、初期位置に戻ります。
2~5はユーザーがいつのタイミングで、どの順番でボタンを押すかわかりません。これまでの講座のプログラムでは上から順番にプログラムが実行され、下までいくとプログラムが終了するものでした。これだと、順番を予め決めておかないとプログラムできません。
しかし、世の中のプログラムの多くはユーザーが自由に操作できるようになっています。つまり、「ボタンが押された」という状態になれば何かの処理をするというプログラムが必要です。この「ボタンが押された」のように何かこれまでと異なった状態になることをプログラミングではイベントと呼んでいます。
プログラム
このボタン作成の部分は【5-3】章でお話したp5.jsのsetup関数の中に書いておきます。setupは初期化のために最初に一度だけ呼ばれる関数でした。
const buttonRun = createButton("再生 / 停止");
buttonRun.position(10, canvasHeight + 20)
buttonRun.mousePressed(runSPH);
const buttonReset = createButton("リセット");
buttonReset.position(100, canvasHeight + 20)
buttonReset.mousePressed(resetSPH);
ボタンを作るのはcreateButton関数です。これはp5.jsの関数です。この関数はボタンのオブジェクトを返すので、最初のbuttonRunという変数には[再生/停止]ボタンのオブジェクトが格納されます。.positionはボタンの位置をキャンバス座標で指定します。
そして、.mousePressed()は「ボタンが押された」というイベントが起こったときには、その引数の関数を呼び出すという命令です。ここでは、runSPHという関数を呼び出します。
function runSPH() {
isRun = !isRun;
}
runSPHは簡単な関数です。isRun = !isRunこの文は、上述のisRunがtrueならfalseにする、falseならtrueにするというスイッチの役目をはたします。これで、[再生/停止]ボタンを押すごとに、計算を開始したり停止したりすることができるようになります。
同様に、buttonReset変数に、[リセット]ボタンオブジェクトを定義します。こちらはボタンが押されるとresetSPH関数を呼び出します。
function resetSPH() {
isRun = false;
time = 0;
initialParticle();
drawParticle();
}
resetSPH関数は初期状態に戻すための関数です。isRunをfalse(計算停止)、時間timeをゼロにリセット、initialParticle関数で粒子を初期状態にする、そしてその状態で描画(drawParticle関数)しています。
最後になりましたが、setup関数を全て示しておきます。
function setup() {
setParameter();
createCanvas(canvasWidth, canvasHeight);
resetSPH();
const buttonRun = createButton("再生 / 停止");
buttonRun.position(10, canvasHeight + 20)
buttonRun.mousePressed(runSPH);
const buttonReset = createButton("リセット");
buttonReset.position(100, canvasHeight + 20)
buttonReset.mousePressed(resetSPH);
}
setParameter関数で条件を設定し、createCanvasでキャンバスを定義し、resetSPH関数で初期状態にしています。後は前述のボタンの作成です。
グローバル変数
話が前後しますが、最後にグローバル変数の説明をしておきます。グローバル変数は、関数の外で宣言する変数で、ここで宣言された変数はどの関数からも参照することができます。通常はプログラムの先頭で宣言しておきます
"use strict";
// global variables ***************************************************
// canvas
let canvasWidth, canvasHeight;
// particle
let p, pWall;
let particleSize, h;
let massParticle, stiffness, density0, viscosity;
let w;
let grv;
// region
let regionAll, regionInner, regionInitial;
let thicknessWall;
let cell;
// time
let time, timeDelta;
// control
let isRun;
最初の"use strict"は、「strict(厳格)なモードにする」ことを意味し、より厳格なエラーチェックがなされます。とりあえず先頭に入れておくとよいでしょう。あとは、グローバル変数の宣言です。変数はこれまでに全て説明してきました。いちおう変数名をわかりやすくしていますので、変数名を見ると大体意味がわかると思います。
SPH法プログラムのソースコード
どうもお疲れさまでした。これでSPH法でダム崩壊問題を解くJavaScriptプログラムが完成しました。これまで説明したのと同様に、JavaScriptのプログラムとp5.js、index.htmlを用意すれば実行することができます。
今まで、小出しにコードを示して来ましたが、全てのソースコードをGitHubにあげておきますので、参考にしてください。全てを眺めたうえで、もう一度復習してみると理解しやすいかも知れません。
→プログラムはこちら(GitHub)
※dambreakのフォルダーを参照してください。
まとめ
粒子の可視化部分を作成し、全てのプログラムを完成させました。全体のソースコードも載せているので復習してみてください。
さて、次回はシリーズ最終回です。回転するブレードのプログラムの説明をして終わりにします。