CUDA × OpenGL:格子ボルツマン法(LBM)によるリアルタイム流体シミュレーターの構築

GPUの演算能力を、物理シミュレーションとグラフィックスの融合に活用してみました。 本プロジェクトでは、流体解析の手法である「格子ボルツマン法(LBM: Lattice Boltzmann Method)」をCUDAで実装し、OpenGLを用いてその動態をリアルタイムに可視化するシミュレーターを作成しました。

中心には複雑な干渉を生む「星型」の障害物を配置。マウスドラッグによる滑らかな波紋の生成や、障害物による波の回折・干渉を、512×512のグリッド上で高フレームレートにシミュレートします。物理演算と描画の両方をGPU内で完結させることで、CPUでは到底不可能なリアルタイム性を実現しています。


技術詳細解説

本プログラムは、大きく分けて**「物理演算(LBM)」「パルス注入(入力)」「可視化」**の3つのフェーズで構成されています。

1. 格子ボルツマン法 (D2Q9モデル)

流体を微視的な粒子の集まりとして捉え、その速度分布関数の変化を計算します。

  • D2Q9モデル: 各格子点は9つの速度方向(静止、上下左右、斜め)を持ちます。

  • ストリーミング(移動): 隣接する格子から粒子が移動してくるステップです。本コードでは、境界条件(星型の壁)に衝突した粒子を跳ね返す「Bounce-back法」を実装しています。

  • 衝突(緩和): 粒子同士の衝突により、局所的な平衡状態へ近づくステップです。定数 omega(緩和時間)が流体の粘性を決定します。

2. 滑らかなマウス入力(線形補間パルス)

マウスドラッグの軌跡を滑らかに表現するため、単なる点ではなく「線分」としてエネルギーを注入しています。

  • 線分最短距離計算: 前フレームと現フレームの座標を結ぶ線分から、各格子点への最短距離を計算します。

  • ガウス分布による注入: 距離に応じた重み付け(expf)を行い、密度を加算することで、航跡のような自然な波紋を生成します。

3. GPUダイレクト可視化

演算結果を一度もCPUに戻すことなく、GPU内で直接映像へ変換しています。

  • 物理量のカラーマッピング: 格子点ごとの流速(ux, uy)と密度(rho)の変化を色相に変換。tanhf 関数を用いることで、激しい変化もサチュレーションを起こさず美しく描写します。

  • テクスチャ共有: CUDAで計算した画像バッファをOpenGLのテクスチャとしてバインドし、1枚の板ポリゴンに貼り付けて表示しています。

    This project is a real-time fluid dynamics simulator that implements the Lattice Boltzmann Method (LBM) by leveraging the parallel processing power of the GPU. By tightly integrating CUDA for physical computations and OpenGL for rendering, it achieves exceptionally smooth visualization of fluid behavior on a high-resolution grid.

    Technical Highlights

    • Star-Shaped Obstacle: A gray, star-shaped obstacle is positioned at the center of the domain. Utilizing a mathematical Signed Distance Function (SDF) approach based on polar coordinates, the simulator accurately handles wave diffraction and interference against complex geometries.

    • Linearly Interpolated Drag Input: To ensure seamless wave generation regardless of mouse movement speed, a custom kernel interpolates between previous and current mouse coordinates, injecting energy along the continuous path.

    • Zero-Copy Visualization: Physical quantities are converted directly into pixel colors within GPU memory. These are then rendered as OpenGL textures without being transferred back to the CPU, eliminating data bottlenecks and ensuring high frame rates.

    How It Works

    1. Collision & Streaming: The particle distribution functions at each grid point move to neighboring nodes and collide to approach a local equilibrium state.

    2. Boundary Conditions: Particles hitting the star-shaped wall undergo a “bounce-back” process, creating intricate wave patterns around the object.

    3. Visualization: Changes in local velocity and density are mapped to a vibrant color palette using a tanh activation function to prevent color saturation.

CUDA C サンプルプログラム

Gemini先生の教えメモ

このプログラムは、2つの配列(ベクトル)の各要素をGPUの数千ものスレッドで同時に足し合わせるもの。

 

 

1. __global__ 修飾子

