TFO

TFFのsetupを簡略化

 前回は初めての逐次更新でしたので、小細工無しでそのままやりました。しかしいちいちアトポンするのは面倒ですね...スロットが増えてきた場合、VAOで簡略化したくなりますね。実は、bindBufferBase関連の処理も、VAOのように記録装置を使って簡略化できるんです。それがTFO:Transform Feedback Objectです。wgldのサイトではなんか初期化時にでてきてそのままノータッチで忘れ去られるあれです。前回述べたように主にコードの整理が目的で導入されるものですが、それなりに役に立ちます。ぼちぼち進めていきましょうか。

コード全文

作品39のリンク

interleavedにする

 先のことを考えてinterleavedに移行します。TFFのスロット4つしかないんで。それで、位置と速度を分けます。コードを読みやすくするためです。アトリビュートは16個もスロットがあります。ぜいたくに使いましょう。もっとも、バッファ内の配置には何の変更も無い(そもそも位置と速度を順に並べて使っている)ので、バッファの初期化部分は完全に同じです。変わるのはシェーダーの方です。

#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aVelocity;
out vec2 vPosition;
out vec2 vVelocity;
void main(){
  vec2 p = aPosition;
  vec2 v = aVelocity;
  if(p.x + v.x < -1.0 || p.x + v.x > 1.0){ v.x *= -1.0; }
  if(p.y + v.y < -1.0 || p.y + v.y > 1.0){ v.y *= -1.0; }
  p += v;
  vPosition = p;
  vVelocity = v;
}

 フラグメントシェーダは一緒なので略です。分けただけなので特に問題ないですね。プログラムはseparateをfalseにしてinterleavedです。その結果も特に変わりません。ただもっと増やそうってなった時に追加しやすいのでやはりinterleavedの方がいいですね。

  // interleavedしましょう
  const pgUpdate = createShaderProgram(gl, {
    vs:vsUpdate, fs:fsUpdate, outVaryings:["vPosition", "vVelocity"], separate:false
  });

VAOとTFO

 さて、こうしてスロットが増えてくると役に立つのがVAOです。今回のコードは前回とほぼ同じです。バッファも2つ用意しています。パーティクルは1024個に増やしました(400x400という縛りの都合上、制限しています)。なので、それらに応じてそのバッファに対するVAOを用意します。ついでにTFOを用意します。まずは雰囲気を感じてください。

  const vaos = [];
  const tfos = [];
  for(let i=0; i<2; i++){
    // vao作ります
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    gl.bindBuffer(gl.ARRAY_BUFFER, bufs[i]);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
    gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    gl.enableVertexAttribArray(0);
    gl.enableVertexAttribArray(1);
    gl.bindVertexArray(null);
    // tfo作ります
    const tfo = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfo);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufs[1-i]); // bufのもう片方
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    vaos.push(vao);
    tfos.push(tfo);
  }

 VAOサイドは、該当するバッファをバインドしてインターリーブの要領で0番と1番に位置と速度を供給しています。これが分かんない場合はアトリビュートの章で復習してください。それで、そのあとでenableもしています。前回は普通に0を開けっ放しにしましたが、後々のことを考えるとVAOでカプセル化した方がいいですね。そのあとですが、しれっとgl.createTransformFeedbackを実行しています。これにより、Transform Feedback Objectが作られます。これをVAOみたいにして、gl.bindTransformFeedbackという関数でバインドしています。なぜターゲットが必要なのかは聞かないでください。まあWebGLの闇ってやつです...それで、すぐヌルバしてますからその間に実行する処理が重要なわけですが、bindBufferBaseでいつものようにさっきと違う方のバッファをbufferBaseのTFF枠にセットしていますね。これは禁忌を避けるためなので当然です。しかしTFOの役割とは一体何でしょうか...。

TFOの役割

 実は、TFOがバインドされている間、bindBufferBaseとbindBufferRangeをターゲットがTFFの状態で実行した場合、それらはglobal stateのbufferBaseではなくbind中のTFOを参照する仕組みになっています。また、逆にbufferBaseのTFF枠を使う際も、もし仮にTFOがバインドされているなら、その時はbind中のTFOを参照する仕組みになっています。要はTFF枠の記録装置ですね。完全にセーブされ、ロードされます。それでVAOと同じような扱いが為されています。
 なおしれっとbindBufferRangeに言及しましたが、はっきりいって使う機会があんまないのと、検証が面倒なので、特に何もしないことにします。雰囲気だけ感じてください。すべてのスロットの状態が記録されるので、separateの方が恩恵を感じやすいかもしれませんが、今回はinterleavedです。バッファは1つだけ、Rangeもいじりません。それでもこれを用意する理由はTFFサイドだけ直接バッファを使うと不整合だからです。どっちも記録装置を使った方がコードがすっきりするという私の個人的な都合でこのようにしています。コードは主観で書くものですから、好きにやります。

