TransformFeedback基礎

TFFだ!

 なぜトランスフォームフィードバックの略称が「TFF」なのかというと、TFだとトランスフォームで使われ過ぎているし、TFFBだと長すぎるからです。ちなみに自分以外でこの略称を使ってる人はおそらくいません。無駄な話は以上です。

 トランスフォームフィードバックです。仰々しい名前が付いていますが、名前は置いておいて何をしているのか確かめましょう。名前が仰々しいからって思考停止したり無駄に持ち上げたり動揺したりする人が世の中にはいますが、まずは中身を自分の目で確かめて判断して頭を使うってことをしてください。それをしないと何にも始まりません。

 ざっくりいうと、glslでWebGLBufferに書き込みをする処理です。つまりプログラムは実行するんですがドローコールを実行するとは限らないのが特徴の一つです。主な目的はWebGLBufferへの書き込みですが、描画をしてもOKです。ただし単純な命令しかできません。他の枠組みではいわゆるコンピュートシェーダとして扱われているものなので、中途半端な機能ではあるんですが、使いどころ次第ですね...。
 前置きは以上です。早速始めましょう!

コード全文

作品37のリンク

何がフィードバックなのか

 いずれ3Dでやると思いますが(4-11でちらっと出てきたんですが)、モデル変換というのがあって、バーテックスシェーダで頂点の位置を変更します。これが「トランスフォーム」です。その結果はドローコールに基づいてラスタライズされフラグメントシェーダで色を決めて...と扱われていくので、その場限りで、結果が保存されることはありません。しかしトランスフォームフィードバックでは、その結果がバッファに書き戻されるわけです。フィードバックですね。ただし、使用中のバッファに書き戻すのはNGというルールがあるので、ほとんどの場合代わりの入れ物を用意してとっかえひっかえすることになりますが...。

 とはいえ、まずはバッファへの書き戻し処理を学ぶことにしましょう。基礎は大事です。

展望台(global state)

 この章のテーマは「bufferBase」です。global stateの該当する部分を見てみましょう。

  globalState ={
    currentProgram:null,
    bindingFramebuffer:null,
    bindingBuffers:{
      arrayBuffer:null,
      elementArrayBuffer:null,
      uniformBuffer:null, // UBOで使うかも
      transformFeerbackBuffer:null, // 多分使わない
      pixelPackBuffer:null,
      pixelUnpackBuffer:null, ...
    },
    bindingVertexArrayObject:null,
    vertexAttributeArray:[
      {
        enable:false, arrayBuffer:null, layout:{
          size:4, type:gl.FLOAT, normalize:false, stride:0, offset:0
        }, divisor:0,
        current:DEFAULT_VERTEX_ATTRIB
      },{},{},...
    ],
    bindingTransformFeedbackObject:null, // TFO. そのうち出てくる
    bufferBase:{ // 今回の主役!
      transformFeedback:[ // 今回はこっちですね
        {buffer:null, offset:0, size:0}, {}, {}, ...
      ],
      uniform:[ // いずれ紹介できるかも
        {buffer:null, offset:0, size:0}, {}, {}, ...
      ]
    },
    etc...
  }

 bufferBaseはバッファ置き場です。見ての通りTFFサイドとUBOサイドがあります。それぞれ容量が決まっています(機種依存)。おいおい説明します。まずはTFFが何をするのか説明しましょう。

シェーダーの説明

 まず書き込みを実行するにはバッファを用意しなくてはいけません。シェーダーから説明した方が早いので、バーテックスシェーダを見ましょう。

