p5.jsのフレームバッファを使ってGPGPUで点描画を実行する

# はじめに
　p5.jsには現在、点描画に関するシェーダーなどの機構が整備されていません。そこに関しては自前の処理で補うこととして、今回はそれとp5.jsのframebufferを作成する関数：
　[createFramebuffer](https://p5js.org/reference/p5/createFramebuffer/)
を使って、いわゆるGPGPUをやってみようと思います。内容的には、いつもお世話になっております、wgldのこれ：
　[GPGPU でパーティクルを大量に描く](https://wgld.org/d/webgl/w083.html)
に相当するものですが、装飾は本質的でないので、単に反射させるだけにとどめます。

## コード全文
　パーティクルの個数は16384個です。控えめです。大きさも雑に調整してあります。
　[p5 framebuffer gpgpu](https://openprocessing.org/sketch/2491323)
```js
// VTFできましたね

// bindTextures()ですね
// baseMaterialShader()の方はこれをやってるんですよね
// 必須ですね

let fbo0, fbo1;
let dataBuf, indexBuf;
let updateShader;
let displayShader;
const TEX_SIZE = 128;

const inputVS =
`#version 300 es
in float aIndex;
in vec4 aData;
out vec4 vData;
uniform vec2 uSize;
void main(){
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  uv = 2.0*uv - 1.0;
  vData = aData;
  gl_Position = vec4(uv, 0.0, 1.0);
  gl_PointSize = 1.0;
}
`;

const inputFS =
`#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
  color = vData;
}
`;

const updateVS =
`#version 300 es
in float aIndex;
out vec2 vUv;
uniform vec2 uSize;
void main(){
  // 位置を取得して送るだけ
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  vUv = uv;
  // y座標は逆にする。お約束。
  gl_Position = vec4(uv.x*2.0-1.0, 1.0-uv.y*2.0, 0.0, 1.0);
  gl_PointSize = 1.0;
}
`;

const updateFS =
`#version 300 es
precision highp float;
in vec2 vUv;
out vec4 finalData;
uniform sampler2D uData;
void main(){
  vec4 data = texture(uData, vUv);
  vec2 pos = data.xy;
  vec2 vel = data.zw;
  if(pos.x + vel.x > 1.0 || pos.x + vel.x < -1.0) vel.x *= -1.0;
  if(pos.y + vel.y > 1.0 || pos.y + vel.y < -1.0) vel.y *= -1.0;
  pos += vel;
  finalData = vec4(pos, vel);
}
`;

const displayVS =
`#version 300 es
in float aIndex;
uniform sampler2D uTex;
uniform vec2 uSize;
void main(){
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  vec2 p = texture(uTex, uv).xy;
  gl_Position = vec4(p, 0.0, 1.0);
  gl_PointSize = 4.0;
}
`;

const displayFS =
`#version 300 es
precision highp float;
out vec4 color;
void main(){
  color = vec4(vec3(0.1, 0.5, 0.7), 1.0);
}
`;

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);

  const _gl = this._renderer;
  const gl = _gl.GL;

  const iArray = [];
  const fArray = [];
  for(let k=0; k<TEX_SIZE*TEX_SIZE; k++){
    iArray.push(k);
    const direction = random(TAU);
    const speedValue = random(0.003, 0.005);
    fArray.push(
      random(-0.95, 0.95), random(-0.95, 0.95), 
      speedValue*cos(direction),speedValue*sin(direction)
    );
  }

  indexBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(iArray), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  dataBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, dataBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(fArray), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  const inputShader = createShader(inputVS, inputFS);

  fbo0 = createFramebuffer({
    width:TEX_SIZE, height:TEX_SIZE, format:FLOAT, textureFiltering:NEAREST
  });
  fbo1 = createFramebuffer({
    width:TEX_SIZE, height:TEX_SIZE, format:FLOAT, textureFiltering:NEAREST
  });
  // これでflip-flopできるはずです。

  // input.
  fbo0.begin();
  shader(inputShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  inputShader.enableAttrib(inputShader.attributes.aIndex, 1);
  gl.bindBuffer(gl.ARRAY_BUFFER, dataBuf);
  inputShader.enableAttrib(inputShader.attributes.aData, 4);

  inputShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);
  _gl._setPointUniforms(inputShader);

  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);

  inputShader.unbindShader();

  fbo0.end();

  updateShader = createShader(updateVS, updateFS);
  displayShader = createShader(displayVS, displayFS);
}

function draw() {
  const _gl = this._renderer;
  const gl = _gl.GL;

  // update.
  fbo1.begin();
  shader(updateShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  updateShader.enableAttrib(updateShader.attributes.aIndex, 1);
  updateShader.setUniform("uData", fbo0.color);
  updateShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);

  updateShader.bindTextures();
  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
  updateShader.unbindShader();

  fbo1.end();

  // flip-flop.
  const tmp = fbo1;
  fbo1 = fbo0;
  fbo0 = tmp;

  // display.
  shader(displayShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  displayShader.enableAttrib(displayShader.attributes.aIndex, 1);

  displayShader.setUniform("uTex", fbo0.color);
  displayShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);

  // これに相当する処理でfillとstrokeはやってるんですよね。bindTextures()を。
  _gl._setPointUniforms(displayShader);
  // でもpointはやってないんですよね。それで失敗するわけですね。

  // blend処理はupdateにも影響するので、描画時のみ有効化し使ったらオフにする
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE); 
  background(0);
  displayShader.bindTextures();
  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
  displayShader.unbindShader();
  gl.disable(gl.BLEND);

  // performance check.
  if(frameCount%10===0)console.log(frameRate());
}
```
実行結果：

![wrfefegrg55555.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/b27a2b9d-1a57-0995-968f-488a0a003001.png)

## バッファの準備
　まず点描画に使うバッファを準備します。
```js
  const iArray = [];
  const fArray = [];
  for(let k=0; k<TEX_SIZE*TEX_SIZE; k++){
    iArray.push(k);
    const direction = random(TAU);
    const speedValue = random(0.003, 0.005);
    fArray.push(
      random(-0.95, 0.95), random(-0.95, 0.95), 
      speedValue*cos(direction),speedValue*sin(direction)
    );
  }

  indexBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(iArray), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  dataBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, dataBuf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(fArray), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
```
dataBufの方はデータの入力用です。indexBufの方はupdateなどでも使います。上記のh_doxasさんの記事でindex単体のattributeを使ってると思いますがこれがそれに相当します。なおvec4の成分のうちxyが位置、zwが速度を表現しており、speedValueとかいろいろ書いてあるところでそれを入力しています。

## フレームバッファの準備と初期データの格納
　次に、2枚のfboを準備します。データ入力の点描画なのでNEAREST必須ですね。
```js
  fbo0 = createFramebuffer({
    width:TEX_SIZE, height:TEX_SIZE, format:FLOAT, textureFiltering:NEAREST
  });
  fbo1 = createFramebuffer({
    width:TEX_SIZE, height:TEX_SIZE, format:FLOAT, textureFiltering:NEAREST
  });
```
inputするためのshaderは以下です。とてもシンプルにできています。
```glsl:vert
#version 300 es
in float aIndex;
in vec4 aData;
out vec4 vData;
uniform vec2 uSize;
void main(){
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  uv = 2.0*uv - 1.0;
  vData = aData;
  gl_Position = vec4(uv, 0.0, 1.0);
  gl_PointSize = 1.0;
}
```
```glsl:frag
#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
  color = vData;
}
```
aIndexで描画位置を決めてそこに該当する位置にaDataを置いています。描画部分はこんな感じですね。
```js
  // input.
  fbo0.begin();
  shader(inputShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  inputShader.enableAttrib(inputShader.attributes.aIndex, 1);
  gl.bindBuffer(gl.ARRAY_BUFFER, dataBuf);
  inputShader.enableAttrib(inputShader.attributes.aData, 4);

  inputShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);
  _gl._setPointUniforms(inputShader);

  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);

  inputShader.unbindShader();

  fbo0.end();
```
fbo0の方に焼きます。あとでfbo1に焼いてフリップ、とかするので。attributeの準備などはこっちでやらないといけないので、それをまずバッファ変数で実行しています。最後にダイレクトドローコールで描画しています。unbindShader()はおまじないのようなものです。最後にfbo0.end()で戻しておきます。これでfbo0に初期データが書き込まれました。

## update処理
　位置と速度の更新は、まずfbo0の内容を元に点描画でfbo1に更新結果を焼き、次いでfbo0とfbo1の役割を入れ替えます。これでfbo0に更新結果がフィードバックされます。
```glsl:vert
#version 300 es
in float aIndex;
out vec2 vUv;
uniform vec2 uSize;
void main(){
  // 位置を取得して送るだけ
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  vUv = uv;
  // y座標は逆にする。お約束。
  gl_Position = vec4(uv.x*2.0-1.0, 1.0-uv.y*2.0, 0.0, 1.0);
  gl_PointSize = 1.0;
}
```
```glsl:frag
#version 300 es
precision highp float;
in vec2 vUv;
out vec4 finalData;
uniform sampler2D uData;
void main(){
  vec4 data = texture(uData, vUv);
  vec2 pos = data.xy;
  vec2 vel = data.zw;
  if(pos.x + vel.x > 1.0 || pos.x + vel.x < -1.0) vel.x *= -1.0;
  if(pos.y + vel.y > 1.0 || pos.y + vel.y < -1.0) vel.y *= -1.0;
  pos += vel;
  finalData = vec4(pos, vel);
}
```
　書き込む先がframebufferなので、vertで位置を決める際に y 座標を逆にしておきます。データ入力なのでgl_PointSize=1.0;を忘れずに。fragでは位置と速度をxy,zwの形で取得した後計算して戻していますね。おそらく先ほどの記事でも似たようなことをやっているはずです。処理の内容は端っこで反射させてるだけです。
　描画処理は以下ですが、注意すべきポイントがあります。
```js
  // update.
  fbo1.begin();
  shader(updateShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  updateShader.enableAttrib(updateShader.attributes.aIndex, 1);
  updateShader.setUniform("uData", fbo0.color);
  updateShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);

  updateShader.bindTextures();
  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
  updateShader.unbindShader();

  fbo1.end();
```
　ドローコールの前にbindTextures()を実行していますね。これがないとfbo0.colorの結果を用いることができません。box()やsphere()ではこれをやっているのですが今回はダイレクト描画なので、そこら辺を補う必要があるわけです。なお、indexBufを再利用していますね。
　処理が終わったらfbo0とfbo1をフリップします。wgldの記事でも同じことをしていますね。
```js
  // flip-flop.
  const tmp = fbo1;
  fbo1 = fbo0;
  fbo0 = tmp;
```

## display処理
　ベース描画を実行します。
```glsl:vert
#version 300 es
in float aIndex;
uniform sampler2D uTex;
uniform vec2 uSize;
void main(){
  float x = mod(aIndex, uSize.x);
  float y = floor(aIndex/uSize.y);
  vec2 uv = (vec2(x,y)+0.5)/uSize;
  vec2 p = texture(uTex, uv).xy;
  gl_Position = vec4(p, 0.0, 1.0);
  gl_PointSize = 4.0;
}
```
```glsl:frag
#version 300 es
precision highp float;
out vec4 color;
void main(){
  color = vec4(vec3(0.1, 0.5, 0.7), 1.0);
}
```
　描画の内容的には、単色で出力してるだけです。
```js
  // display.
  shader(displayShader);

  gl.bindBuffer(gl.ARRAY_BUFFER, indexBuf);
  displayShader.enableAttrib(displayShader.attributes.aIndex, 1);

  displayShader.setUniform("uTex", fbo0.color);
  displayShader.setUniform("uSize",[TEX_SIZE, TEX_SIZE]);

  // これに相当する処理でfillとstrokeはやってるんですよね。bindTextures()を。
  _gl._setPointUniforms(displayShader);
  // でもpointはやってないんですよね。それで失敗するわけですね。

  // blend処理はupdateにも影響するので、描画時のみ有効化し使ったらオフにする
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE); 
  background(0);
  displayShader.bindTextures();
  gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
  displayShader.unbindShader();
  gl.disable(gl.BLEND);
```
　ブレンド処理ですが、一応雑にADDでやっています。このブレンド処理はupdateにも影響してしまうので、実行した後でオフに戻しています。fbo0の内容を使っているのでbindTextures()は必須です。indexBufをまたまた使っています。indexにアクセスするためです。

# おわりに
　p5.jsに点描画関連の処理が充実すれば、とは思いますが...誰か実装してくれるといいですね。ここまでお読みいただいてありがとうございました。