はじめに
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());
}
実行結果:
バッファの準備
まず点描画に使うバッファを準備します。
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は以下です。とてもシンプルにできています。
#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;
}
#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に更新結果がフィードバックされます。
#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;
}
#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処理
ベース描画を実行します。
#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;
}
#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に点描画関連の処理が充実すれば、とは思いますが...誰か実装してくれるといいですね。ここまでお読みいただいてありがとうございました。