#version 300 es
out vec2 vPosition; // 8バイト*10
out float vFish; // 4バイト*10
out vec3 vColor; // 12バイト*10
void main(){
  float i = float(gl_VertexID);
  vPosition = vec2(2.0*i, 2.0*i+1.0);
  vFish = -(i+1.0)*20.0;
  vColor = vec3(9.0*i, 9.0*i+1.0, 9.0*i+2.0);
}

 vPositionはvec2で、vFishはfloatで、vColorはvec3です。それぞれ8バイト、4バイト、12バイトです。特に意味はありません。バッファへの書き込みが目的なので。それで、POINTSで0から9まで走らせて、indexごとに走ります。main関数でこれらのv変数に代入していますが、これがドローコールのindexごとの書き込みに相当します。順番に入っていきます。つまり全部で80バイト、40バイト、120バイトの領域が必要になります。このあとgl.POINTSのドローコールでこれを実行するんですが、gl.PointSizeの記述がないですね。それだと描画が実行されないですね。しかし今回描画は実行しないので問題ありません。というのもフラグメントシェーダが...

#version 300 es
void main(){}

こんなだからです。なんとprecisionすらない。描画をしないので。今回はバッファへの書き込みしかしないからですね。これもおいおい説明していきます。まずバッファを用意しましょう。

separateとinterleaved

 バッファを用意しましょう。

  // 受け皿を作る
  const createBuf = (b) => {
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, b, gl.DYNAMIC_COPY);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    return buf;
  }
  const buf0 = createBuf(80);
  const buf1 = createBuf(40);
  const buf2 = createBuf(120);
  const bufAll = createBuf(80+40+120);

 80バイト、40バイト、120バイトの領域です。前回やった「領域のサイズ指定」です。基本的に0かなんかで初期化されます。それらとは別に、すべてのバイト数を合計した領域も用意しました。どういうことかというと、ここでcommon.jsを見ます。実は、プログラムがバッファへの書き込みを実行できるようになるためにはとある処理が必要です。それを紹介しましょう。

  setAttributeLayout(gl, program, layout);

  // outVaryingsの設定はアタッチしてからリンクするまでの間に実行する
  setOutVaryings(gl, program, outVaryings, separate);

  gl.linkProgram(program);
// TFF用の設定箇所
function setOutVaryings(gl, pg, outVaryings = [], separate = true){
  if(outVaryings.length === 0) return;
  gl.transformFeedbackVaryings(pg, outVaryings, (separate ? gl.SEPARATE_ATTRIBS : gl.INTERLEAVED_ATTRIBS));
}

 アタッチからリンクまでの間に、transformFeedbackVaryingsという仰々しい名前の関数を実行します。引数はプログラムとvarying配列と(名前の配列)、separateもしくはinterleavedの指定です。ここにあるような定数を使います。

 この後で、それぞれのバッファを別々のところにセットしてSEPARATEで書き込みを実行した後で、合計サイズのバッファを0番にセットしてINTERLEAVEDで書き込みを実行します。つまりそれぞれやります。まあとりあえずやってみましょうか。

やってみようTFF

 先ほど説明したような仕組みを実装するにはcreateShaderProgramでVaryings配列を指定します。こんな風に:

  // 両方作りましょう
  const pg_separate = createShaderProgram(gl, {
    vs, fs, outVaryings:["vPosition", "vFish", "vColor"], separate:true
  });
  const pg_interleaved = createShaderProgram(gl, {
    vs, fs, outVaryings:["vPosition", "vFish", "vColor"], separate:false
  });

 アトリビュートの時のような面倒な問題は発生しません。実は、シェーダーの宣言順や登場順がどうであれ、ここで指定した配列における順番が、そのままそれらのvaryingの位置となります。まあ後発仕様ですから、いろいろと考えられているのでしょう...というわけで、それぞれ見て行きます。

