はじめに
Claude.aiを使って流体力学を遊んでいたら「あれ、これラテアートできるんじゃね?」といことで実装に踏み切りました。
作ったもの
ブラウザでラテアートが楽しめるアプリです。
デモ
遊び方
- マウスでドラッグするとミルクが出ます
- 適当に模様を描いてみましょう
- 完成したらスクショして友達に自慢しましょう??
実装
流体シミュレーション
ナビエ–ストークス方程式を使った流体シミュレーションです。
流体シミュレーションは以下のステップで構成されています。
1. 拡散 (Diffusion) - ミルクが周囲に広がる
/**
* 拡散ステップ
* @param {number} b 境界の種類
* @param {Float32Array} x 解く配列
* @param {Float32Array} x0 元の配列
* @param {number} diff 拡散率
* @param {number} dt タイムステップ
*/
function diffuse(b, x, x0, diff, dt) {
const a = dt * diff * N * N;
lin_solve(b, x, x0, a, 1 + 4 * a);
}
2. 移流 (Advection) - 流れに沿ってミルクが移動する
/**
* 移流ステップ
* @param {number} b 境界の種類
* @param {Float32Array} d 解く配列
* @param {Float32Array} d0 元の配列
* @param {Float32Array} u x方向速度
* @param {Float32Array} v y方向速度
* @param {number} dt タイムステップ
*/
function advect(b, d, d0, u, v, dt) {
const dt0 = dt * N;
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
let x = i - dt0 * u[IX(i, j)];
let y = j - dt0 * v[IX(i, j)];
if (x < 0.5) x = 0.5;
if (x > N + 0.5) x = N + 0.5;
let i0 = Math.floor(x);
let i1 = i0 + 1;
if (y < 0.5) y = 0.5;
if (y > N + 0.5) y = N + 0.5;
let j0 = Math.floor(y);
let j1 = j0 + 1;
let s1 = x - i0;
let s0 = 1 - s1;
let t1 = y - j0;
let t0 = 1 - t1;
d[IX(i, j)] = s0 * (t0 * d0[IX(i0, j0)] + t1 * d0[IX(i0, j1)]) +
s1 * (t0 * d0[IX(i1, j0)] + t1 * d0[IX(i1, j1)]);
}
}
set_bnd(b, d);
}
3. 射影 (Projection) - 非圧縮性流体の制約を満たす
/**
* 射影ステップ
* @param {Float32Array} u x方向速度
* @param {Float32Array} v y方向速度
* @param {Float32Array} p 圧力配列
* @param {Float32Array} div 発散配列
*/
function project(u, v, p, div) {
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
div[IX(i, j)] = -0.5 * (
u[IX(i + 1, j)] - u[IX(i - 1, j)] +
v[IX(i, j + 1)] - v[IX(i, j - 1)]
) / N;
p[IX(i, j)] = 0;
}
}
set_bnd(0, div);
set_bnd(0, p);
lin_solve(0, p, div, 1, 4);
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= N; i++) {
u[IX(i, j)] -= 0.5 * N * (p[IX(i + 1, j)] - p[IX(i - 1, j)]);
v[IX(i, j)] -= 0.5 * N * (p[IX(i, j + 1)] - p[IX(i, j - 1)]);
}
}
set_bnd(1, u);
set_bnd(2, v);
}
状態の更新
4. 密度(Density) - ミルクの濃度を更新
/**
* 密度ステップ
* @param {Float32Array} x 密度配列
* @param {Float32Array} x0 元の密度配列
* @param {Float32Array} u x方向速度
* @param {Float32Array} v y方向速度
* @param {number} diff 拡散率
* @param {number} dt タイムステップ
*/
function dens_step(x, x0, u, v, diff, dt) {
for (let i = 0; i < SIZE; i++) {
x[i] += dt * x0[i];
}
diffuse(0, x0, x, diff, dt);
advect(0, x, x0, u, v, dt);
}
5. 速度(Velocity) - 流体の流れを更新
/**
* 速度ステップ
* @param {Float32Array} u x方向速度
* @param {Float32Array} v y方向速度
* @param {Float32Array} u0 元のx方向速度
* @param {Float32Array} v0 元のy方向速度
* @param {number} visc 粘度
* @param {number} dt タイムステップ
*/
function vel_step(u, v, u0, v0, visc, dt) {
for (let i = 0; i < SIZE; i++) {
u[i] += dt * u0[i];
v[i] += dt * v0[i];
}
diffuse(1, u0, u, visc, dt);
diffuse(2, v0, v, visc, dt);
project(u0, v0, u, v);
advect(1, u, u0, u0, v0, dt);
advect(2, v, v0, u0, v0, dt);
project(u, v, u0, v0);
}
パラメータ調整
流体シミュレーションのパラメータ(粘度、拡散率、力の大きさなど)をいい感じにするのが本当に難しかったです。
パラメータ失敗例①
ハンドミキサーみたい...
パラメータ失敗例②
探知機みたい...
最終的なパラメータ
調整して、ようやく「ラテアートっぽい」動きになりました。
let viscosity = 0.005; // 粘度
let diffusion = 0.000001; // 拡散率
let force = 5; // ミルクを注ぐ力
let densityAmount = 300; // ミルクの量
// 密度の減衰を非常に少なくする(ミルクのパターンが残る)
for (let i = 0; i < SIZE; i++) {
dens[i] *= 0.9995;
}
改善点
1. ミルクの出し方をもっとリアルに
現状はアートなのかというと怪しいです。絵が描けるくらいリアルな動きをできるように調整したいですね。
2. かき混ぜる機能
スプーンでかき混ぜる機能があると面白そうです。
3. パフォーマンス改善
JavaScriptでの流体シミュレーションはかなり処理が重くなります。
グリッドサイズを128にしているのですが、これ以上げると重くなりラテアートっぽい動きができなくなります。そのため、画質が荒くドット絵みたいになってしまいました。
const N = 128; // グリッドサイズ
const SIZE = (N + 2) * (N + 2); // 境界を含む配列サイズ
const DT = 0.1; // タイムステップ
今回は、Javascriptで完結させたいということで妥協しましたが、WebGL・Unity・WebAssemblyなどを用いて再挑戦したいですね。
まとめ
今回は、WEBでラテアートを実装しました。
今のところ、リアルな動きができていないので、役割は持ててないですが、カフェのホームページのトップにあればオシャレかも知れないです。
また、Claude.aiを利用することで流体力学という分野に触れることができました。いい時代ですね!
開発リポジトリを公開しているので、興味があったら見てみてください。アプリへのリンクもあります。
もし面白いと思ってもらえたら、GitHub のスターや Qiita の LGTM をいただけると嬉しいです!
ここまで読んでいただきありがとうございました!


