穴日記

どうだ明るくなったろう

WebGLでリアルタイム流体シミュレーション+レンダリングを実装してみた

はじめに

なんかWebGLが流行ってるらしいのでWebGLすることにしてみました。OpenGLはぼんやりとやったことがあったのですが、ウェッブ技術に対する疎さが深刻化しているのでモダンで先端的なテクノロジーを追及するためにもWebGLの習得は急務といえました。

WebGLJavaScriptと呼ばれるプログラミング言語を用いるようです。僕がJavaScriptを最後に書いたのは四年くらい前にクック●●●のハッカソン?みたいなのに参加した時その場で習得してその場でアプリを作ったとき以来です。それ以来一度も書いていません。

まあJavaScriptとか意識高い大学生でも書けるわけだし適当にAPIを呼び散らかす分には何の問題もないでしょう。というわけでWebGLのサンプルコードを読みながらサンプルドリブンでJavaScriptを習得すんのがいいだろと思いました。

先に結果だけ貼っておきます。いろいろあって流体シミュレーションしつつレンダリングするようなものになりました。
githole/webglSmoke · GitHub

サンプルドリブンプログラミング

とりあえずWebGLのサンプルコードが乗ってるサイトを探しました。WebGLは流行りなだけあって無限にそういうサイトは存在します。こういう時はGoogleで上の方に出てくるサイト=高品質なサイト、というヒューリスティックを実行します。今回参考にしたのは以下の二つのサイトです。

Learning WebGL

wgld.org

前者は英語、後者日本語です。両方のサイトの最初の方のページをブワーって読んでローカルにコピーして実行したりしてみます。適当にコードを書き換えたりするうちに概ねWebGLの初期化の方法を覚えます。

この段階で、WebGLの初期化にさえ至ればあとは慣れ親しんだOpenGLとなんら変わらないという悟りを得ます。こうなればしめたものです。OpenGLAPIを呼ぶのはそう難しいことではありません。GPUにデータを送って、GPUにドローコールを積むだけです。

一つ、面倒なことがあるとすれば行列まわりの演算です。GPUプログラミングなんで、CPUで行列演算してGPUに頂点送ってシェーダーセットしてドローコール呼ぶだけなんですが、その行列演算が驚くべき面倒さです。今回はglMatrixを使いました。なぜか、最新版は4x4行列の逆行列の計算関数が実装されてないっぽく、過去のバージョンから関数を引っ張ってきたりしています。実は実装されてた、みたいな落ちだったら笑えますね。

2D流体シミュレーション

そもそも、「ああ、ブラウザで流体シミュレーションを見たいなあ」と思ったのが今回ウェッブに手を出すきっかけだったので流体シミュレーションを実装します。2Dの流体シミュレーション(WebGL)は無限にあるのですが、3Dは全然ありません。まったく気合いの足りないことです。

というわけで、とりあえず2Dの流体シミュレーションを実装→3Dのシミュレーションに拡張、という手順を踏むことにします。

2Dの流体シミュレーションは、特に頭を使いたくなかったので、Jos Stam先生のStable FluidsをそのままGLSLに移植しました。ググればいくらでも情報が出てくるでしょう。基本的に、ナビエ・ストークス方程式を離散化して解いてるだけですが、移流の解決のためにセミラグランジュ法を使っているのでいい感じに安定になります。

安定なのはいいんですけど、セミラグランジュ法はウンコみたいに数値拡散が起こってしおしおになります。よって、今回はナビエ・ストークス方程式の拡散項は無視してます。数値拡散だけでもやばいのにまともに拡散項なんて解いている場合ではありません。この方法はBridson先生も推奨していたような気がします。まあ移流スキームをもうちょい賢い感じにすればいいんですけど、今回の本旨じゃない気がしたのでこの辺で終わりにします。

さて、WebGLで流体シミュレーションみたいなGPGPUじみたことを行うにはどうすればよいか?という話です。WebCLみたいなのもあるみたいですがとりあえずWebGLだけ使うとすると、レンダーターゲットを二枚用意してピンポンするのが一番です。

このようにすると、二次元の離散化格子はそのままテクスチャの各テクセルに対応するのでシミュレーションも直感的に実装できます。境界条件をちゃんと設定する必要はありますが、どうせWebGLのテクスチャボーダーの処理は二種類くらいしかないので適当に指定します。

テクスチャフォーマットは、RGBAのFLOATテクスチャにしました。これは、本当は拡張機能なのですが、さすがにUnsinged Byteのフツーの8ビットテクスチャだと精度が全然足りないし、パックとかアンパックを組み合わせ始めると泥沼式に面倒さが増していくのであきらめて拡張機能を使いました。

3D流体シミュレーション

というわけで、GLSLへの移植と結果の確認自体はまあすぐできました。やるだけですね。問題はここからです。WebGLは先進的なテクノロジーにも関わらず3Dテクスチャをサポートしてないらしいのです。そしたらどうやって3Dシミュレーションすればいいのでしょう。

まあ実はそんな大したことはありません。3Dテクスチャを2Dテクスチャ上に展開してマッピングしてやればいいのです。シミュレーション空間が64x64x64だとすると、512x512の2Dテクスチャを確保して、その上に64x64のタイルを縦横8枚ずつならべれば64x64x64相当のテクスチャとみなすことができます。

この手の処理は大概テクスチャ境界が面倒になるものですが、FLOATテクスチャは最近傍フィルタしか使えないのでどうにでもなります。

そしたら、あとは2Dシミュレーションを3Dに拡張しつつテクスチャサンプリング部分を自前でアドレッシングするようにするだけです。ヤッター!

レンダリング

シミュレーションが首尾よくできたら最後にレンダリングです。これが結構面倒です。ボリュームレンダリングの手法は無数にあります。

しかし、最初考えてた方法(光源方向からシャドウボリュームテクスチャを生成していい感じにやる)は、WebGLがどうやらマルチレンダーターゲットに対応していないらしいので破綻しました。まあ対応してたところで、イマイチなんですけど。

しかたないので普通にカメラからレイマーチング+光源にも各サンプル点からレイマーチングという方法にしました。この方法だと、サンプリング数がO(N^2)(Nはサンプル数)みたいな感じになりあまり筋がよくないのですが、プライマリのレンダーターゲットの解像度を半分にしてみたりきちんとAABBで枝刈りしたりみたいなことをしていったら結構速度が出るようになったのでそれでよし!としました。

高速化とか

シミュレーションステップの順番を細かく変えたり、圧力項を解くためにガウスザイデルする際、前フレームの結果を使い回りしたり、みたいな涙ぐましい最適化をします。まあまあ速くなります。

おわり

まあなんとかシミュもレンダリングもできました。dat.gui.jsとかStats.jsとか、便利ライブラリを突っ込むとそれっぽくなってかっこよくなります。組み込みも簡単です。

だいたい、一日ちょっとくらいで絵が出て、あとは細かい調整って感じでしたし、どんどんWebGLすればいいんじゃないでしょうか。

シミュレーションを放置しておくと割と爆発したりするんですが、これは色々シミュレーションを雑にやってるせいです。TODOってことで。

githole/webglSmoke · GitHub