SEPARATEの場合

 bufferBaseのTFF枠にセットするにはbindBufferBaseという名前の関数を使います。

  // bufferBaseのTFF枠にセット
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf0);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, buf1);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, buf2);

 同じ関数をUBOでも使うので、きちんとターゲットをgl.TRANSFORM_FEEDBACK_BUFFERに指定します。あとindexとbufferを指定します。割り当ての関数の引数にバッファが登場しています。アトポンよりわかりやすいと思いましたか?今はそう思っておいてください(意味深)。それで、これで0,1,2番にバッファがセットされます。0,1,2はそれぞれvPosition,vFish,vColor(宣言順は無関係)なので、それらに対応するデータが格納されていきます。データを格納しましょう。

  gl.beginTransformFeedback(gl.POINTS);
  gl.enable(gl.RASTERIZER_DISCARD);
  // ドローコールはPOINTS,LINES,TRIANGLESのみでArraysのみ(Elements不可)
  // インスタンシング?知らない
  // 特に種類は関係なく、またPOINTSでもgl_PointSizeの指定は...描画の場合は要る。
  gl.drawArrays(gl.POINTS, 0, 10);
  gl.disable(gl.RASTERIZER_DISCARD);
  gl.endTransformFeedback();

 データの書き込みの場合POINTS以外の選択肢は基本的にあり得ないでしょうね...LINESとTRIANGLESも一応使えるんですが用途が分かりません。それで、実行するにはgl.beginTransformFeedback(mode)gl.endTransformFeedback()で囲います。内部で実行するドローコールはこれと同一でなければなりません。その前後でdisableとenableしているのはgl.RASTERIZER_DISCARDという機能で、ラスタライズを無効化する処理を一時的に有効化しています。無効化を有効化...なんだかややこしいですが、して当たり前のラスタライズという処理をしないようにするのですから、無効化する処理に意味があると考えるのは妥当ですね。これにより、フラグメントシェーダが骨抜きでもおとがめなしになります。まあ使わないですからね。

 (補足:何が言いたいかというと、もちろん他のフェッチでも各々のindexに対して処理されるんですが、たとえばTRIANGLESの場合、点の個数が3の倍数でないと余りについては処理されません。またLINESも奇数の場合末尾はスルーです。POINTSだけがすべてのindexでフェッチします。なので書き込み前提の場合、他のドローコールは役割が存在しません。加えてtransformFeedbackではエレメント描画がNGなので特定indexのフェッチもできません。連番オンリーです。補足は以上です。)

 おわったらbufferBaseからバッファを外しておきます。

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, null);

 これで終わったので、さっそく中身を見てみましょう。

  // 確認
  const showBuf = (buf, target = gl.ARRAY_BUFFER) => {
    gl.bindBuffer(target, buf);
    const byteLength = gl.getBufferParameter(target, gl.BUFFER_SIZE);
    const dw = new DataView(new ArrayBuffer(byteLength));
    gl.getBufferSubData(target, 0, dw);
    let s = "";
    for(let k=0; k<byteLength; k+=4){
      const f = dw.getFloat32(k, true);
      s += `${f}`;
      if(k<byteLength-4){ s += `, `; }
    }
    console.log(s);
    gl.bindBuffer(target, null);
  }

  // separateの場合はそれぞれ別々
  showBuf(buf0); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
  showBuf(buf1); // -20, -40, -60, -80, -100, -120, -140, -160, -180, -200
  showBuf(buf2); // 0, 1, 2, 9, 10, 11, 18, 19, 20, 27, 28, 29, 36, 37, 38, 45, 46, 47, 54, 55, 56, 63, 64, 65, 72, 73, 74, 81, 82, 83

 前回紹介したgetBufferSubDataで中身をFloatの列という形で取得しています。DataViewで取得してフェッチ...まあ普通にFloat32でいいんですが、なんとなくこうしました。targetですが、後で説明しますが...とりあえずARRAY_BUFFERでいいです。無事中身を取得できました。本当に順繰りに0から9に対するフェッチの結果が入っていますね...素晴らしい。初めてのTFF, 成功です。

INTERLEAVEDの場合

 INTERLEAVEDの場合は0番しか使いません。もうめんどうなのでコードをすべて書いてしまいましょう。わかるでしょうか。

  // 次にinterleaved
  gl.useProgram(pg_interleaved);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufAll);

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

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

 これでbufAllにすべて入ります。outVaryingsで指定した通りの順番に、0ごと、1ごと...と言った感じで放り込まれます。オールインワンです。内容:

  showBuf(bufAll); // 0, 1, -20, 0, 1, 2, 2, 3, -40, 9, 10, 11, 4, 5, -60, 18, 19, 20, 6, 7, -80, 27, 28, 29, 8, 9, -100, 36, 37, 38, 10, 11, -120, 45, 46, 47, 12, 13, -140, 54, 55, 56, 14, 15, -160, 63, 64, 65, 16, 17, -180, 72, 73, 74, 18, 19, -200, 81, 82, 83

 インターリーブの意味が分かりましたね。これでTFFの使い方についての説明は一通り終了です。お疲れ様でした。以降、いくつか補足が続きます。

