TFFとインスタンシング
インスタンシング!
前回はTFOとVAOを使って逐次更新するコードを書きました。いいですね。アップデート処理が見やすくなって満足です。しかし今のままだと点描画しかできないですね...もちろんそんなことはありません。今更新している内容をインスタンスパラメータとして使えば、インスタンシングでパラメータだけ逐次更新するコードを書くのは簡単です。前回のコードを発展させて、正方形を回しながら反射で飛び回らせるコードを書いてみましょう。ひな形ですから味気ないものですが、正方形が他の何かになろうと、3次元空間での運動になろうと、基本は全く同じです。基本って味気ないものなんですよ。創意工夫次第でいくらでも化けます。それでは参りましょう。
なお3Dを知っているなら、この節の内容を応用して簡単に3Dでもインスタンシングで大量に逐次更新でオブジェクトを動かせるので、興味のある人は挑戦してみましょう。
コード全文
オブジェクトのデータ更新
静止画が目的ならパフォーマンスは必要なく、大量に描けばいいだけです。そこまでパフォーマンスが良くなくとも時間を掛ければ終わります。しかし毎フレーム動かすとなるとそうはいかないので、毎フレーム全部描くとなればパフォーマンスがどうしても問題になってきます。動かす方法はこれまでにもいくつか紹介してきたんですが、ここでまとめておきます。
uniformで動かす
手っ取り早く動かすなら一番楽な方法です。uniformで直接いじればいい。そのための変数もCPUサイドで保持します。つまりCPUで動かしてuniformで反映させるわけですね。この後述べる方法も、一部の(例えば時間などの)変数はuniformを使うことは普通にあるんですが、ここで述べてるのは位置や時間も含めてすべてという意味です。そうなるとuniformの限界が問題になってきます。一応プログラムで確認できるようにしました。ChromeまたはFirefox系の場合、
MAX_VERTEX_UNIFORM_VECTORS: 4096
MAX_FRAGMENT_UNIFORM_VECTORS: 1024
となります。ここでいうVECTORというのは16バイトのベクトルのことです。つまりバーテックスシェーダの場合合計が4096*16バイトを超えたらアウトであり、フラグメントも似たような意味で、合計が1024*16バイトを超えたらアウトです。ちなみに私のAndroidではどうなるかというと、
MAX_VERTEX_UNIFORM_VECTORS: 256
MAX_FRAGMENT_UNIFORM_VECTORS: 256
です。貧弱ですね...mat4を64個使うともうアウトです。まあ普通にこれでも多いんですが(そもそもuniformを使いまくるという状況がまれ)。ただ手軽さは一番なので手っ取り早く動かすならuniform一択ですね。
バッファの動的更新
4-12で扱ったバッファ操作による更新です。bufferSubDataの負荷は大したことありません。この場合問題になるのはCPU処理です。自分は詳しくないですがWebWorkerってのを使えばある程度高速化できるかもしれません。小細工無しの場合、前節でも述べたように単純な反射程度、あるいはなんらかの単純な値の当てはめ程度なら10万個くらいでも余裕です。uniformほどお手軽ではないですがプログラムを走らせる必要が無いという最大の利点があるのでそれなりに有用です。使いどころを間違えなければ最も使える手段です。
なおuniform bufferを使うとより大きい数のuniformを扱えますが、これはさっき紹介したuniform法の欠点を補う形にはなるんですが、結局のところやってることはバッファ操作による逐次更新ですから、目的によっては大差ないでしょう。ボーンメッシュアニメーションで行列配列を逐次更新するのもこれに相当します。あれもそこまでたくさん行列を扱うわけではないので(むしろそこがボーンメッシュの強み)、充分頼りになります。
なお、これを使ってWebGL-2Dで大量のポイントスプライトを高速で動かしているのがPIXI.jsです。ツクールとかでも使われています。そこまで詳しくないのでお話し程度でご了承ください。
Transform Feedback
トランスフォームフィードバックでバッファをスワップさせる、まさに今取り組んでる方法です。パラメータ部分だけ更新してインスタンシングでメッシュを描画するのが一般的な流れで、この後で取り扱います。CPUを経由しないので、更新の仕方がある程度複雑でも負荷に耐えられる分、動的更新よりも優位です(更新の仕方がクソ単純な場合は簡便さで劣ります、前回述べた通り)。プログラムを書かないといけないんですが、それとは別に欠点があって、それは相互作用に弱い点ですね。たとえば衝突やboidsは無理で、個別の更新が基本的な使い方になります。そういうのを扱いたい場合はこの後述べる方法に頼ることになります。
フロートテクスチャスワップ
まだ紹介していないテクスチャを使う方法です。ざっくり説明すると、データの塊です。自分も詳しくないのでざっくりとだけ。フロートテクスチャというのがあって、まあデータストレージのようなものなんですが、Float32のvec4が512x512個程度なら余裕です。これを2枚用意してTFFのように互いにとっかえひっかえして更新するわけです(TFFと似たような禁忌があって同じテクスチャで更新をすることはできない仕組み)。フレームバッファという、これまたTFFのような機能があって、ざっくりいうとドローコールでテクスチャに書き込む仕組みです。インタフェースってやつです。まあTFFもインタフェースですが。これを使って逐次更新します。最大の利点は相互作用に強いことです。まあ何でもってわけにはいかないですがある程度の自由は効きます。ただ工夫がかなり必要なのであくまで「できなくはない」というレベルですが、全くできないよりはマシです。TFFと同じでGPU処理なので速いです。たとえばバイトニックソートなどはこれを使うのが簡便です。
以上、いろいろ紹介しましたが、まあそういうのはおいておいて、まずはTFFによる逐次更新からのインスタンシングを解説します。
attributeを増やす
ちょっとアトリビュートを増やします。というのも今回、正方形を回転させたいからです。まあ実験ですから、それほど難しいことはしません。回転角度と回転速度をそれぞれ用意して、逐次更新で回転速度を回転角度に足す、ただそれだけのことです。いくらでも複雑にできます。あくまでひな形なので、難しいことはしなくていいですね。
#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aVelocity;
layout (location = 2) in float aRotation;
layout (location = 3) in float aRotationSpeed;
out vec2 vPosition;
out vec2 vVelocity;
out float vRotation;
out float vRotationSpeed;
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;
vRotation = aRotation + aRotationSpeed;
vRotationSpeed = aRotationSpeed;
}
ぜいたくにアトリビュートをそれぞれの枠で使っています。まあテストなので.,.まとめた方がいいんでしょうが、この程度なら下手にまとめて読みにくくなるよりは普通に書いた方がいいでしょう。さっき述べた通りの処理をしています。回転速度は据え置きです。ここも工夫して変化を加えると楽しいかもしれませんね。
なおアウトプットも同じように増やしています。vRotationSpeedが単純代入になっていますが、据え置きでもきちんと定めないと値が未定義となり消滅します(据え置きの概念は存在しない)。ここ間違えやすいので気を付けてください。何もしなかったからと言って値が据え置きになることはありません。
プログラムの方にも反映されています。ここ、interleavedだと便利ですね。いくつ増えてもつなげるだけですから。
// interleavedしましょう
const pgUpdate = createShaderProgram(gl, {
vs:vsUpdate, fs:fsUpdate, outVaryings:["vPosition", "vVelocity", "vRotation", "vRotationSpeed"], separate:false
});
それで準備ができたのでVAOとかいろいろ用意しましょう。
VAOとTFOの用意
VAOとTFOの用意を実行してるパートはこちらになります。VAOサイドがちょっと長めなのは描画用も用意しているからです。今回は描画用と更新用で異なるVAOを使います:
const vaos = [];
const tfos = [];
const vaosDraw = [];
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, 24, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 24, 8);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 24, 16);
gl.vertexAttribPointer(3, 1, gl.FLOAT, false, 24, 20);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);
gl.enableVertexAttribArray(2);
gl.enableVertexAttribArray(3);
gl.bindVertexArray(null);
// 描画用のvao作ります(インスタンシング)
// つまみぐいもいいとこですが、まあこんな感じだと思います
const vaoDraw = gl.createVertexArray();
gl.bindVertexArray(vaoDraw);
gl.bindBuffer(gl.ARRAY_BUFFER, bufForSquare);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, bufs[i]);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 24, 0);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 24, 16);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);
gl.enableVertexAttribArray(2);
gl.vertexAttribDivisor(1, 1);
gl.vertexAttribDivisor(2, 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);
vaosDraw.push(vaoDraw);
}
ここで用意しているのは、いつものように用意した2つのバッファの逐次更新用のVAO並びにTFO, それとは別に、インスタンシング描画用のVAOです。これも2通り用意しています。似たようなものをバッファ違いで2つ用意していますが、そうしないと入れ替えで余計な手間がかかってしまうので当然の処置ですね。ここに出てくるbufForSquareというのはインスタンス描画用のバッファであり、divisorを0として扱うバッファになります。上の方で用意しています:
// 正方形描画用のバッファ
const bufForSquare = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufForSquare);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1/32, -1/32, 1/32, -1/32, -1/32, 1/32, 1/32, 1/32
]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
ちっこい正方形です。ここを星形にしたり例の「F」にしたりトーラスや球にするわけですね。まあ今回は普通にArrays描画ですが。エレメントの場合はVAOにそれに応じた修正を施す必要があるんですが、VAOのところで散々詳しくやったのでもはや説明する必要は無いですね。それで、前半で0番スロットにそれを入れた後で、1番と2番にそれぞれ...見ればわかると思うんですが「位置」と「回転」を当てはめています。フェッチもそれに応じたものになっています。アトリビュートの理解がきちんとしているなら説明不要でしょうが、シェーダーを見た方が分かりやすいですね:
#version 300 es
layout (location = 0) in vec2 aPosition; // ジオメトリ
layout (location = 1) in vec2 aOffsetPosition; // インスタンス
layout (location = 2) in float aOffsetRotation; // インスタンス
void main(){
vec2 p = aPosition;
float r = aOffsetRotation;
p *= mat2(cos(r), -sin(r), sin(r), cos(r));
p += aOffsetPosition;
gl_Position = vec4(p, 0.0, 1.0);
}
行列を素直に乗算代入できるとコードが読みやすくていいですね~!見ての通り、回転させて、位置を動かす。それだけです。フラグメントは色を決めてるだけなので省略します。このようにつまみ食いをすることによりインスタンス変数で描画できるわけです。もちろん速度関連の変数を彩色や変形に使いたいならそれ相応の情報を取り入れるのもありです。まあ好みに応じてアレンジして楽しんでください。インスタンスなのでちゃんとdivisorを設定しています。どの段階においても0に戻す必要はありません。VAOは優秀だからです。
TFOは相変わらずバッファをバインドして終わりですが、コードがすっきりするので、これでいいですね。
ドローコール
準備ができたので、早速ドローコールを実行しましょう。
const draw = () => {
gl.useProgram(pgDraw);
gl.bindVertexArray(vaosDraw[bufIndex]);
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);
// 今回正方形ですからね。まあFとかでもいいんですが...Fにする?
// まあFにするのは宿題ということで...
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, 256);
gl.disable(gl.BLEND);
gl.bindVertexArray(null);
gl.flush();
}
drawArraysIstancedを使っています。当然ですがElementsであってもそのバインドをここでやる必要はありません。今回はやりませんが、たとえば使い古した「F」でも回してみてください。256個描画します。これで完成です。お疲れ様でした。
おわりに
以上です。結局のところ、どれもこれもただの選択肢です。たとえば2Dも選択肢です。2Dでもいいわけです。すぐれた作品はいくらでもあります。2Dか3DかWebGLかuniformか動的更新かバッファスワップかテクスチャスワップか、どれもただの選択肢です。好きなものを好みと必要に応じて選びましょう。このサイトはWebGLの解説サイトだからWebGLの解説をしているだけです。自分も2Dでコードを書きます。選択肢、です。
次回でTFF編は終了です。wgldがパーティクルを動かしているのでこれをやってみましょう。楽しみですね。