0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-18

はじめに

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

コード全文

 パーティクルの個数は16384個です。控えめです。大きさも雑に調整してあります。
 p5 framebuffer gpgpu

// 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

バッファの準備

 まず点描画に使うバッファを準備します。

  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必須ですね。

  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は以下です。とてもシンプルにできています。

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;
}
frag
#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
  color = vData;
}

aIndexで描画位置を決めてそこに該当する位置にaDataを置いています。描画部分はこんな感じですね。

  // 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に更新結果がフィードバックされます。

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;
}
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の形で取得した後計算して戻していますね。おそらく先ほどの記事でも似たようなことをやっているはずです。処理の内容は端っこで反射させてるだけです。
 描画処理は以下ですが、注意すべきポイントがあります。

  // 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の記事でも同じことをしていますね。

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

display処理

 ベース描画を実行します。

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;
}
frag
#version 300 es
precision highp float;
out vec4 color;
void main(){
  color = vec4(vec3(0.1, 0.5, 0.7), 1.0);
}

 描画の内容的には、単色で出力してるだけです。

  // 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に点描画関連の処理が充実すれば、とは思いますが...誰か実装してくれるといいですね。ここまでお読みいただいてありがとうございました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?