スロットの限界など

 はじめに、スロットの限界など、各種パラメータについて補足します。先に述べておくと機種依存です(一部)。公開スケッチで左下になんか出ていますね。Chromeの場合こんな風です:

MAX_TFF_SEPARATE_ATTR: 4
MAX_TFF_SEPARATE_COMP: 4
MAX_TFF_INTERLEAVED_COMP: 120
MAX_UNIFORM_BUFFER_BINDINGS: 24

 略称ですが、長ったらしいのでTRAN...をTFFで、COMPONENTSをCOMPで置き換えてるだけです。内容は次の通り:

 無関係なのが混じっていますが気にしないでください。Chromeではこうでした。実は私のAndroidではこうです:

MAX_TFF_SEPARATE_ATTR: 4
MAX_TFF_SEPARATE_COMP: 4
MAX_TFF_INTERLEAVED_COMP: 128
MAX_UNIFORM_BUFFER_BINDINGS: 72

 interleavedの限界が128でした。まあ2べきの方がキリがいいですからね。スマートなスマホらしい実装です。

 内容について...まずSEPARATE_ATTRというのは書いた通りスロット数の限界です。なのでseparateで5つ以上バッファを用意するとエラーになります。

#version 300 es
out vec2 vPosition; // 8バイト*10
out float vFish; // 4バイト*10
out vec3 vColor; // 12バイト*10
out float vFurseal; // 4バイト*10
out float vSealion; // 4バイト*10
void main(){
  float i = float(gl_VertexID);
  vPosition = vec2(2.0*i, 2.0*i+1.0);
  vFish = -(i+1.0)*20.0;
  vColor = vec3(9.0*i, 9.0*i+1.0, 9.0*i+2.0);
  vFurseal = i*100.0;
  vSealion = i*(-100.0);
}
  const pg_separate = createShaderProgram(gl, {
    vs, fs, outVaryings:["vPosition", "vFish", "vColor", "vFurseal", "vSealion"], separate:true
  });
INVALID_VALUE: transformFeedbackVaryings: too many varyings

 なんとそもそもコンパイルが通りません。さらにbindBufferBaseで4以上の値を指定するとエラーになります。

INVALID_VALUE: bindBufferBase: index out of range

 なんとも少ないですね...しかしそこでインターリーブです。インターリーブの場合、普通にコンパイルが通り、先ほど説明したようにoutVaryings配列の順番通りにすべてのデータが入ります。

 SEPARATE_COMPというのは要するにseparateで宣言できるデータ型は16バイト以下でないといけないということです。vec4までですがmat2もぎりぎりセーフです。なおmat2の場合分かれたりはせず、行ベースの成分がそのまま並びます。mat2x3やmat4を指定するとエラーを食らいます。

Transform feedback varying vMat components (16) exceed the maximum separate components (4).

 もちろんinterleavedならエラーは出ません。mat4でもmat3x2でも何でもありです。それで限界ですが...これがINTERLEAVED_COMPですね。Chromeでは120となっているこれは、フェッチごとのバイト数/4の限界です。つまり480バイトが限界。mat4は64バイトですから、これが7.5個分です。たとえば次のようなコードを書いても問題なく順繰りに入ります。

