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]とあるように、出力は128*128の配列である
つまり、この関数は128*128回実行されることになる
この値は自由に設定できる

せっかくなので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のすごさを体感するには十分だった

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.