GPGPUでライフゲームをやってみる
GPGPUとは
General-purpose computing on graphics processing units の略らしい
要は、本来なら画像処理に用いられるGPUを汎用計算(特に並列処理)に用いようという考え
WebGL2.0を使えば、面倒なソフトウェアも必要なしにGPGPUが試せる
gpu.jsとは
詳しくはgpu.js - GPU Accelerated JavaScriptを参照
要は、WebGLでGLSLの知識なしに汎用計算ができるライブラリ
特に難しい記法でもない
HTMLなら
<script src="https://cdn.rawgit.com/gpujs/gpu.js/45103b7f154a12ec639990c6896b21a053eb6a6c/bin/gpu.js"></script>
Node.jsの場合は
npm install gpu.js --save
で利用できる
ライフゲームを作る
詳しい記法はほかの記事を見るかGitHub - gpu.jsを参照のこと
ここでは必要なことのみ解説する
オブジェクトを初期化する
この時、引数にcanvasエレメントを指定してやると、後で描画ができる
const gpu = new GPU({canvas:[エレメント]});
次に、gpu.createKearnel(function,option)
で、GPU内で実行する関数を定義する
普通のjavascriptのように書けるが、内部でこの関数はいったんフラグメントシェーダに変換されるので、いろいろと制約がある模様
例えばアロー関数は使えないとかネイティブ関数でも数学系以外は使えないなど
Supported-Math-Functions
このライフゲームは上下左右は絶対に死んでいる
const life = gpu.createKernel(function(board) {
//現在位置とワールドの大きさ、集計用の変数
const x = this.thread.x;
const y = this.thread.y;
const l = this.constants.size;
let sum = 0;
//盤面の端なら絶対死亡
if(x==0||y==0||x==l-1||y==l-1){
return 0;
}
//周囲八マスの生きているセルを集計
sum = board[y-1][x-1] + board[y-1][x ] + board[y-1][x+1] +
board[y ][x-1] + board[y ][x+1] +
board[y+1][x-1] + board[y+1][x ] + board[y+1][x+1];
//ルールに照らし合わせて生死を判断(1が生、0が死)
if(board[y][x]==1&&(sum==2||sum==3)){
return 1;
}
if(board[y][x]==0&&sum==3){
return 1;
}
return 0;
}, {
constants: { size: 128 },
output: [128,128]
});
第二引数にあるconstants
には、関数内で使う定数を指定できる
output:[128,128]
とあるように、出力は128128の配列である
つまり、この関数は128128回実行されることになる
この値は自由に設定できる
せっかくなのでcanvasに出力する用の関数も作る
const render = gpu.createKernel(function(board) {
this.color(
board[this.thread.y][this.thread.x],
board[this.thread.y][this.thread.x],
board[this.thread.y][this.thread.x],
1);
}, {
output: [128,128],
graphical:true
});
graphical:true
にしたうえでthis.color(r,g,b,a)
を使って色を指定する
出力と描画を一緒の関数にすることはできないらしい(?)
あとは普通の関数のように使える
output = life(board);
graphical:true
にした関数は出力がないので
render(board);
これだけで、初期化時に指定したcanvasに描画される
全コード
//初期化
const gpu = new GPU({canvas:[エレメント]});
const life = gpu.createKernel(function(board) {
//現在位置とワールドの大きさ、集計用の変数
const x = this.thread.x;
const y = this.thread.y;
const l = this.constants.size;
let sum = 0;
//盤面の端なら絶対死亡
if(x==0||y==0||x==l-1||y==l-1){
return 0;
}
//周囲八マスの生きているセルを集計
sum = board[y-1][x-1] + board[y-1][x ] + board[y-1][x+1] +
board[y ][x-1] + board[y ][x+1] +
board[y+1][x-1] + board[y+1][x ] + board[y+1][x+1];
//ルールに照らし合わせて生死を判断(1が生、0が死)
if(board[y][x]==1&&(sum==2||sum==3)){
return 1;
}
if(board[y][x]==0&&sum==3){
return 1;
}
return 0;
}, {
constants: { size: 128 },
output: [128,128]
});
//描画用
const render = gpu.createKernel(function(board) {
this.color(
board[this.thread.y][this.thread.x],
board[this.thread.y][this.thread.x],
board[this.thread.y][this.thread.x],
1);
}, {
output: [128,128],
graphical:true
});
//盤面を作る
let board = [];
for (let i = 0; i < 128; i++) {
let p = [];
for (let j = 0; j < 128; j++) {
p.push(Math.round(Math.random()));
}
board.push(p);
}
//fps60で計算と描画
var fps = 60;
setInterval(function(){
board = life(board);
render(board);
},1000/fps);
触ってみて
かなり扱いやすい
WebGLをゼロから書くよりも、面倒な処理がない
何よりjavascriptそのままの記法で関数が書けるというのは、GLSLをあまり触ったことのないものにとっては素晴らしい利点である
ただし、このライフゲームでは、GPU本来の速度は出せていなかった
GPU以外の部分でボトルネックがあったのだろうと推測される
ともかく、GPGPUのすごさを体感するには十分だった