#version 300 es
// 並び替えてもVaryings配列の並び順が優先される
out mat4 vMat0;
out mat4 vMat1;
out mat4 vMat4;
out mat2x4 vSealion;
out mat4 vMat5;
out mat4 vMat2;
out mat4 vMat3;
out mat4 vMat6;
//out float vOver; // つまりこれがアウト。
void main(){
  float i = float(gl_VertexID);
  vMat0 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat1 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat2 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat3 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat4 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat5 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vMat6 = mat4((i+1.0)*10.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,16.0);
  vSealion = mat2x4(i*99.0, -i*99.0, i*330.0, -i*330.0, i*55.0, -i*55.0, i*908.0, -i*908.0);
  //vOver = 6.666;
}
  const pg_interleaved = createShaderProgram(gl, {
    vs, fs, outVaryings:["vMat0", "vMat1", "vMat2", "vMat3", "vMat4", "vMat5", "vMat6", "vSealion"], separate:false
  });

 ひねくれて宣言順をいじっていますが、attributeとちがってvaryings配列での宣言順が正義ですから、そのように格納されます。これが限界です(120の場合)。ここにあるようにvOverを追加するとエラーを食らいます。

Transform feedback varying total components (121) exceed the maximum interleaved components (120).

 しかし私のスマホは128が限界ですからまだまだいけるのでしょう、いずれにせよ、機種の限界に挑戦するようなコードはあまり書くべきではないですね。

バッファ操作する際の注意

 先ほどバッファ操作、そのうちのgetBufferSubDataですが、これを実行する際、そのバッファはbufferBaseのTFF枠には置いてありませんでした。置いてあるとどうなるのでしょう。実は次のようなエラーが発生し、不履行となります。

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, buf1);
  showBuf(buf1);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
INVALID_OPERATION: blink::WebGL2RenderingContextBase::getBufferSubData: buffer is bound to an indexed transform feedback binding point and some other binding point

 このように、bufferBaseのTFF枠に入れた状態でARRAY_BUFFERにバインドし、その状態でバッファ操作の関数を実行しようとするとエラーになります。あくまで実行するのがエラーなだけでバインドは成功します(成功するからエラーになる...)。しかしこれを回避する方法があって、実はTRANSFORM_FEEDBACK_BUFFER にバインドすることでバッファ操作が許されるようになります。なおbufferSubDataも同じような形です。次のコードならエラーになりません。

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, buf1);

  gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buf1);
  gl.bufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, new Float32Array([4,4,4,4,4]));
  gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);

  // ARRAY_BUFFERの場合:
  // INVALID_OPERATION: blink::WebGL2RenderingContextBase::getBufferSubData:
  // buffer is bound to an indexed transform feedback binding point and some other binding point
  showBuf(buf1, gl.TRANSFORM_FEEDBACK_BUFFER); // 4, 4, 4, 4, 4, -120, -140, -160, -180, -200

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);

 この辺りはおそらくuniform buffer objectでも同じような仕組みなのかな...まあともかく、あんまこういうことはしないでしょうから豆知識程度でいいと思います。

bindBufferRangeについて

 書き込みのスタート位置と、そこから書き込むデータ量の限界を決めることができます。bindBufferRangeという関数です。たとえばSEPARATEの例で、

gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf0, 20, 40);

と実行すると、bufferBaseのTFF枠の0番スロットにbuf0がセットされ、スタート位置のバイトは20,そこからのフェッチ量は40バイトまでとなります。その場合の結果は、ドローコールを

gl.drawArrays(gl.POINTS, 0, 5);

のように変更するとして、

  showBuf(buf0); // 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0

見ての通り、indexで5~14に0~9が入りました。このように、20バイトの位置から40バイト分埋められます。

 ここであの引数にはoffsetとsizeという名前がついてますがoffsetとsizeは4の倍数限定という制約があります。TFFはとにかく4バイトが基本です。それと、書いてあるのでわかるんですが、bufferの登録処理が重複してるのに気づいたでしょうか。ええ、重複しています。あっちはバインドオンリーですがこっちは範囲を決めています。つまり範囲を決める場合あっちの処理は必要ないわけですが、通常はあっちしか使わないでしょう...もっとも、後から範囲だけ操作する場合、わざわざバッファを再指定しないといけないのは手間ではあります。用途次第ですね。

 当然ですが、この処理により異なるスロットに同じバッファがバインドされてしまうとエラーを食らいます。おそらく無いとは思いますが気を付けてください。アトリビュートと違って異なるスロットに同じバッファをセットするのはタブーです。エラーはこんな風:

