TASOGARE GAMES BLOG

タソガレゲイムス :: 神奈川県でゲームを開発する人々のブログです。

uGUI/Texture2Dで描画するリアルタイム流体計算

前回の記事では、UnityのGUIを使わずにC#だけでアプリを作成するという非生産的なことをしました。まだお楽しみでない方は是非ご覧下さい。 tasogare-games.hatenablog.jp

さて、UnityにはGUIによる直観的なオブジェクト操作の他にも、便利で強力な機能が多々あります。その中でも特に魅力的なのは、組み込みで手軽に使えるリアルタイム物理演算でしょう。

ということで今日は、組み込みの物理演算を使わずに、シンプルな流体のリアルタイム演算と描画までをやってみたいと思います。

StableFluidで演算する

有名な流体シミュレーション手法として、SIGGRAPH '99で発表された「Real-Time Fluid Dynamics for Games」より、StableFluidがあります。
このStableFluidの良いところは、格子法を用いているので処理も描画も簡単なところです。

元のはOpenGL/GLUT向けに書かれているのですが、これをUnityで扱うべくC#に移植したものを用意しました。ソースコードはこちら

上記は静的クラスとして用意したので、使い方は至って簡単。
「入力→VelStep→DensStep→描画」を毎Updateで繰り返すだけです。

public int N;
public float diff, visc;
private float[] u, u0, v, v0, dens, dens0;

void Update()
{
    // 入力を反映する
    GetFromUI(dens0, u0, v0);

    // 粛々と計算する
    FluidMath.VelStep(N, ref u, ref v, ref u0, ref v0, visc, dt);
    FluidMath.DensStep(N, ref dens, ref dens0, ref u, ref v, diff, dt);

    // 描画する
    DrawDensity();
}

Nは解像度、densは圧力、u, vは速度、visc, diff, dtは調整用のパラメータです。
なお、引数の前についているrefは、参照渡しを指定する修飾子です。

変数の詳しいことは、元の論文(PDF)を読んでみてください。
ね、簡単でしょう?

演算結果をTexture2Dで画像化する

ProcessingやopenFrameworksでこの手の計算をしたことのある方なら、次にやることは分かりますよね。
そうです、Float配列で持っている演算結果をテクスチャーに割り当てます。

Unityの場合は、Texture2Dクラスのインスタンスを用意して、そこに結果を割り当てます。

private Texture2D _tex;
private Color[] _colors;

void Start()
{
    _tex = new Texture2D(N, N, TextureFormat.RGB24, false);
}

void DrawDensity()
{
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            float d = _dens[FluidMath.IX(N, i + 1, j + 1)];
            _colors[i + N * j] = new Color(d, d, d);
        }
    }

    _tex.SetPixels(_colors);
    _tex.Apply();
}

FluidMath.IX(N, i + 1, j + 1) の意味ですが、これはStableFluidにおいて、ボーダー部分を除くためにこういう書き方になっています。意味が分からなければ論文(PDF)を……

Texture2Dを更新する際は、SetPixelsの後にApplyメソッドを呼ぶことを忘れないでください。私はこれで2日近く悩みました。

RawImageでテクスチャーを表示する

Texture2Dによって演算結果の画像化は出来ましたが、まだこれでは何も表示されません。Texture2Dを表示する先が必要です。

今回はuGUIのRawImageに生成したテクスチャーを割り当てることで、表示を実現します。
RawImageは、Imageよりも機能が少ない代わりに、Spriteを生成せずTextureを直接アタッチできる利点を持っています。

CanvasおよびRawImageは以下の設定値にしておきます。

f:id:tasogare_games:20150428151929p:plain

Texture2DをRawImageに割り当てるには、以下のコードを追加するだけで大丈夫です。

public RawImage raw;

void Start()
{
    raw.color = Color.white;
    raw.texture = _tex;
}

なお、raw.colorは非常に重要です。誤って黒にしようものなら、なにも表示されなくなります。私はこれで人生のうち4日分を無駄にしました。

マウス入力を反映する

おっと、入力処理がまだでした。
以下に簡単なマウス入力処理を抜粋します。

private Vector3 _pos0;

void GetFromUI(float[] d, float[] u, float[] v)
{
    // 配列を初期化する
    int size = (N + 2) * (N + 2);
    for (int i = 0; i < size; i++) {
        u[i] = v[i] = d[i] = 0;
    }

    // (中略)

    // マウス座標をRawImageのローカル座標に変換
    Vector3 pos = raw.transform.InverseTransformPoint(Input.mousePosition);

    // 座標から対象セルを導く
    int px = Mathf.CeilToInt(pos.x + N * 0.5f);
    int py = Mathf.CeilToInt(pos.y + N * 0.5f);
    int i = FluidMath.IX(N, px, py);

    // 左ドラッグで加速
    if (Input.GetMouseButton(0)) {
        u[i] = force * (pos.x - _pos0.x);
        v[i] = force * (pos.y - _pos0.y);
    }
    // 右クリックで流体注入
    if (Input.GetMouseButton(1)) {
        d[i] = source;
    }

    _pos0 = pos;
}

注意すべきは、マウス座標はグローバルなので、transform.InverseTransformPointメソッドを使うことでRawImageのローカル座標に変換する処理が必要だということ点ぐらいでしょうか。
なお中略には、枠からマウスがはみ出していたり、マウス入力が無い場合の中断処理などが入ります。

プログラムを仕上げて動かす

説明が面倒で端折っていた諸々を追加し、完成した最終的なソースコードはこちらです。

最後に、空のゲームオブジェクトをHierarchy上に作成して、今まで書いてきたC#のコードをAddComponentします。

プレビューして、右ドラッグで流体を配置して、左ドラッグで加速させましょう。
こんな感じで表示されたら完成です。お疲れ様でした。

f:id:tasogare_games:20150428153656p:plain