アップデート処理

 そういうわけで、VAOとTFOを用いた更新処理を書きましょう。

  // bufIndexのbufをotherBufに書き込む処理を用意する感じですね
  const update = () => {
    gl.useProgram(pgUpdate);

    // 要はsetup関数なんですよ。簡潔でしょ?
    gl.bindVertexArray(vaos[bufIndex]);
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfos[bufIndex]);

    gl.beginTransformFeedback(gl.POINTS);
    gl.enable(gl.RASTERIZER_DISCARD);
    gl.drawArrays(gl.POINTS, 0, 1024);
    gl.disable(gl.RASTERIZER_DISCARD);
    gl.endTransformFeedback();

    gl.bindVertexArray(null);
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

    bufIndex = 1-bufIndex;
  }

 実行順は仕様を理解するうえで重要です。TFF枠にバッファを置くのもbeginTransformFeedbackを実行するのもいずれも描画準備ですから、どっちが先ということは無いんですが、このように書いた方がすっきりするのは確かですね。ラスタライザはカットして、POINTSで1024個描いて終わりです。あっさりしたもんです。最後に前回紹介しなかった恒常ループでも挙げておきます。

  let resetFlag = false;
  const loop = () => {
    update();
    draw();
    window.requestAnimationFrame(loop);
    if(resetFlag){
      dataReset(bufs[bufIndex]);
      resetFlag = false;
    }
  }

  loop();

 なお前回説明しなかったんですが、このresetFlagというのはguiで実行できて、クリックすると配置がリセットされます。遊んでみてください。今回はこれで以上です。

TFOを正しく使ってるサイト

 TFFの利点が何かといえば、頂点単位でGPUで並列でデータを更新できることと、コンピュートシェーダっぽいことができることですが、後者は詳しくないのでスルーします。前者ですが、次のサイトみたいなことができるわけです。
 TFFとインスタンシング
 このサイトがTFOを正しく使ってないのはおいておくとして...大量の球を描画しています。最後のところで「インスタンシングオンリーでも充分速いね」って言っていますが、じゃあTFF使わないで30000個の球の位置と速度をどうやって更新しているかというとバッファ操作による動的更新です。結局運動処理の内容が単純なので、動的更新で足りる範囲なわけです。30000個程度ではTFFと大差ないのでほとんど描画負荷の問題になっています。とんだとばっちりですね...運動処理がより複雑になれば、TFFにとっては屁でもないですから、TFFに軍配が上がるはずです。TFFと速度を競うのであればフロートテクスチャスワップを出すべきでしょう。CPU処理の動的更新などお呼びではありません。
 さっき上げたサイトではTFOをバインドする処理のあとでbindBufferBaseを使ってTFF枠において、なんか実行して、んで解除していますね...つまり仕事してないですね。このサイトと、あとこっちもそうですね:
 Transform Feedback で GPGPU
 そもそもTFOが登場していない...加えてスロット3個、色も使っていてぜいたくですね。まあこれを見てもわかるように、TFOが無くてもTFFはできるんです。それでもTFOを(以下略)。

 TFOを正しく使ってるサイトもあります。ちゃんと。例えば面白法人カヤックさんの次のサイトは正しいです:
 【WebGL2】GPU Instancing x Transform Feedback で大量のインスタンスの計算と描画をGPUで行う
 あとこれですね。2017年、WebGL2が世に出たころに作られたと思われるWebGL2のサンプル集です。こっちが正しく使ってるのに何でこんなことになっているのか...
 transform_feedback_interleaved.html
 そんなわけで、TFOを正しく使いましょうというお話でした。要らないならそもそも作らないのがコードを書く上でのマナーです。

次回予告

 そういうわけで次回はインスタンシングです。いつまでも点描画してるわけにもいかないので。点描画は個数を増やす方向性です。インスタンシングはどうしても体積があるのでたくさん増やすことにあまり意味があるわけではないですが、間違いなく役に立つ選択肢ですから、しっかり押さえていきましょう。

今回登場した関数

createTransformFeedback
bindTransformFeedback