webgl2.0でGPGPUする

# 概要

webgl2.0からtransformFeedbackが利用できるようになったので、GPUに計算させた結果をjavascript側に戻すことができます。
これを利用して、最近話題になっているGPGPUをすることができます。

まぁwebgl1.0の場合でもgl.ReadPixelsでfragmentShaderの計算結果を戻すことはできますけどね。

# 非常に簡単なサンプル

とりあえず簡単なサンプルはこんな感じです。

```index.html
<!DOCTYPE html>
<html style="width:100%;height:100%">
  <head>
    <meta charset="UTF-8" />
    <title>bsTest</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <div id="result"></div>
    <script type="text/vertex-shader" id="vs">#version 300 es
out float result;

void main() {
  result = 1.0 + 1.0;
}
    </script>
    <script type="text/fragment-shader" id="fs">#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
  outColor = vec4(1.0);
}
    </script>
    <script type="text/javascript">
window.onload = () => {
  var canvas = document.getElementById("canvas");
  var result = document.getElementById("result");

  var gl2 = canvas.getContext("webgl2");
  if(!gl2) {
    throw new Error("webgl2.0 is not supported.");
  }
  // vertex shader
  var vs = gl2.createShader(gl2.VERTEX_SHADER);
  gl2.shaderSource(vs, document.getElementById("vs").innerText);
  gl2.compileShader(vs);
  if(!gl2.getShaderParameter(vs, gl2.COMPILE_STATUS)) {
    throw new Error("failed to make shader.");
  }
  // fragment shader
  var fs =gl2.createShader(gl2.FRAGMENT_SHADER);
  gl2.shaderSource(fs, document.getElementById("fs").innerText);
  gl2.compileShader(fs);
  if(!gl2.getShaderParameter(fs, gl2.COMPILE_STATUS)) {
    throw new Error("failed to make shader.");
  }
  // programつくる
  var program = gl2.createProgram();
  gl2.attachShader(program, vs);
  gl2.attachShader(program, fs);
  gl2.transformFeedbackVaryings(program, ["result"], gl2.SEPARATE_ATTRIBS);
  gl2.linkProgram(program);

  if(!gl2.getProgramParameter(program, gl2.LINK_STATUS)) {
    throw new Error("failed to make program.");
  }
  gl2.useProgram(program);
  var vTransformFeedback = gl2.createBuffer();
  var transformFeedback = gl2.createTransformFeedback();
  gl2.bindBuffer(gl2.ARRAY_BUFFER, vTransformFeedback);
  gl2.bufferData(gl2.ARRAY_BUFFER, Float32Array.BYTES_PER_ELEMENT, gl2.DYNAMIC_COPY);
  gl2.bindBuffer(gl2.ARRAY_BUFFER, null);

  // あとは描画を実施する
  gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, transformFeedback);
  gl2.bindBufferBase(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, vTransformFeedback);
  gl2.beginTransformFeedback(gl2.POINTS);
  gl2.drawArrays(gl2.POINTS, 0, 1);
  gl2.endTransformFeedback();

  // 結果を取り出す
  var array = new Float32Array(1);
  gl2.getBufferSubData(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, array);
  console.log(array);
  result.innerText = "ok:" + array[0];
}
    </script>
  </body>
</html>
```

とりあえずこのコードでは、GPUに1.0 + 1.0を計算させてます。
もちろん答えは2.0になります。

# 解説

1. vertex演算のコードを作成し、GPUに託す
2. fragment演算のコードを作成し、GPUに託す
3. 情報データを準備してGPUに渡す
4. 実際の描画を実施する(普通はここで描画する)
5. 描画での計算結果をtransformFeedbackでjs側に戻す

という動作をします。

## vertex shader

```
    <script type="text/vertex-shader" id="vs">#version 300 es
out float result;

void main() {
  result = 1.0 + 1.0;
}
    </script>
```

main関数の中身が実施されるもの。
out float resultで出力で利用する変数を定義
ということをやってます。
今回は1.0 + 1.0を計算してますが

```
void main() {
  result = 2.0 * 3.0;
}
```

とかにすると2 x 3を計算させることができます。

## fragment shader

```
    <script type="text/fragment-shader" id="fs">#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
  outColor = vec4(1.0);
}
    </script>
```

実際の色を出力するときに利用するプログラムです。
一応定義しますが、今回は利用しないので、どうでもいいです。
とりあえずrgba = (1.0, 1.0, 1.0, 1.0);真っ白にしてあります。

## 下準備

```
  var canvas = document.getElementById("canvas");
  var result = document.getElementById("result");

  var gl2 = canvas.getContext("webgl2");
  if(!gl2) {
    throw new Error("webgl2.0 is not supported.");
  }
  // vertex shader
  var vs = gl2.createShader(gl2.VERTEX_SHADER);
  gl2.shaderSource(vs, document.getElementById("vs").innerText);
  gl2.compileShader(vs);
  if(!gl2.getShaderParameter(vs, gl2.COMPILE_STATUS)) {
    throw new Error("failed to make shader.");
  }
  // fragment shader
  var fs =gl2.createShader(gl2.FRAGMENT_SHADER);
  gl2.shaderSource(fs, document.getElementById("fs").innerText);
  gl2.compileShader(fs);
  if(!gl2.getShaderParameter(fs, gl2.COMPILE_STATUS)) {
    throw new Error("failed to make shader.");
  }
  // programつくる
  var program = gl2.createProgram();
  gl2.attachShader(program, vs);
  gl2.attachShader(program, fs);
  gl2.transformFeedbackVaryings(program, ["result"], gl2.SEPARATE_ATTRIBS);
  gl2.linkProgram(program);

  if(!gl2.getProgramParameter(program, gl2.LINK_STATUS)) {
    throw new Error("failed to make program.");
  }
  gl2.useProgram(program);
  var vTransformFeedback = gl2.createBuffer();
  var transformFeedback = gl2.createTransformFeedback();
  gl2.bindBuffer(gl2.ARRAY_BUFFER, vTransformFeedback);
  gl2.bufferData(gl2.ARRAY_BUFFER, Float32Array.BYTES_PER_ELEMENT, gl2.DYNAMIC_COPY);
  gl2.bindBuffer(gl2.ARRAY_BUFFER, null);
```

これらはwebgl2.0の下準備処理です。
コンテキストをcanvasから取得して
vertexとfragmentのshaderプログラムを作成 -> プログラムソース設定 -> コンパイル -> 処理確認として
動画プログラムを2つのshaderからつくって
transformFeedbackを準備するということをやってます。

## 実際に計算させる。

```
  // あとは描画を実施する
  gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, transformFeedback);
  gl2.bindBufferBase(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, vTransformFeedback);
  gl2.beginTransformFeedback(gl2.POINTS);
  gl2.drawArrays(gl2.POINTS, 0, 1);
  gl2.endTransformFeedback();

  // 結果を取り出す
  var array = new Float32Array(1);
  gl2.getBufferSubData(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, array);
  console.log(array);
  result.innerText = "ok:" + array[0];
```

普通のwebglの場合はrequestAnimationFrameやsetIntervalの処理の中で実施するwebglの描画処理の部分です。
今回はいきなりtransformFeedbackを発動させてdrawArraysで描画を1つの頂点についてのみ実施しています。

これでとりあえずGPUに計算を実施させてその結果をjavascript側(CPU側)で取得できました。