← Blog
SimulationPhysicsCanvas APIMath

ブラウザで流体シミュレーション — 100×60グリッドで実現するリアルタイム演算

更新 2026年5月27日8 分で読める

このトピックを深掘りする

クリエイティブコーディング

ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法

全12本中 2 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

流体シミュレーションの3ステップ

Navier-Stokes方程式を直接解くのは計算コストが高すぎる。Jos Stamの手法に従い、3ステップに分解する:

  1. 拡散(Diffusion): 速度場・密度場が周囲に広がる
  2. 移流(Advection): 流体が自身の速度場に沿って運ばれる
  3. 圧力補正(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はパイプネットワーク上の圧力差から流量を計算する離散モデルで、計算手法がまったく異なる。