フィードバックループ
逐次更新だ!
TFFといえば逐次更新です。wgldのサイトでもそのような例が紹介されていますからね。前回はTFFの基本的な使い方を学ぶためにgl_VertexIDでしかドローコールを実行しませんでしたが、ここからは応用編なので、堂々とアトリビュートを使います。そうです、2つのバッファを用意して、片方のバッファをアトリビュートとしてセットし、もう片方のバッファをbufferBaseにおいて、アトリビュートサイドの情報を元に計算した結果をbufferBaseの方に落とします。え?同じバッファを両方に用意した方が楽?それはTFFの禁忌なのでできないんです...まあ詳しくはおいおい説明しようかと思います。
なにはともあれ、TFFの一番楽しい話題ですから、楽しく学びましょう。
コード全文
コードの内容
点描画です。64個のパーティクルを動かしています。位置を更新するのはどうしているかというと、実はこの程度なら動的更新でやった方が手っ取り早いです。しかし勉強のためにTFFで更新しています。まあ大変なんですが、学習なので...。
最初に用意しているbuf0というのがその位置データの入ったバッファです。位置と速度のためにvec4としています。つまり点ごとのデータ量は16バイトで、それが64個分です。実は動的更新は初期化時にやっています。しかし更新は基本的にTFFで行ないますから、usageはWebGLサイドから頻繁に更新するDYNAMIC_COPYです。初期化は点の位置を均等に配置したうえで、適当に速度を決めています。速度に従って点が動き、壁で反射しながら動く仕組みですね。
const buf0 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.bufferData(gl.ARRAY_BUFFER, 16*64, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 今回は少ないので動的更新で
const dataReset = (buf) => {
const data = new Float32Array(4*64);
let offset = 0;
for(let y=-7/8; y<1; y+=1/4){
for(let x=-7/8; x<1; x+=1/4){
data[offset++] = x;
data[offset++] = y;
const v = 0.008 + Math.random()*0.02;
const a = Math.random()*Math.PI*2;
data[offset++] = v*Math.cos(a);
data[offset++] = v*Math.sin(a);
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
dataReset(buf0);
先に描画部分を見てみます。これは1-6でやった点描画です。
#version 300 es
layout (location = 0) in vec2 aPos;
uniform float uDPR;
void main(){
gl_Position = vec4(aPos, 0.0, 1.0);
gl_PointSize = 16.0*uDPR;
}
#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
vec2 p = gl_PointCoord;
p -= vec2(0.5, 0.5);
p *= 2.0;
float alpha = 1.0 - length(p);
fragColor = vec4(1.0) * alpha;
}
PointSizeはDPRを考慮することでスマホなどでも同じサイズになるようにしています。フラグメントシェーダの処理は1-6でやったものと同じなので省略します。これをblendで実行しています。
const draw = () => {
const curBuf = bufs[bufIndex];
gl.bindBuffer(gl.ARRAY_BUFFER, curBuf);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(pgDraw);
uniformX(gl, pgDraw, "1f", "uDPR", DPR);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.POINTS, 0, 64);
gl.disable(gl.BLEND);
gl.flush();
}
curBufというのが今現在扱っているバッファという認識で問題ないです。後で説明します...今回VAOは使わないので、その場でアトポンしてます。オフセットを使うことで位置データだけ用いていますね。この辺分かるでしょうか...まあ問題ないですね。みっちりやったので。それで、ブレンドやuniformをやっています。blendもどっかできちんと扱えたらと思います。でもまあこれも1-6で説明したアルファブレンド以外はほとんど使わないので(せいぜいONE-ONEくらい)、気になった時に自分で調べたらいいかと思います。
さて、bufs[bufIndex]となっていますね。そうです、実は先ほど用意したのと同じサイズのバッファがもうひとつ、あります。それらの間でデータのやり取りをしています。普通だったら一つだけデータがあって、その内容を元にそのデータを更新できればいいはずですが、なぜそのような不思議なことをしているのでしょうか...?
更新処理とTFFの禁忌
先に更新処理の詳細を見てみます。TFFのコードです。アトリビュートとして用意しているのが入力データで、アウトプットが出力データですね。
#version 300 es
layout (location = 0) in vec4 aData;
out vec4 vData;
void main(){
vec2 pos = aData.xy;
vec2 vel = aData.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;
vData = vec4(pos, vel);
}
#version 300 es
void main(){}
プログラムは次のようになります。バリイングは1つで、かつ16バイト以下なので単純にseparateとしています。バリイングが1つであってもたとえば行列のmat4とかの場合はinterleavedでなければならないのは前回説明した通りです。
const pgUpdate = createShaderProgram(gl, {vs:vsUpdate, fs:fsUpdate, outVaryings:["vData"]});
例によってフラグメントの方は死んでいます。更新なので。それで、成分を取って位置と速度とし、反射処理、そのあとで位置に速度を足していますね。これで終了ですね。この処理で、入力に使うアトリビュートをさっきのbuf0とし、出力もbuf0とする素直なコードを書こうとすると、更新処理はこのようになりますね:
const update = () => {
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(pgUpdate);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf0);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, 64);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
}
特に問題なさそうに見えます。入力に使うデータは入力の際にデータとして処理され、処理が終わったら元のデータにそのまま上書きされる。めでたしめでたし。ところがそうは問屋が卸さないんですね。これをやるとエラーを食らい不履行となります。
GL_INVALID_OPERATION: glDrawArrays: A transform feedback buffer that would be written to is also bound to a non-transform-feedback target, which would cause undefined behavior.
なんか前節からエラーばっかり紹介していますが、TFFにしろテクスチャにしろフレームバッファにしろ、扱いを間違えると容易に変なエラーを出されるので、慣れた方が理解が早くなります。恐れず立ち向かいましょう。
この場合ですが、実はbufferBaseのTFF枠に置いたバッファと同じバッファをアトリビュートサイドに置くのは禁忌なのです。ちなみにプログラムで使われる必要は無くて、別スロットに存在していてもエラーを食らいます。スロットがdisableでも関係ありません。どこにも存在してはいけません。
const update = () => {
// this_is_not_buf0.
gl.bindBuffer(gl.ARRAY_BUFFER, this_is_not_buf0);
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 3は有効でなくかつプログラム外。それでもダメ
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.vertexAttribPointer(3, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(pgUpdate);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf0);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, 64);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
}
予期せぬエラーを防ぐための最善策です。それゆえ、必然的に2つ以上のバッファを用意して、互いに互いを書き換えてとっかえひっかえするしかないわけですね。このコードではそうしています。
// 逐次更新だが...どうしようね?
const buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, 16*64, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
let bufIndex = 0;
const bufs = [buf0, buf1];
こんな風にもうひとつ用意して、bufIndexを0と1の間で行ったり来たりさせます。ようやく正しいupdate関数の紹介ができます。
const update = () => {
const curBuf = bufs[bufIndex];
const otherBuf = bufs[1-bufIndex];
gl.bindBuffer(gl.ARRAY_BUFFER, curBuf);
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(pgUpdate);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, otherBuf);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, 64);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
bufIndex = 1-bufIndex;
}
同じスロットに交代で違うバッファがセットされ、もう片方がbufferBaseのTFF枠にセットされます。これらは常に重複しないので問題は起きず、また最後にindexをスワップしているので、描画時は更新結果のバッファがcurになります。それで上記のdraw処理に行くわけですね。これですっきりしました。解説は以上です。
TFOは?
wgldのサイトに出てくるようなTransform Feedback Objectは今回登場しませんでした。実はこのサイトのTFFのコードでTFOを活用しているものはありません。TFOは必要ないのでしょうか。実はこれがあるとTFFのコードが若干書きやすくなります。ただ、次以降のコードではそれをあまり実感できないかもしれません。なぜならinterleavedはバッファを1つしか使わないからです。実は次以降ではアトリビュートを増やす関係でinterleavedに移行します。バッファが1つしかない場合、わざわざTFOを用意するのは手間です。それでもあった方がすっきりします。それはVAOとセットで使うからです。TFOはVAOと役割が似ているので、両方使うことでコードがすっきりして可読性が向上します。そのための仕組みです。おたのしみに。