GL_INVALID_OPERATION: glBeginTransformFeedback: Transform feedback has a buffer bound to multiple outputs.

 offsetとsizeを取得するには次のようにします:

  //gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufAll);
  gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufAll, 80, 160);

  console.log(gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0) === bufAll); // true.
  console.log(gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_START, 0)); // 80.
  console.log(gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_SIZE, 0)); // 160.

 getIndexedParameterという関数があって、これのターゲットにTFF関連のクエリ引数を入れるだけです。BINDINGでバッファを取得できます。offsetとsizeはそれぞれこのように、STARTとSIZEで指定します。なぜSTARTなのかは聞かないでください。それで、デフォルトの場合これらを取得すると0,0となります。なんとsizeも0ですが、これはsizeが0の場合の特別扱いをしているわけではなくて単に未指定を意味するフラグのようです。どういうことかというと、明示的にbindBufferRangeでsizeを0に指定するとエラーを食らうからです。0にするなというわけです。だって0じゃん...突っ込みは厳禁なのです。

 一応基礎的な内容はこれで説明し終えたかと思います。駆け足でしたがついてこられたでしょうか。まあいろいろ作例を作って遊んでみてください。次回はアトリビュートと絡めてみようかと思います。いわゆるフィードバックループですね。この節は、まあ普通はやらないんですがTFFでTRIANGLES描画をやるとかいう、およそここでしかやらないようなことをやって締めとします。

作例の解説

 内容的にはサムネイルができます。三角形の色は頂点の色で決めています。TFFには一応、なっています。バーテックスシェーダは次のようになります。vPositionはvec2で、これが3つですから、24バイトの領域を作りました。

#version 300 es
const float TAU = 6.28318;
out vec2 vPosition; // このoutの先はfragmentShaderではなくWebGLBuffer...
void main(){
  float i = float(gl_VertexID);
  vec2 p = vec2(cos(TAU*i/3.0), sin(TAU*i/3.0));
  vPosition = p;
  gl_Position = vec4(p, 0.0, 1.0);
}

 このvPositionの内容をgl_VertexID...まあアトリビュートめんどいからね。そもそも必要無いし。それで、これで格納するんですが、このvPositionがバッファに格納される、TFFですからフラグメントシェーダは仕事をしない...と思いきや、今回ラスタライザは無効化しません。

  gl.useProgram(pgTest);

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufTest);

  // 描画するならラスタライザの無効化は要らんのですよ。
  gl.clearColor(0.2,0.1,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  // なおここ、一緒じゃないとエラーです...当然ですけどね。
  gl.beginTransformFeedback(gl.TRIANGLES);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.endTransformFeedback();

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

 きわめてシンプルですねぇ。それで三角形になるんですが、その際ラスタライザを無効化しないので、フラグメントシェーダで色を付けています。それで...

#version 300 es
precision highp float;
in vec2 vPosition; // だと思うでしょう?実はこっちでも使えるんです。
out vec4 fragColor;
void main(){
  fragColor = vec4(1.0, vPosition*0.5+0.5, 1.0);
}

なんとバッファに送られるはずのvPositionのデータがフラグメントシェーダにも送られています。そうです。どっちでも使えるんです。まあそれだけですが、覚えておくといいことがあるかもしれませんね。

 なおTFFですからバッファの内容を確認しないといけないですね。安心してください。入ってますよ:

  showBuf(bufTest); // 1, 0, -0.4999985694885254, 0.8660261631011963, -0.5000028014183044, -0.8660237789154053

 トランスフォームフィードバック基礎編はこれにて終了となります。お疲れ様でした!

今回登場した関数

transformFeedbackVaryings
bindBufferBase
bindBufferRange
getIndexedParameter
beginTransformFeedback
endTransformFeedback