はじめに
p5.jsには現在、点描画をサポートする仕組みが無いので、自前の処理でやっています。ここではwgldさんの次のサイトにあるような方法で、位置をリアルタイムで更新しながら点を描画しようと思います。
VBOを逐次更新しながら描画する
コード全文
短いです。10000個の頂点を位置を毎フレーム更新しながら描画するだけです。
dynamic update
let buf;
let displayShader;
const TEX_SIZE = 100;
const NUM = TEX_SIZE*TEX_SIZE;
const positions = new Float32Array(2*NUM);
const displayVS =
`#version 300 es
in vec2 aPosition;
void main(){
gl_Position = vec4(aPosition, 0.0, 1.0);
gl_PointSize = 4.0;
}
`;
const displayFS =
`#version 300 es
precision highp float;
out vec4 color;
void main(){
color = vec4(0.3, 0.5, 0.7, 1.0);
}
`;
function setup() {
createCanvas(800, 800, WEBGL);
pixelDensity(1);
const _gl = this._renderer;
const gl = _gl.GL;
buf = gl.createBuffer();
for(let k=0; k<NUM; k++){
const x = ((k%TEX_SIZE))/TEX_SIZE;
const y = floor(k/TEX_SIZE)/TEX_SIZE;
positions[2*k] = x*2-1;
positions[2*k+1] = y*2-1;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
displayShader = createShader(displayVS, displayFS);
}
function draw() {
background(0);
const _gl = this._renderer;
const gl = _gl.GL;
dynamicUpdate();
shader(displayShader);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
displayShader.enableAttrib(displayShader.attributes.aPosition, 2);
_gl._setPointUniforms(displayShader);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
displayShader.bindTextures();
gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
displayShader.unbindShader();
gl.disable(gl.BLEND);
gl.flush();
}
function dynamicUpdate(){
const _gl = this._renderer;
const gl = _gl.GL;
for(let k=0; k<NUM; k++){
const x = ((k%TEX_SIZE))/TEX_SIZE;
const y = floor(k/TEX_SIZE)/TEX_SIZE;
const radius = 0.2*noise(0,0,k);
const t = millis()/(500+1000*noise(k,k,k));
positions[2*k] = (2*x-1) + radius*cos(noise(k,0)*100 + t);
positions[2*k+1] = (2*y-1) + radius*sin(noise(0,k)*100 + t);
}
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
点データの用意
今回は動的更新です。毎フレームバッファの中身を書き換えるので、DYNAMIC_DRAWを指定します。なお、更新には型付配列が必要なので、今回はグローバルでその配列を用意しています。
buf = gl.createBuffer();
for(let k=0; k<NUM; k++){
const x = ((k%TEX_SIZE))/TEX_SIZE;
const y = floor(k/TEX_SIZE)/TEX_SIZE;
positions[2*k] = x*2-1;
positions[2*k+1] = y*2-1;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
シェーダーについては正規化デバイス座標で直接位置を決めています。今回は2次元なのでカメラは必要ないです。
#version 300 es
in vec2 aPosition;
void main(){
gl_Position = vec4(aPosition, 0.0, 1.0);
gl_PointSize = 4.0;
}
#version 300 es
precision highp float;
out vec4 color;
void main(){
color = vec4(0.3, 0.5, 0.7, 1.0);
}
点の色は点ごとに変えたりできますし、uniformで変えたりできます。その辺は自由に改変してみてください。
動的更新
更新するには、更新したいバッファをバインドして、bufferSubDataを使います。バイト単位でオフセットを指定するのですが、全部まるごと変える場合は0でOKです。
function dynamicUpdate(){
const _gl = this._renderer;
const gl = _gl.GL;
for(let k=0; k<NUM; k++){
const x = ((k%TEX_SIZE))/TEX_SIZE;
const y = floor(k/TEX_SIZE)/TEX_SIZE;
const radius = 0.2*noise(0,0,k);
const t = millis()/(500+1000*noise(k,k,k));
positions[2*k] = (2*x-1) + radius*cos(noise(k,0)*100 + t);
positions[2*k+1] = (2*y-1) + radius*sin(noise(0,k)*100 + t);
}
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
三角関数で雑に位置を決めています。正規化デバイス座標は-1~1なので、それを踏まえたうえで自由に位置を決めることができます。色など、追加でアトリビュートを用意してそっちも更新したいなら、シェーダーなどいろいろ準備して同じようにやればできます。
描画
描画についてはenableAttribをいつものように使います。今回はvec2なのでサイズに2を指定します。
shader(displayShader);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
displayShader.enableAttrib(displayShader.attributes.aPosition, 2);
_gl._setPointUniforms(displayShader);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
displayShader.bindTextures();
gl.drawArrays(gl.POINTS, 0, TEX_SIZE*TEX_SIZE);
displayShader.unbindShader();
gl.disable(gl.BLEND);
gl.flush();
textureを使う場合はbindTexturesを使うんですが今回は要らないですね。しかし使う癖を付けておくといいので入れました。ブレンドは単純にADDです。これで終わりです。
負荷(描画速度)について
当然ですが、CPUで更新しているので、更新処理で複雑なことをすればするほど重くなります。このコードの場合自分のスマホでは10fps前後になってしまいました。並列で出来る場合はシェーダーで更新した方が圧倒的に速いですね。あくまで並列で出来ない場合のための手法と考えるべきです。あるいは、インタラクションで部分的に更新する場合に使ったりもできます(その場合はオフセットの指定の仕方などを工夫する必要があります)。
おわりに
これをやるために必要なのは、点のみのジオメトリーの作成機構、点描画用shaderのカスタマイズ、点描画用の描画機構ですが、いずれもp5.jsは現在サポートしていません。今後の(以下略)。
ここまでお読みいただいてありがとうございました。