ブラウザで流体シミュレーション — 100×60グリッドで実現するリアルタイム演算
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 2 本目。
次に読む記事
フラクタルの数学をブラウザで描く — マンデルブロ集合からバーンズリーのシダまで
複素数反復、エスケープタイムアルゴリズム、IFS(反復関数系)、再帰描画を使い、ブラウザでフラクタルを描画する実装解説。
Perlinノイズで自然な地形を生成する — ランダムなのに美しい理由
勾配ノイズの数学的な仕組みからオクターブ合成、Canvas上での3Dメッシュ描画まで、Perlinノイズによる自然な地形生成をステップごとに実装・解説する。
波動方程式をブラウザで可視化する — 干渉・回折・共鳴の世界
1D・2D波動方程式を差分法で実装し、干渉・回折・共鳴パターンをCanvasで可視化する。WaveInterference、WaveEquation、ChladniPlateの描画手法と物理の対応を解説。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
流体シミュレーションの3ステップ
Navier-Stokes方程式を直接解くのは計算コストが高すぎる。Jos Stamの手法に従い、3ステップに分解する:
- 拡散(Diffusion): 速度場・密度場が周囲に広がる
- 移流(Advection): 流体が自身の速度場に沿って運ばれる
- 圧力補正(Projection): 非圧縮性を保つため速度場を補正
グリッド構造
100×60のグリッドにスカラー値(密度)とベクトル値(速度)を格納する:
const N = 100
const M = 60
const size = (N + 2) * (M + 2) // 境界セル含む
const density = new Float32Array(size)
const velocityX = new Float32Array(size)
const velocityY = new Float32Array(size)Float32Array を使う理由は、通常の配列より数値演算が高速で、メモリ使用量も半分(64bit → 32bit)になるから。
拡散ステップ
ガウス・ザイデル反復法で拡散を近似する:
function diffuse(
b: number,
x: Float32Array,
x0: Float32Array,
diff: number,
dt: number,
) {
const a = dt * diff * N * M
for (let k = 0; k < 20; k++) {
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const idx = i + (N + 2) * j
x[idx] =
(x0[idx] + a * (x[idx - 1] + x[idx + 1] +
x[idx - (N + 2)] + x[idx + (N + 2)])) /
(1 + 4 * a)
}
}
setBoundary(b, x)
}
}20回の反復は精度と速度のトレードオフ。FluidSimulationでは4回まで減らしても視覚的にはほぼ変わらない。
移流ステップ
セミラグランジアン法で、各セルから速度場を逆追跡して値を補間する:
function advect(
b: number,
d: Float32Array,
d0: Float32Array,
u: Float32Array,
v: Float32Array,
dt: number,
) {
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const idx = i + (N + 2) * j
// 逆追跡
let x = i - dt * N * u[idx]
let y = j - dt * M * v[idx]
// クランプ
x = Math.max(0.5, Math.min(N + 0.5, x))
y = Math.max(0.5, Math.min(M + 0.5, y))
// バイリニア補間
const i0 = Math.floor(x)
const j0 = Math.floor(y)
const s1 = x - i0
const t1 = y - j0
const s0 = 1 - s1
const t0 = 1 - t1
d[idx] =
s0 * (t0 * d0[i0 + (N + 2) * j0] + t1 * d0[i0 + (N + 2) * (j0 + 1)]) +
s1 * (t0 * d0[i0 + 1 + (N + 2) * j0] + t1 * d0[i0 + 1 + (N + 2) * (j0 + 1)])
}
}
setBoundary(b, d)
}逆追跡が直感に反するが、こうすることで数値的に安定する。前方追跡だと「どのセルに届くか」の重複処理が必要になる。
圧力補正(Projection)
ヘルムホルツ分解で発散のない速度場を作る:
function project(
u: Float32Array,
v: Float32Array,
p: Float32Array,
div: Float32Array,
) {
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const idx = i + (N + 2) * j
div[idx] = -0.5 * (
(u[idx + 1] - u[idx - 1]) / N +
(v[idx + (N + 2)] - v[idx - (N + 2)]) / M
)
p[idx] = 0
}
}
setBoundary(0, div)
setBoundary(0, p)
// ポアソン方程式をガウス・ザイデルで解く
for (let k = 0; k < 20; k++) {
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const idx = i + (N + 2) * j
p[idx] = (div[idx] + p[idx - 1] + p[idx + 1] +
p[idx - (N + 2)] + p[idx + (N + 2)]) / 4
}
}
setBoundary(0, p)
}
// 速度場から勾配を引く
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const idx = i + (N + 2) * j
u[idx] -= 0.5 * N * (p[idx + 1] - p[idx - 1])
v[idx] -= 0.5 * M * (p[idx + (N + 2)] - p[idx - (N + 2)])
}
}
setBoundary(1, u)
setBoundary(2, v)
}Canvas API描画
密度値を色にマッピングして描画する。ImageData を直接操作してピクセル単位で塗る:
function renderFluid(
ctx: CanvasRenderingContext2D,
density: Float32Array,
width: number,
height: number,
) {
const imageData = ctx.createImageData(width, height)
const cellW = width / N
const cellH = height / M
for (let j = 1; j <= M; j++) {
for (let i = 1; i <= N; i++) {
const d = Math.min(255, density[i + (N + 2) * j] * 255)
const px = Math.floor((i - 1) * cellW)
const py = Math.floor((j - 1) * cellH)
// セル内のピクセルを塗る(簡略版)
const idx = (py * width + px) * 4
imageData.data[idx] = d * 0.3 // R
imageData.data[idx + 1] = d * 0.6 // G
imageData.data[idx + 2] = d // B
imageData.data[idx + 3] = 255 // A
}
}
ctx.putImageData(imageData, 0, 0)
}FluidPipesとの違い
FluidPipesは流体の「輸送」に特化しており、パイプネットワーク上の流量計算がメイン。Navier-Stokesは使わず、各パイプの圧力差から流量を計算する単純なモデル。見た目は似ているが、計算モデルはまったく異なる。
まとめ:ブラウザ流体シミュレーションの道具たち
流体力学の数学的背景を理解するなら、道具棚の書籍が参考になる。Jos Stamの原論文 "Stable Fluids" (1999) も併せて読むと理解が深まる。
よくある質問
- Q. ブラウザで流体シミュレーションをリアルタイムに動かすことは可能ですか?
- 可能。100×60のグリッドにNavier-Stokes方程式を簡略化した3ステップ(拡散・移流・圧力補正)を適用し、Float32ArrayとImageDataで高速描画することで60fpsで動作する。
- Q. 流体シミュレーションの拡散ステップで反復回数はどれくらい必要ですか?
- ガウス・ザイデル反復法で20回が標準だが、リアルタイム描画なら4回まで減らしても視覚的にはほぼ変わらない。
- Q. FluidSimulationとFluidPipesの違いは何ですか?
- FluidSimulationはNavier-Stokes方程式ベースの連続流体シミュレーション。FluidPipesはパイプネットワーク上の圧力差から流量を計算する離散モデルで、計算手法がまったく異なる。