これは**「カーネル (Kernel)」**であることを宣言するキーワードです。

  • 意味: 「CPUから呼び出され、GPU上で実行される関数」であることを示します。

  • 役割: コンパイラ(nvcc)はこのキーワードを見て、「この関数はx86(CPU)用ではなく、NVIDIAのアーキテクチャ(PTXコード)用にビルドせよ」と判断します。

  • 制約: 戻り値は必ず void である必要があります。


2. カーネルの引数 (const float *A など)

引数はすべて「GPUのメモリ(ビデオメモリ/VRAM)」を指すポインタです。

  • CPU側のメモリを直接渡すことはできず、事前に cudaMalloc で確保し、cudaMemcpy でデータを転送しておいたアドレスを渡します。

  • numElements は、配列のサイズを伝えて、範囲外アクセスを防ぐために必要です。


3. スレッドIDの計算(最重要ポイント)

C++

ここが並列計算の魔法の種明かしです。GPUは、この関数を数万個のスレッドで同時に実行します。各スレッドは「自分は誰か?」を知るために、組み込み変数(構造体)を参照します。

変数名 意味 例(今回のケース)
blockIdx.x 現在のスレッドが所属しているブロックの番号 0 〜 195
blockDim.x 1つのブロックの中に含まれるスレッドの数 256
threadIdx.x ブロック内でのスレッドの通し番号 0 〜 255

4. if (i < numElements) によるガード

なぜこれが必要なのでしょうか?

GPUは「ブロック(今回なら256単位)」でスレッドを起動します。

  • 計算したい要素数:50,000

  • 起動したスレッド数:196ブロック × 256 = 50,176スレッド

  • 余り:176スレッド

もしこの if 文がないと、50,001番目以降の存在しないメモリ領域にアクセスしようとして、プログラムがクラッシュ(セグメンテーション違反)してしまいます。これを防ぐために「自分の番号 i が配列サイズ内にあるときだけ仕事をする」という条件分岐を入れています。


5. 計算処理:C[i] = A[i] + B[i];

ここまで来て、ようやく実際の計算です。

  • CPUの場合: for(int i=0; i<N; i++) と書いて、1つずつ順番に足していきます。

  • GPUの場合: 50,000人の作業員(スレッド)が、一斉に自分の担当番号 i の箱を開け、中身を足して、結果を C[i] に書き込みます。

論理的には**「一瞬」**で全要素の足し算が終わります(実際にはメモリ帯域などの物理的制約がありますが、CPUのループより遥かに高速です)。


まとめ:GTX 1060の中での動き

あなたがプログラムを実行したとき、GTX 1060の中では以下のことが起きています。

  1. GPU内部のスケジューラが、196個のブロックを各コア(SM)に割り当てる。

  2. 各コアで256個ずつのスレッドが爆速で立ち上がる。

  3. 各スレッドが計算式を使って「俺は3456番目の担当だ!」と計算する。

  4. 一斉にメモリを読み書きする。

この「スレッドIDの計算」さえマスターすれば、どんなに複雑な並列計算も作れるようになります。

 

The process of brute force calculation by Coulomb force with multiprocessing and numpy. TEST3

Each result returned by the distributed processing is not made into a PN-sized matrix, but is placed as is. This lightens the processing.

The process of brute force calculation by Coulomb force with multiprocessing and numpy. TEST2

The split process reduced the matrix size as much as possible, but it still eats up too much memory.
Multiprocessing consumes a lot of memory.

The process of brute force calculation by Coulomb force with multiprocessing and numpy. TEST1

The small distributed matrices also eat too much memory because they are only PNxPN in size.
Too slow.

The process of brute force calculation by Coulomb force

The process of brute force calculation by Coulomb force is calculated as a matrix using numpy. In the calculation, each element is calculated in a small matrix and summed up at the end to enable parallel processing later.

Calculating long-range particle interactions with numpy (handmade mesh grid)

Too slow

Calculating long-range particle interactions with numpy

乱数を使って円周率もどきを求める2

先日試した乱数を使った円周率を求める方法を
numpyを使った方法に置き換えた。

実行結果

numpy使わずにwhileで計算していたまえの処理に較べて15倍位早い結果。全て配列に収めてから処理するとさすがに早い。ただしメモリもバカ喰い。