OpenGLはいろいろなプラットフォームで使えますが、その中でもWebGLはブラウザ上のJavaScriptでササっと動作確認できるので非常に便利です。
今回はこれを使って、自分の勉強がてらGPGPUの入門&体験ができるような記事をまとめてみました。
なおWebGLについては wgld.org が大変強力です。これだけ豊富な内容が無料かつ日本語で読めるのは神のようなサイトです! ここにはお世話になりました。
#GPGPUとは#
GPUはグラフィックを扱うハードウェア... というくらいの理解は皆さん持っていると思います。これを使ってグラフィック以外の汎用計算に活用しようというのがGPGPU (General Purpose GPU) ですが、GPUの特徴をひとことで言うと SIMD (Single Instruction Multiple Data)という並列処理機構になります。
並列処理というと代表的なものはマルチスレッドですが、この場合それぞれのスレッドが全く別の処理をすることができます。スレッドAがマウスやキーボードの入力を処理し、スレッドBがネットワークの非同期処理を担当し、スレッドCが動画の再生をする、みたいなことはみなさんのPCでも日常的に行われていますね。物理的な処理ユニットはCPUの「コア」ですが、最近のPCでは4とか8のコア数が多い状況で、ハイエンドなXeonでは20程度のコアが利用できます。(実際には、OSがコアを使いまわして数十~数百のスレッドを疑似的に同時実行します)
これに対しGPUでは、「数千」の処理ユニットを搭載しており、CPUとは2桁違います。これだけ見るとスゲー!と思いますが、SIMDなので、「Single Instruction」なのです。数学でいえば何かの関数 f(x) があって、f(1)~f(1000)の1000個を並行に計算する、というイメージです。"f"自体は全体で共通で、そうホイホイ変更することはできません。処理は共通だが入力が違う、というタイプの並列計算になります。
昔だとやはりグラフィックくらいしか主要な適用分野がなかったのですが、最近ディープラーニングというGPGPUに超うってつけのネタが登場したので注目度は高まりました。
#シェーダー言語#
この共通の**"関数f"を記述するのがシェーダー言語**です。最初に出力結果を確認したほうが理解しやすいと思うので見ておきましょう。
10段階のグレースケールの四角(ここではユニットと呼ぶことにします)が並んでますが、この画像を出力しているシェーダーはこうなります。
<script id="fs1" type="x-shader/x-fragment">
precision mediump float;
varying vec2 index; // 0~9の10段階の値を受け取る
uniform float count; //全体で共通で、値 10 が格納されている
void main(void){
float c = index.x / count; // 0~9の値が0.0~0.9の10段階になる
gl_FragColor = vec4(c,c,c,1.0); // 色を決定
}
</script>
WebGLの場合、コイツをいきなりscriptタグでHTMLの中に埋め込んでしまいます。type="x-shader/x-fragment" となっていると、ブラウザはこのスクリプトはJavaScriptではない、と認識するだけで終わってしまいますが、別の場所のJavaScriptがこれを読んでシェーダーとしてコンパイルし、GPUに転送して実行します。
シェーダー言語は1つの立派なプログラム言語で、どのプラットフォームでOpenGLを使うにしても避けて通れません。
- Cに似た構文。forループやifの分岐もあるし、独自の関数定義もできる。
- プリミティブ型として、intやfloatはもちろん、4次元までのベクトルや行列も持っている。
といった特徴がありますが、なんといってもSIMDなので、10個のユニットを並行して描画している様子を想像してほしいと思います。(実際には並列数は10ではないですし、この前段の頂点シェーダのステップもあるのですが、話を単純にするため最初は10個の並列、という理解でOKと思います)
このシェーダをもう少し詳しく解説します。
varying vec2 index;
これはユニットの一つ一つについて受け取ってくるパラメータで、vec2というのは2次元ベクトルの型です。今は1方向のグラデーションなので、x要素のみ使用しています。varyingというキーワードが見慣れませんが、最初はSIMDの"MD"部分、と理解してください。ユニットごとに異なる値です。
具体的にどうやってユニットごとの値を渡してやるのか、は長くなるので大胆に省略しますが、末尾に実際に動作するHTMLページへのリンクをつけますので、興味のある方はその中のJavaScriptをじっくり読んでください。
次の行にいきます。
uniform float count;
これもシェーダーの入力引数で、プログラマが指定できる値ですが、全ユニットで共通です。ユニット間では共通ですが、描画が1回完全に終われば次回の描画時にセットしなおせるので、時間とともにuniform変数をゆるやかに変化させてやればイカしたアニメーションも簡単に作れます。ここでは、ユニットの個数"10"を受け取る役割にしています。
float c = index.x / count;
gl_FragColor = vec4(c,c,c,1.0);
最後のここのmain関数本体です。gl_FragColorは色を出力するための特殊な変数です。これはvec4型(4次元ベクトル)で、RGBAの順で色を指定し、各要素は0.0~1.0の値です。vec4(c,c,c,1.0) なのでRGBの値は等しくグレースケールになります。vec4(c,0,0,1.0)なら赤くなりますし、vec4(c,c,c,0.5)なら半透明になるので別の画像と重ねることもできます。
入力値がどう渡ってくるのかの説明は省略していますが、そのあとの部分はSIMDの処理がイメージできるのではないかと思いますがどうでしょうか?
#if分岐#
<script id="fs2" type="x-shader/x-fragment">
precision mediump float;
varying vec2 index;
uniform float count;
void main(void){
float c = index.x / count;
if(mod(index.x, 2.0) == 0.0)
gl_FragColor = vec4(c,0,0,1.0);
else
gl_FragColor = vec4(0,0,c,1.0);
}
</script>
次はコイツです。if文で mod(index.x, 2.0) の条件で分岐して、vec4(c,0,0,1.0)かvec4(0,0,c,1.0)を出力しています。これで予想できると思いますが、index.xの偶数・奇数によって赤か青かを出すわけです。剰余計算はmod関数を使い、C言語の % ではないことに注意してください。
これを実行するとこのような画像になります。
WebGLのいいところは、こうやって修正結果をすぐグラフィックで確認できるところですね。PCでもスマホでもいけますし、本当にすばらしい。
#2次元ベクトルを扱う#
<script id="fs3" type="x-shader/x-fragment">
precision mediump float;
varying vec2 index;
uniform float count;
void main(void){
gl_FragColor = vec4(index.x / count, 0, index.y / count, 1.0);
}
</script>
せっかく2次元ベクトルを受け取っているのでXYの両方の要素を使ってみましょう。x座標で赤の要素が変化、y座標で青の要素が変化、としています。Gは0, Aは1で固定です。なので出力結果はこうなります。
だんだん雰囲気がわかってきたでしょうか?
#ユニット数が多くても大丈夫#
<script id="fs4" type="x-shader/x-fragment">
precision mediump float;
varying vec2 index;
uniform float count;
uniform float timer;
void main(void){
float cx = mod(index.x / count + timer*0.01, 2.0);
float cy = mod(index.y / count + timer*0.01, 2.0);
cx = cx>1.0? 2.0-cx : cx;
cy = cy>1.0? 2.0-cy : cy;
gl_FragColor = vec4(cx,0, cy,1.0);
}
</script>
今度はuniform変数timerを使ってアニメーションをやります。最初に2.0で割った余りを求めて0~2.0の値を得て、そこから0->1->0->1と連続的変化するような値を得てから色を決定します。
さらに調子にのって、描画のユニット数をなんと50*50の2500個にしてしまいます。CPUで一つ一つ描いていたらさすがに重くなりそうですが、SIMDの威力をもってすればこの程度は全く問題になりません。
Qiitaの記事だと静止画しか載せられませんが、こちらにはちゃんとアニメーションするデモをつけています。
#GPGPUといいつつ画像を出しているだけじゃんか#
その質問はごもっともです。ただ、結果を目に見えるように出力するには画像にするのが手っ取り早かっただけのことです。実際には、画像のふりをしたメモリ上の一定の領域へシェーダーに書き出してもらい、後でそこに何が描いてあるかを調べるという手順になります。画像のレンダリング機構を借用して目的の計算をするわけですが、ユニットごとの出力が32ビットで収まらないようなケースでどうするのか、とかまだ調べ切れていないところはいくつかあります。
#デモ#
上記4種の実際に動くHTML+JavaScriptを 用意しました。
四角形の列を定義するところも含め、特に外部のライブラリ等を使うことなく素直に書いたので、丁寧に読めば全容を把握できると思います。少ない行数でかなり高度なことができますね!
#おわりに#
WebGLというと、ブラウザ上で3Dをやるための技術と思われがちですが、WebGLでの2Dグラフィックを先にやった方がGPUの動作イメージやSIMDの練習にはぴったりだと思います。面白いですよ!