フラグメントシェーダーで数値計算(マンデルブロ集合)
WebGLのGLSL ESでは、変数や演算子、各種関数を使った「数値計算」を行うことができます。型の自動変換が行われず制御構文にも一部制約があるなどやや窮屈ですが、C言語に近い感覚でシェーダーで行う演算処理を記述できるというのは、興味深いですね。
最近のGPUは膨大な演算機を備えているので、その計算能力を数値計算にも活用する「GPGPU」が注目されています。今回は、WebGLを通してそのGPGPUの真似事をしてみることにしましょう。
今回試してみる数値計算は、マンデルブロ集合の計算です(各座標における計算やピクセルに対する色成分の決定方式などはHTML5(JavaScript)で描くマンデルブロ集合を参照)。もっとも、計算結果はそのまま画面に描画するので、GPGPUというよりは、シミュレーション描画ですが……
マンデルブロ集合に必要なデータは、二つの数値(二次元座標)ですね。今回は、(-1.5, -1.0i)~(0.5, 1.0i)の範囲を描くことにします。
実際の計算はフラグメントシェーダーで行い、計算座標はバーテックスシェーダーからvarying変数で渡すことにしましょう。バーテックスシェーダーには、WebGLの描画領域一杯に正方形を描く頂点と共にマンデルブロ集合の描画座標を渡します。各頂点に(付加情報として)渡す座標の値をマンデルブロ集合の描画座標の四隅に対応させ、受け取った値はそのままvarying変数でフラグメントシェーダーに流すようにしました。これで、フラグメントシェーダーには、自動的に補間された中間点の座標が入力されてくるはずです。
WebGLの描画コマンドではWebGLの描画領域全体を覆う4つの頂点(-1, -1)(-1, 1)(1, -1)(1, 1)をパラメーター(attribute変数vPos)にTRIANGLE_STRIPで正方形を描きます。同時に各頂点に対応するマンデルブロ集合の計算(描画)領域(-1.5, -1)(-1.5, 1)(0.5, -1)(0.5, 1)もattribute変数pPsoとして渡すようにしました。
これで、バーテックスシェーダーではvPosをvec4型に拡張の上頂点として出力し、varing変数mPosにpPosの値をそのまま入れれば、フラグメントシェーダー側のmPosには頂点の間にあるピクセルに対して補間された値が入ってくるはずです。
フラグメントシェーダーでは、渡されたmPos(マンデルブロ集合の座標)を元に、以下のようなGLSL ELコードでピクセルの色を計算してみました。
varying mediump vec2 mPos;
void main() {
mediump vec2 z = vec2(0.0, 0.0);
mediump vec2 t = vec2(0.0, 0.0);
int n = 0;
for (int i = 0;i < 50;i++) {
t.x = (z.x * z.x) - (z.y * z.y) + mPos.x;
t.y = (z.x * z.y * 2.0) + mPos.y;
z = vec2(t);
if (length(z) > 2.0) {
break;
}
n++;
}
if (n == 50) {
gl_FragColor = vec4((mPos + vec2(1.5, 1.0)) / 2.0, 0.0, 1.0);
} else {
gl_FragColor = vec4(0.0, 0.0, float(n) / 50.0, 1.0);
}
}
このコードでは、50回計算を繰り返しても大きさが2を超なければマンデルブロ集合内と判定し現在の座標に応じて赤ー緑のグラデーション、集合外なら発散判定に至った計算回数に応じて青のグラデーションを描きます。
Mozilla Developer NetworkのWebGLに関するTIPSでは、モバイル機器対応のためフラグメントシェーダーでは精度としてhighpを指定せずmediumpを使え、と書いてあるので、mediumpにしてみましたが、今回みたいな用途ならそれなりの結果を得られるようですね。
もっともデータの入出力の問題、さらにWebGLのシェーダー処理能力(GPUの機能や性能)は、環境による差がCPU以上に大きいわけで、シェーダーによるGPGPU的な数値計算は使いどころが難しいかもしれません。JavaScriptで計算した方が、WebGL周りの初期化やGLSL ESコードのコンパイルなどのオーバーヘッドもなく安定した計算速度(と精度)を発揮できるでしょうし……