前置き

こんにちは。

今日は、マーチンキューブについて記事を書きたかったんですが、そもそもマーチンキューブって何の役に立つのかを知らないと興味を持てないと思うので、まず先にマーチンキューブがよく活躍する流体シミュレーションについて紹介したいと思います。

流体シミュレーションとは何かというと、水とかの動きをシミュレーションする事です。ゲームに流体シミュレーションを活用すると、リアルな水の表現が可能になるかもしれません。

流体シミュレーションには色々な手法がありますが、私が知ってるのはSPH法です。なのでこの記事ではSPH法について紹介します。

SPHとはSmoothed Particle Hydrodynamicsの略です。直訳すると「平滑化された粒子の流体力学」です。名前の通り、SPH法では水を沢山の粒子(粒々)の集まりとして表現します。というのも、実際に水は水分子の粒々の集まりだからです。それぞれの粒々の動きを一つづつ物理シミュレーションする事で全体として水みたいな動きになります。

「沢山の粒々を物理シミュレーションするだけなら、SPHとか使わなくてもUnityで大量のスフィアコライダ突っ込めばいいんじゃね?」

はい、今読者の心の声が聞こえてきましたね。
たしかにぶっちゃけUnityで大量のスフィアコライダをぶち込むだけでも割とそれっぽくシミュレートできるかもしれません。

ただし、Unityの物理演算では水の粘性とかが考慮されないので、よりリアルな水表現を追い求めるならSPHを使うといいかもしれません。
ゲームに使うだけならそれっぽく見えればそれでいいかもしれませんが、流体力学を研究してる人とかは、当然一番リアルに水を再現できる手法を使いたいでしょうね。

SPHを使うとこんな水表現ができるらしいです↓

ちなみになんで私がSPHについて知ってるかと言うと、私は学生の頃の卒論でSPHとマーチンキューブで水を描画する研究やってました。十数年前の話ですが。

SPH法

というわけでSPHをやっていきましょう。

沢山の粒子で水を表現するという事なので、早速つぶつぶをばら撒いてみました。

さて、それぞれの粒子をどうやって動かせばいいんですか?
そのためには、流体を表現するナビエストークス方程式というのを使います。
SPH向けのナビエストークス方程式はこれです↓

(1) ナビエストークス方程式

「うわ…わけわかんねえ数式出てきた…やばい…こわい…ブラウザ閉じよ…」
まあ大丈夫です。冷静に眺めるとこの方程式の言わんとする事はこういう事です。

つまり、”ある粒子の加速度は、その粒子の位置の圧力項と粘性項と外力(重力とか)を足し合わせたものですよ”と言っているだけにすぎない。

簡単でしょ?

「でもなんか三角形のわけわからん記号∇とか出てるやん、こんなん学校で習ってねえぞ」
∇ってのはナブラって言って、なんかベクトルとかスカラー場に付けると勾配(傾き)を意味する?らしいです。偏微分とかして求めるらしいですが私もよく知りません。その内勉強するかもしれません。まあ今はぶっちゃけどうでもよくないっすか。どうせ実装する時はコードにナブラとか出てこないんで。

離散化

私もナブラとか言われてもよく分からんのと同じように、パソコンだってナブラとか言われてもよく分かりません。
パソコン「数式とかじゃなくてもっとソースコードで説明してくれないと分かんない」
パソコンが喋った

パソコンは数式とか分からないので、パソコンでもわかるように近似的に計算で求められる形に離散化してあげる必要があります。

ところで、一つの水粒子の場所の密度とか圧力って、そこの周りにどんだけ他の粒子が集まってるかで決まりそうですよね。
つまり、近くの粒子同士が影響し合って、かつ粒子同士の距離が近いほど大きな影響を与えることになります。
その事を数式として表現するとこうなります。

(2) 離散化した奴

なんちゃらは”重み関数”と言って、距離が離れるほど影響が小さくなる感じの関数が入ります。距離がhを超えると影響ゼロになるようになってます。

