
2026/05/27 7:15
Godot ゲームエンジンによるナヴィエ-ストークスの流体シミュレーション解説
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
このリポジトリは、主に流体力学のロジックを学ぶための教育用ツールとして設計された、Godot エンジン内での専用の CPU 専用流体シミュレーションデモを提供します。即座にゲームへの展開向けのパフォーマンス最適化が目的ではないことに留意してください。2026 年 5 月 19 日に rskupnik によって開発され、このプロジェクトは AI で生成されたコードを使用することを厳格に避け、手動でのテキスト/コード作成のみを用いています(AI は研究目的のみに利用されます)。また、基礎となる作品として Jos Stam の「Real-Time Fluid Dynamics for Games」と Mike Ash の「Fluid Simulation for Dummies」を引用しています。実装は、ベクトル速度場によって輸送されるスカラー密度場上の Navier-Stokes 方程式を解き、16×16 のグリッド(ボーダーを含む)とカスタムインデックスを持つ一次元配列、拡散および圧力投影にガウス・ザイデル緩和を用いること、シミュレーション段階に応じて調整される特定のリテンション率を使用することで、これらの概念を実現しています。これらは、クリック操作で密度を追加し、ドラッグ操作で密度/速度を注入するユーザーとの相互作用によって実行されるロケット火炎のエフェクトのシミュレーションを通じて示されます。現在の実装は最適化が施されておらず、完全に CPU で動作しますが、物理学の数式とデータ構造(水平・垂直速度用の別々の配列を含む)に関する深い理解を容易にするためであり、将来の計画としてこのロジックを GPU 計算シェーダへ移植して高速化するまでの明確な出発点となります。
本文
Godot で実装したリアルタイム流体シミュレーション解説
2026 年 5 月 19 日の投稿に基づく、ゲーム開発向けの流体力シミュレーション実装の日本語訳と整理です。
💡 導入と前提条件
本プロジェクトは学習および理解を最優先に設計されたものです。パフォーマンスの最適化(GPU 計算や極限までの変数削減)よりも、コードの可読性と仕組みの明確さを重視しています。
重要なお知らせ
- 著者プロフィール: 私は数学者ではありません。説明に誤りがある場合は Bluesky で DM を送るか、メールしてください。
- 使用ツール: Godot エンジンを使用(GDScript)。
- コードベース: 全リポジトリは github.com/rskupnik/godot-fluid-simulation-demo で確認可能。コミット履歴と各章のコードスナップショットが用意されています。
- AI 利用状況: 記事全文・図・動画・コードはすべて人手で作成。AI は研究支援のみ使用しました。
推奨学習資料
- Jos Stam: Real-Time Fluid Dynamic for Games (論文)
- Mike Ash: Fluid Simulation for Dummies (チュートリアル系)
🧱 基礎:グリッドとデータ構造
流体シミュレーションの根幹は、ナビエ・ストークス方程式に基づいた物理モデルです。ここでは、計算精度よりも速度を犠牲にして「ゲームで使える」程度の品質を目指します。
シミュレーションのコスト削減手法
- グリッドサイズ: コーストを抑えるため、大きなセルを持つ比較的小さなグリッドを使用。
- 時間ステップ: シミュレーションを進化させる時間を任意に設定(
)し、高精度計算をしない。delta - 近似手法: 高ス・セidel 緩和法など、完全解より近似で「十分」な結果を得る手法を採用。
データ構造とグリッド定義
コードは単一のスクリプト
FluidGrid に含まれます。配列は二次元ではなく、アクセスしやすくするために**単一次元の配列(1D Array)**を使用しています。
@export var N := 16 # グリッドのセル数 (Nx) @export var cell_size := 32 # セルごとのピクセルサイズ var size := 0 # 全セル数 (後で計算)
データ配列の詳細
境界(ボーダー)処理のため、有効領域だけでなく周囲も用意し、実質
(N+2) x (N+2) のサイズにします。
| 変数名 | 格納内容 | 備考 |
|---|---|---|
| 物質の量(密度) | 0.0 (空) 〜 1.0 (満ち) |
| 水平方向速度 ($x$ 軸) | |
| 垂直方向速度 ($y$ 軸) | Godot では Y が下向き増加 |
| 前フレームの値 | 計算中のデータ改ざんを防ぐため(スナップショット) |
ヘルパー関数:インデックス変換
二次元座標
(i, j) を単一配列へのインデックスに変換する関数です。
func IX(i: int, j: int) -> int: return i + (N + 2) * j
グリッドの描画 (_draw
)
_draw各セルを
Rect2 として描画し、境界色と内部色の区別を行います。
💧 「fluid」シミュレーションの実装ステップ
以下の順序で機能を追加していくことで、視覚的な流体が動き出します。
1. マウスでの密度注入
マウスクリック(ドラッグ)時に特定のセルに密度
+1.0 を加えます。
func _input(event): if event is InputEventMouseButton and event.pressed: var cell := cell_from_mouse(to_local(event.position)) # ボーダー内側のみ対象 (1 から N) if cell.x >= 1 and cell.x <= N and cell.y >= 1 and cell.y <= N: density[IX(cell.x, cell.y)] += 1.0
2. 密度のフェードアウト (fade_density
)
fade_densityグリッドが白く染まるのを防ぐため、時間経過とともに密度を減衰させます。 注意:
delta(フレーム間隔)を掛けることで FPS に依存しない速度を実現します。
func fade_density(delta: float) -> void: for i in range(1, N + 1): for j in range(1, N + 1): density[IX(i, j)] = max(0.0, density[IX(i, j)] - density_fade_rate * delta)
3. 速度矢印の表示 (_draw_velocity_arrows
)
_draw_velocity_arrows現在
u, v はすべて 0 ですが、視覚化のために矢印を描画する関数を追加します。
: 矢印の描画スケール(シミュレーション挙動には影響しない)。velocity_draw_scale
4. マウスドラッグによる速度注入
クリックだけでなく、マウスを動かした分だけ速度ベクトルを追加できます。これにより、密度を持たない空気の流れ(風のよう)も表現できます。
# ドラッグ時の相対移動量に基づき速度を追加 var delta_velocity := event.relative * velocity_add_scale u[idx] += delta_velocity.x v[idx] += delta_velocity.y
5. 速度のフェードアウト (fade_velocity
)
fade_velocity密度と同様に、速度も時間をかけて減衰させます。
move_toward を使用して滑らかに消えるようにします。
🌊 魔法を起こさせるメカニズム:流体運動の導入
ここからがシミュレーションの核心です。「密度を静止状態」から「流れる状態」に変化させる処理を加えます。
🔀 密度の対流 (advect_density
)
advect_density原理: 現在のセルの密度は、「過去(時間を遡った先)のある場所」にある値を引き継いでいます。
- 現在の速度ベクトルを「逆転させて時間を遡る」。
- その位置を特定し、その点を取り囲む 4 つの隣接セルから双線形補間を行い密度を取得。
このアプローチを採用する理由:
- 速度場の連続性: 速度が瞬間的に方向を変えることは物理的に起きないため、単純な前方追跡よりも逆算の方が安定する。
- 計算精度とコストのバランス: 近似方程式を使用し、計算負荷を下げつつ十分な品質を得る。
# スナップショットを取得(計算中の改ざんを防ぐ) func copy_density_to_prev() -> void: for idx in range(size): density_prev[idx] = density[idx] # 対流処理実行 func advect_density(delta: float) -> void: # 時間を遡ってソース座標を計算 var x := i - dt0 * u[idx] var y := j - dt0 * v[idx] # その位置を取り囲む 4 つのセルを見つける # 双線形補間を行い、密度値を取得して現在のセルに設定
🎨 密度拡散 (diffuse_density
)
diffuse_density原理: 墨を水に垂らしたような現象。各セルから近傍セルへ均等に関係なく値が広がる。
- 手法: Gauss-Seidel 緩和法を使用(推測を繰り返して値を滑らかにする)。
- 効果: 濃淡の境界を滑らかにし、自然な雲のような動きを作る。
# 繰り返し反復 (Iterations) を行うことで拡散効果を強める func diffuse_density(delta: float) -> void: for k in range(20): # 20 回繰り返す # 各セルの値を、近傍セルの平均に近づける計算を行う
🧱 境界の設定 (set_bnd
)
set_bndシミュレーション領域の壁際(ボーダー)で流体が外へ流出しないように処理します。
- 壁としての振る舞い: 壁面に向かってくる速度成分を反転させる(反射)。
- 密度の保持: 壁際のセルは、内部のセルの値をコピーまたは平均化して設定。
⚡ 速度そのものを流す:速度対流と圧力投影
密度だけでなく、「風」そのものも動き回らせる必要があります。また、流体が圧縮されたり膨張したりしないよう(非圧縮流体)する処理を加えます。
💨 速度拡散 (diffuse_velocity
)
diffuse_velocity密度と同様に、速度場も時間とともに緩やかに変化・広がる特性を与えます。これにより、角ばった流れを滑らかにし、粘性効果を実装します。
🌪️ 速度対流 (advect_velocity
)
advect_velocity原理: 「空気の流れ」そのものを追跡する。現在ある位置に空気が存在しているのは、「過去のある位置から移動して来たから」。
- 注意点: この処理では実際の速度配列
,u
を書き換えるため、計算を安定させるためにv
スナップショットを使用します(密度対流とは異なります)。_prev
⚖️ 圧力投影 (project_velocity
):非圧縮性の担保
project_velocity流体が「膨らむ・潰れる」のを防ぐための最も重要なステップです。
- 発散計算: 各セルから流れ出る量と流入する量の差を計算(密度の増減につながる)。
- 圧力解像 (Poisson Solver): 発散が 0 になるような圧力場を計算するために、Gauss-Seidel 緩和法を用いて反復計算を行う。
- 速度修正: 算出された圧力勾配に基づき、各セルの速度ベクトルを微調整し、「膨張/収縮」させない方向へ修正する。
func _process(delta: float) -> void: # --- 速度処理 (先頭と真ん中に必ず project_velocity を挿入) --- copy_velocity_to_prev() diffuse_velocity(delta) advect_velocity(delta) project_velocity() # ★ 圧力による修正 # --- 密度処理 --- copy_density_to_prev() diffuse_density(delta) advect_density(delta) # --- フェードアウト & 再描画 --- fade_density(delta) queue_redraw()
🚀 ゲーム開発での応用と展望
実装が完成した状態
この時点で、マウスで塗った「墨(密度)」が風(速度)を引いて広がり、壁に衝突して跳ねる、粘度を持つ流体的な動きを実現しました。
次のステップ:シェーダーとの連携
現在のシミュレーションは CPU 上で計算されており、複雑な図形を描くには限界があります。今後の方向性として以下のアプローチが考えられます。
- Compute Shader への移植:
- GPU を活用することで、より高分解能のグリッドや更大規模のシミュレーションを実現可能になる。
- GPU で計算したデータを CPU から取得し、それをシェーダーに渡し、美しい視覚効果を合成する。
- コンテキストとしての使用:
- ロケット排気: エンジンの炎を流体として吐き出させる。
- 魔法・特殊効果: 重力の歪みやエネルギー波など、物理挙動を持つエフェクト生成。
まとめ
- 密度と速度を別々に扱い、互いに影響し合うシミュレーションを行う。
- 対流で移動させ、拡散で滑らかにする。
- 圧力投影によって非圧縮性を確保し、壁面で適切な反射を実現する。
この手法は、数式を深く理解せずとも、「物理的な挙動」をゲーム内に組み込むための強力なツールとなります。