今回、エンジニアブロクを担当する小瀬です。
最近ではモバイルなどで使用されているGPUも高速化が進み、数百GFLOPSと一昔前では考えられないようなスペックのものも出てきています。GPUを使った技術として描画処理だけでなく、大量の計算を一度に行わせるような用途に使用することが出来ます。
こういったGPUに描画以外の処理をさせることを一般的にGPGPU(General-purpose computing on graphics processing units)と呼び、様々な分野で研究が行われています。
http://ja.wikipedia.org/wiki/GPGPU
ただし、GPUの特性上どんなものでも高速化出来る訳ではなく(工夫次第ではありますが)、上手く使いこなすには色々とテクニックが必要となります。
今回は、比較的簡単にGPU処理に落とし込めそうな反応拡散系(Reaction Diffusion system)と言った計算モデルをGPUで高速化させてみることにします。
■反応拡散系とは?
と、その前に反応拡散系について少し触れていこうかと思います。
参考:Wikipedia(英語版のほうが画像があるので多少イメージしやすいかも知れません)
http://ja.wikipedia.org/wiki/%E5%8F%8D%E5%BF%9C%E6%8B%A1%E6%95%A3%E7%B3%BB
http://en.wikipedia.org/wiki/Reaction%E2%80%93diffusion_system
反応拡散系についてWikiなどで調べてみても、上記のリンクように何だかよく分からない数式や理論が展開され、イマイチよく分からない感じです。反応拡散系と検索すると上位に非常に丁寧に解説してくれているサイトも出てきますが、それでもピンと来ないかもしれません。
という訳で、誤解を恐れずザックリと説明してしまうと「隣接したデータ(ピクセル)を参照して自身のパラメータを再算出し、一定の法則に基づいたパターンを導き出す手法」と言えるのかなと思います。
何故こんな計算をするのかと言うと、個人的に分かりやすかった例として熱帯魚などの模様が派手な魚の柄はどうやって決まるのか?という話がありました。表皮の細胞自体はどの部分でも同じ構造であり自身がどの位置にありどの色になるべきかを知らされているわけではないが、隣り合った細胞の情報を元に自身の色を決めているのではないか等、自然界の様々な現象をシミュレートするため等に使用される計算モデルだそうです。
ゲーム用途としては、テクスチャの自動生成や、自然物の分布計算などに使用できそうです。
■GPUでの高速化
では、早速この計算モデルをGPUで高速化させていきます。今回は「Gray-Scott model」という比較的ポピュラーな計算モデルを使用します。このモデルでは、2次元に展開されたデータの隣接する8マスの情報を参照しながら自身のデータを書き換えていきます。
GPUで高速化するには、2次元テクスチャに入力データを用意し、サンプリング座標を1ピクセルずらして周囲ピクセルをサンプリングし、それらの値から結果を求め、RenderTargetに結果を出力する形になります。
さらに、RenderTargetをダブルバッファにし、入力テクスチャと出力バッファを交互に切り替えながら、前回の結果を入力として受け取り新たな結果を出力するといったプロセスを繰り返します。(GPUに余裕があるなら、この部分を1フレーム内で更にループし一気に計算させます。今回はiPhone5Sを使用したため、処理にかなり余裕があったので毎フレーム64回計算しています)
シェーダ内部で行う計算としては以下のようになります。
#version 300 es precision highp float; in vec2 texcoord; layout (location = 0) out vec4 fragColor; uniform sampler2D buffer; // 入力テクスチャ uniform vec4 param; // 反応拡散系用パラメータ(x:f, y:k, z:ud, w:vd) uniform vec2 uvOffset[8]; // サンプリングポイントのオフセット値 void main() { // 自身の情報を取得 vec2 cp = texture(buffer, texcoord).xy; // 周囲8マスをサンプリング vec2 delta = vec2(0.0, 0.0); for( int i=0; i<8; ++i ) { vec2 duv = texcoord.xy + uvOffset[i]; delta = delta + texture(buffer, duv).xy; } delta = delta * 0.125f - cp; // パラメータを計算 cp.x = cp.x + cp.x * cp.x * cp.y - (param.x + param.y) * cp.x + param.z * delta.x; cp.y = cp.y -cp.x * cp.x * cp.y + param.x * (1.0 - cp.y) + param.w * delta.y; // 計算結果を出力 fragColor.xy = clamp(cp, 0.0, 1.0); fragColor.zw = vec2(0.0, 1.0); }
計算式としては非常にシンプルですが、これに渡すパラメータ( f, k, ud, vd ) の係数を変化させるだけで、同じ計算式であっても得られる結果が大きく異なります。実際にどのように変化するのか、動いている様子を動画で撮影してみました。
以前、同じものをCPUベースで作成したことがあるのですが、計算するグリッドのサイズも数倍に大きくなっているにもかかわらず、非常に高速に計算できています。(細かいチューニングはしていないものの、体感的には数十倍早くなっています。)
ただし、CPUで計算した場合と比べて若干結果が異なるものもありました。おそらく今回使用したRenderTargetが整数バッファ(256階調)のものを使用したため、計算の途中で精度落ちしてしまったのではないかなと思います。
この辺りはまだ調整の余地があるのかなと思いますが、GPUで高速化するといった目的は十分に達成できたのかなと思います。
モバイルのゲーム開発で積極的にGPGPUを使用するような機会はもうちょっと先の話かもしれませんが、高度なシミュレーションからPhotoshopの画像処理、検索エンジンの高速化など意外と近くで使われている技術なので、知っていて損はないのかなと思います。