とにかくこれを使って密度やら圧力やらを計算できます。

密度を求める

密度を求めるには、密度ρを上の(2)の方程式のφのとこに突っ込めばOKです。

「Wpoly6って何やねん!?」
これはね、SPHの研究してる人が考えた、「これ使うと密度がそれっぽく求められるよ」っていう重み関数です。具体的には、

こんな感じです。これは3次元の時に上手く動くヤツで、2Dでシミュレートする時はまた別の重み関数を使います。
otherwiseってなんやねん?と言うと、つまり粒子間の距離がhより小さい場合だけ計算すればよくて、hより大きい時は0、つまり無視してくださいという意味です。

数式だとサッパリ意味不明ですが、実際にC#の疑似コードで書くと大して難しくないです。

簡単でしょ?
このコードは適当に書きましたが、まあこんな感じで密度が求まります。

圧力

圧力を求めるには、状態方程式というのを使います。

状態方程式

kは圧力を調整するための単なるパラメータです。ρ0は基本密度です。密度が基本密度に戻ろうとする圧力が流体にかかります。

C#の疑似コードで書くとこうです

圧力項

圧力が求まったので、圧力項も密度と同じノリで計算できます。
(1)のナビエストークス方程式の圧力項に(2)を適用するとこうなります。

当然こうでしょ?と思うんですが、実はこれだとダメだそうです。
なんか、これだと”作用反作用の法則”を満たさないから、”対称化”する必要があって、

こっちの式を使えだそうです。私も何のこっちゃ分からなくなってきたのですが、しょうがないので言われるがままに従います

∇Wspikyとかいう重み関数(の勾配)は、

これを使います。今までのノリと同じように計算できると思うので、もうC#コードは載せません。(面倒くさくなってきた
ノリさえ分かってれば、他の人が書いたSPHのコードとか見てもらえば大体意味が分かると思います。

粘性項

粘性項も圧力項と同じ感じで、これも対称化が必要だそうで、対称化した式はこうなります。

∇²Wviscとかいう重み関数(のラプラシアン)は、

こうです。

衝突判定

ここまででいい感じに圧力項とかが求まってきましたが、そういえば、水槽みたいな壁や床を用意してあげないと、このままでは粒子が重力で真っ逆さまに落ちていきますよね。

壁や床との衝突判定には、ペナルティ法という手法が利用できます。
ペナルティ法では壁を突き抜けそうな粒子に跳ね返す力を与えて壁を突き抜けないようにします。

数式だとこんな感じです。
例えば粒子のx,y,z座標がそれぞれ-1~1の範囲に収まるようにするには、左右、上下、奥手前の6回ペナルティ法の計算を行う事になるでしょう。

粒子の座標が求まった!

というわけで、やっと粒子の加速度が計算できます。
ある粒子の加速度aは、

a = 圧力項 + 粘性項 + 衝突判定の力 + 重力

となります。
加速度さえあれば、速度も求められますね。
速度vは、

v = v + deltaTime * a;

速度があれば、座標もわかります。
座標posは、

pos = pos + deltaTime * v;

簡単ですね。
こんな風に普通に加速度から速度、座標を求める方法は、”前進オイラー法”という手法になります。
これは実は精度が低い方法です。普通のゲームの感じで1秒に60回の実行では発散(ぐっちゃぐちゃになる)してしまうかもしれません。もっとタイムステップを小さく刻む必要があるでしょう。

タイムステップ小さくするのも面倒だな~という場合は、前進オイラー法より安定度の高い手法を使う手があります。
例えばリープ・フロッグ法だとこうなります。

pos = pos + v * deltaTime + 0.5 * a * deltaTime * deltaTime;

v = v + 0.5 * ( prev_a + a ) * deltaTime; //prev_aは前回のフレームのaという意味

おわり

