9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザで動く流体力学ラテアート☕

9
Last updated at Posted at 2025-12-10

はじめに

Claude.aiを使って流体力学を遊んでいたら「あれ、これラテアートできるんじゃね?」といことで実装に踏み切りました。

作ったもの

ブラウザでラテアートが楽しめるアプリです。

デモ

Animation1.gif

遊び方

  1. マウスでドラッグするとミルクが出ます
  2. 適当に模様を描いてみましょう
  3. 完成したらスクショして友達に自慢しましょう??

実装

流体シミュレーション

ナビエ–ストークス方程式を使った流体シミュレーションです。
流体シミュレーションは以下のステップで構成されています。

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);
}

パラメータ調整

流体シミュレーションのパラメータ(粘度、拡散率、力の大きさなど)をいい感じにするのが本当に難しかったです。

パラメータ失敗例①

ハンドミキサーみたい...

Animation1.gif

パラメータ失敗例②

探知機みたい...

Animation1.gif

最終的なパラメータ 

調整して、ようやく「ラテアートっぽい」動きになりました。

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;
}

Animation1.gif

改善点

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 をいただけると嬉しいです!

ここまで読んでいただきありがとうございました!

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?