以上の説明で、SPHをプログラミングして実行できる知識が身に付きましたね!
「え、マジ?」
まあそこまでは行かなくても、他の人が書いたSPHのソース見てもなんとなく理解できるくらいにはなったかもしれません。

あとは誰かが書いたSPHのプログラムを拾ってきて実行して遊んで改造したりしてみてください。

UnityでSPHやってるソースとかもGitHubとかで見つかると思います。

https://github.com/khskarl/sph-unity

マーチンキューブに向けて

「ところで、SPH法では水の粒々を動かしてるけど、記事の最初の方に出てきた動画では、一塊の水みたいに描画されてるけど、どうやったらただの粒々が水の塊になるんですかね?」

するどい指摘ですね。そうです。その話がしたくて今回の記事を書いたんですが、水の粒々をいい感じに水の塊として描画できるようにするために必要なのが、マーチンキューブなんです!

というわけで、次の記事ではいよいよマーチンキューブについて説明できればいいなと思います。

オマケ:最適化について

記事中のコードでは、何の工夫も無く総当たりで粒子と他の粒子との計算を行ってました。しかし、これでは計算量はO(n²)となってしまい、粒子数が増えるにつれて計算量が爆発的に増えてしまいます。
もっと効率よく計算する最適化の余地がありますね。

例えば、空間全体を縦、横、奥行きがそれぞれh(影響半径)のグリッドで区切って、各タイムステップでの計算の最初に全ての粒子をそれぞれグリッドに割り振ります。

今までの説明で分かっている通り、粒子の計算では、距離がh以上離れている他の粒子からの影響は0なので、無視していいはずです。

このケースでは、計算に必要な他の粒子が存在する可能性があるのは、明らかに計算する粒子の前後左右奥手前の27箇所(3次元の場合)のグリッドのみとなります。
それらのグリッドに所属する粒子とだけ計算を行えばいいので、大幅に計算量を減らせます。

このようなグリッドに分割して近傍探索を高速化する手法をコンピュートシェーダで実装している記事がありました↓ (2019/10/11 追記)

[#Unity] ComputeShaderで近傍探索の高速化

オマケ2:他の最適化について

上のやり方のような手法以外にも、マルチスレッドで並列処理して高速化したり、GPUで計算を行って高速化するアプローチの手法が考えられます。

都合がよい事に、SPHでは粒子の計算がそれぞれ独立して行えるので、メッチャ並列化しやすいです。

SPHをUnityのジョブシステムとバーストコンパイラを用いて並列化する方法について解説してる記事がこちらにあります↓

https://medium.com/@leomontes_60748/how-to-implement-a-fluid-simulation-on-the-cpu-with-unity-ecs-job-system-bf90a0f2724f

Unity Graphics Programming vol.1 という書籍では、コンピュートシェーダを用いてSPHをGPU上で計算する手法についての記事があります。↓

https://indievisuallab.stores.jp/items/59edf11ac8f22c0152002588

サンプルプログラムのリポジトリはこちらです↓

https://github.com/IndieVisualLab/UnityGraphicsProgramming

他にも、GPU上でSPHをやっているリポジトリがあります↓

https://github.com/MangoSister/SPHFluid

参考資料

Unity Graphics Programming vol.1  (書籍)

水の実時間アニメーション (論文)

[CEDEC 2014]剛体から流体まで,セガのプログラマーが語る「位置ベース物理シミュレーション」の最前線 (記事)

Position Based Dynamics Omelette コンピュータグラフィックス関連の最新論文紹介 (上の4Gamerの記事で紹介されてるCEDECの講演。CEDiLにログインすると講演資料がDLできます)

その他参考リンク

https://bigtheta.io/2016/05/23/particle-based-fluid-simulation.html

https://bigtheta.io/2017/07/08/implementing-sph-in-2d.html

http://yuki-koyama.hatenablog.com/entry/2014/05/18/054909

http://yuki-koyama.hatenablog.com/entry/2014/08/17/001312

http://yuki-koyama.hatenablog.com/entry/2015/10/12/181318