indexBufferを使う

IBO:elementArrayBuffer

 なんかいろんな名前で呼ばれているようです。indexBufferというのは俗称で、正式名称は、ELEMENT_ARRAY_BUFFERです。ていうかバッファって何...いきなり出てきた。
 今までの描画ではdrawArraysで数をたとえば0,6なら0,1,2,3,4,5と指定しました。これが7,4とかだと7,8,9,10となります。つまり「オフセット」とそこからのカウントで連番です。これでは不便なので、自由に指定しようというわけです。たとえば0,1,2,3だけ用意しておいて、三角形としては0,1,2と2,1,3で作りたい、とか...TRIANGLE_STRIPじゃんとか言わないでね。そうではなくて、柔軟性を持たせたいわけです。何でもありが欲しい。そのための機能です。あらかじめグリッド状に点を配置しておいて、自由に指定して三角形を作って文字の形にしたりしたいわけです。この「F」とかね(サムネイル参照)。それをやってみましょう。
 あとここで初登場するのが「WebGLBuffer」です。最小単位はバイトです。8bit. バイト列、です。それをGPUサイドで使えるようにするためのものです。これからいっぱいお世話になります。どうぞ、お見知りおきを。

コード全文

作品17のリンク

整数列を自由に取る

 まず、drawArrays(gl.TRIANGLES, 0, 12)とか命令すると、0,1,2,...,11ごとにバーテックスシェーダに頂点が送られて、それらを順に3つずつ選んで三角形ができたりします。LINESであれば6本の線分になります。ドローコールによりそれらからどのように三角形や線を描画するかは異なるんですが、ベースとなる整数列は常に、特定のオフセットからの連番です。
 ここをいじるのが、indexBufferの仕組みです。連番ではなく、3,4,5,8,9,1など、好きな並びを使えます。それでTRIANGLESとか命令すれば、好きな三角形の集合を描画できるわけです。それらのインデックスが頂点と結びついてるところを想像してください。お絵描きができそうですね。そんな感じです。
 なお、現在アトリビュートをやっていないので、相変わらず整数から好きな頂点を選ぶのに苦労しています。実質整数の加工でしか作れません。uniformに配列を送ればそれっぽいことができるんですが、これには多すぎるとエラーを食らうというデメリットがあるので、早いとこアトリビュートを導入したいところですね...とはいえまだやってないので、今回も配列uniformに頼ることとします。

 その前に、ちょっとだけ寄り道をします。展望台に行きましょう。

展望台(global state)

 ソースコードにも書きましたが、global stateというのがあります。概念的に、です。自分の理解の最前線ってやつです。

  globalState ={
    currentProgram:null,
    bindingFramebuffer:null,
    bindingBuffers:{
      arrayBuffer:null,
      elementArrayBuffer:null, // 今回用があるのはここ
      uniformBuffer:null,
      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,
    bufferBase:[
      {
        transformFeedback:{buffer:null, offset:0, size:0},
        uniform:{buffer:null, offset:0, size:0}
      },{},{},...
    ],
    activeTextureIndex:0,
    textureSlot:[
      {texture:{"2D":null, "CUBE":null, "2D_ARRAY":null, "3D":null}},
      {},{},...
    ],
    etc...
  }

 WebGLはステートマシンです。WebGPUはそうじゃないらしいです。まあそれはおいておいて、ステートマシンなので、あらゆるステートが隠蔽されています。処理を実行することでステートが書き換えられ、ドローコールはそのステートに基づいて実行されます。なので同じ内容の描画を繰り返しやるのに適しています。ここに用意したのは自分が「これ隠蔽されてることを意識しないと不便なのでは...」と思ったものの一覧です。上から見て行くと、まず、

といった感じですが、まあ分からないですね。説明してないことだらけなので...1つ目は要するに使っているプログラムです。useProgramでなんかプログラムがセットされ、ドローコールはそれに基づいて実行されます。そんな感じで隠蔽された状態に基づいていろいろ処理が実行されるのがステートマシンです。それで今回用があるのは3つ目の、bindingBuffersの、elementArrayBuffer枠です。

WebGLBuffer

 WebGLBufferというのがあります。作るにはcreateBufferを実行します。

  const buf = gl.createBuffer();

 これはバイト列です。バイトデータの列です。バイトというのは8bitの単位です。つまり内容的には0~255の数がいっぱい並んでいるんですが、それをWebGLの内部構造で利用するためのものです。たとえばUNSIGNED_BYTE(1バイト非負整数)であればそのものずばり、0~255の整数の並びとして活用できるわけですが、4バイトの小数(Float32,いわゆるFLOAT)としても使えるし、2バイトの整数(Int16,いわゆるSHORT)としても使えます。どう使うかというと、2バイトずつとか4バイトずつ採取してリトルエンディアンで解釈します。詳しくは次回やりますが...要するに、数表現の汎用的な形式ということで、これが採用されているわけです。なのでindexBufferについてもこれに即した形でデータを用意し、活用することになります。
 なんかQiitaよりまじめに書いてるな...もっと砕けてもいいのに...まあいいか。

インデックス配列を作る

 それでは早速作っていくんですが、その前に点データです。

point data 3x5
  // 点データuniform
  const points = [];
  for(let k=0; k<6; k++){
    for(let i=0; i<4; i++){
      const x = -3/8 + (1/4)*i;
      const y = 5/8 - (1/4)*k;
      points.push(x, y);
    }
  }

 それで、24個の頂点ができるんですが、これをuniformで送ってシェーダーで使います。ただ今回、三角形はこんな感じで作りたいので、連番では無理ですね。もちろん同じ頂点を大量に用意すればできなくはないですが、汎用性が無いので採用できません。そこでindexBuffer、正式名称はelementArrayBufferですが、これの出番です。

  // indexのデータ
  const indices = [
    0,4,5, 0,5,1, 1,5,6, 1,6,2, 2,6,7, 2,7,3,
    4,8,9, 4,9,5,
    8,12,13, 8,13,9, 9,13,14, 9,14,10, 10,14,15, 10,15,11,
    12,16,17, 12,17,13,
    16,20,21, 16,21,17
  ]

 ここから数を3つずつ取り出して三角形を作る処理を、gl.TRIANGLESでやろうというわけです。

elementArrayBufferを作る

 このデータを基にして、elementArrayBufferを作りましょう。短くIBOと呼ばれることが多いですね...IBOでいいか。まあいろんな呼び名に慣れておくと、結局相手がどんな呼び名になじんでいても方円の器に従う水のように自在に呼び名を変えられますから、便利ですよね。

  // elementArrayBufferを作りましょう
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
  // 型を付けないとどんなバイト列か分かんないんです。なので必要です。
  // 通常の配列では使えないんです...
  // BYTE? SHORT? INT? (ただしUNSIGNEDとする)
  // 8bit? 16bit? 32bit?
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

 まずcreateBufferでWebGLBufferを用意します。バイト列ですが、どのバイトに何を入れるかまだ何にも決まってません。ニートです。仕事を与えましょう(バイトの意味が違う気がしますがまあいいですね)。
 ここにも書きましたが、型付配列でないと困るわけです。通常の配列だとその数が1バイト非負整数(UNSIGNED_BYTE)なのか2バイト非負整数(UNSIGNED_SHORT)なのか4バイト非負整数(UNSIGNED_INT)なのか分かんないので、明示するわけです。今回は整数の上限が23ですから、UNSIGNED_BYTEで充分ですね。そこでUint8Arrayを構成し、登録します。

 型についてはこれくらいとし、メソッドの説明をします。まずbindBufferですが、これは先ほど述べたglobal stateのbindingBufferのelementArrayBuffer枠にこのバッファを登録するものです。先にも述べましたが登録できるのは1つまでです。他のバッファを紐付けると上書きされます。また、nullを紐付けることもできます。その際に指定する第一引数がgl.ELEMENT_ARRAY_BUFFERとなっていますがこれをターゲットといいます。これはどの枠を操作するかを明示するものです。これを指定することでそのバッファはelementArrayBufferとして扱われることになります。
 次にそこにデータを供給します。これがbufferDataの仕事です。実はスペースだけ作って後から値を入れる方法もあるんですが、詳しい話はいずれやるのでここでは割愛させていただき、今回は最も基本的な扱い方として、空っぽのバッファに型付配列の形でバイトデータを供給するという使い方を紹介するにとどめます。3番目の引数であるgl.STATIC_DRAWもいずれ説明しますが、とりあえずこれの意味は「このデータをいじる予定はありません」です。つまり入れっぱなしです。実際そうなので、それでいいですね。
 ここでbufferDataの対象は作ったバッファなのは間違いないんですが、関数の引数にはバッファがありません。その代わりとなる役割を果たすのがターゲットです。このターゲットによりglobal stateのelementArrayBuffer枠が指定され、そこのWebGLBufferObjectがデータの供給(その他もろもろ)の処理の対象となるわけです。ゆえに、1つまでしか登録できず、再登録で上書きされるのですね。バッファ自体を対象とする方が直観的に思えるのですが、ここが若干人によっては分かりにくく感じるところです。

 最後に、今回は不要なんですが、一応マナーとしてelementArrayBuffer枠をnullにしておきます。これを怠ると予期せぬデータの書き換えなどなどを実行してしまいかねないのでなるべくやるようにしましょう(特にarrayBuffer枠)。実行するにはターゲットを指定してnullを指定すればOKです。なお、再登録には当然、作ったバッファが必要です。便利なショートカットなどはありません。(実はVAOがそれに類する役割を果たすんですが割愛します...説明したいことがありすぎてパンクしそうです...)

 (追記...実は供給するデータは型付配列、のちに説明するかもですがDataView,とにかくバイトごとのデータが特定できるなら種類を問いません。ただインデックスバッファの場合Uint8,Uint16,Uint32以外で用意することはほぼ無いでしょうね。)

elementArrayBufferを使う

 というわけで早速IBOを使ってみます。簡単で、バインドして特別なドローコールをするだけです。そのドローコールがちょっと説明が必要なのがあれですが...とりあえずコードを見てみましょう。

  gl.clearColor(0,0,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(pg);
  uniformX(gl, pg, "2fv", "uPos[0]", points); // [0]忘れないでね

  // 使うときはbindする
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
  // 基本的に用意した時と同じ型を指定する。違う場合は...
  // 54はindexの個数ですね。54バイトの「F」
  gl.drawElements(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 0);

 黒でクリアしてuniformXで点データを供給するところはもう繰り返しません。ごめんね!気になる人は復習してください。なお深度クリアが忘れ去られていますが、まあ要するに不要なのでやってないだけです。いずれ必要になったら復活するでしょう。
 IBOを使うにはまず、バインドします。さっき作ったWebGLBufferですね。これを再びバインドしてelementArrayBufferに置きます。その状態で次のgl.drawElementsを実行すると、そのバッファの内容を解釈して数データが復元され、いつもの整数列の代わりに使われる仕組みです。その際、まずcountを指定します。countとは、この場合54ですが、「いくつの数を作るか」を指定します。今回三角形は18個なので54個のインデックスが要るわけですね。次にtype, これは「どんな数を作るか」を指定します。今回作りたいのは1バイトの非負整数、UNSIGNED_BYTEです。なのでそれを指定します。最後の0はバイトオフセット、つまり「何バイト目からフェッチし始めるか」ですが、まあ今回は全部なので0でいいですね。
 すると、もともとがUint8Array, 1バイトずつ数が並んでいるわけですから、用意した通りに数がフェッチされ復元され、あの整数列が再現されます。それをTRIANGLESで3つずつ選んで三角形を作るわけですから、あの図にあるようなFの文字が生成されるわけです。

 なお当然ですが、drawElementsもバッファを直接指定することはありません。あくまでglobal stateを参照して使うべきWebGLBufferを選びます。それがステートマシンと呼ばれるものです。ここ(elementArrayBuffer枠)がnullだと何も起きないですし、別のIBOを置けば別のインデックスが採取されます。なお、drawArraysはこの枠を使わないので、枠の状態に依らず自由に実行できます。

 ともあれ、初めての描画、お疲れ様でした。

offsetをいじる

 オフセットについてちょっとだけ触れます。たとえば3バイト目から51個整数を作ると、最初の三角形だけキャンセルされるわけです。

  gl.drawElements(gl.TRIANGLES, 51, gl.UNSIGNED_BYTE, 3);
first triangle omit

 このようになります。ただoffsetはバイト数で指定するので、今それぞれの数が1バイトだから3つずらすのに3バイトを指定したわけです。2バイト整数なら当然ですが6(バイト)を指定します。この辺は次回詳しくやります。
 また、この状態で54を指定すると用意したバイト列の範囲を超えます。ちょっとでも超えると次のエラーが出て描画不履行となります。

  gl.drawElements(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 3);
[.WebGL-0x7914001e1a00] GL_INVALID_OPERATION: glDrawElements: Insufficient buffer size.

描画が失敗するようでしたら、この辺に注意してデバッグしましょう。以降、いくつか補足をします。

WebGLBufferをconsoleで

出力しようとしないでください。つまり、

  const buf = gl.createBuffer();
  console.log(buf);

のようなコードを書かないでくださいという意味です。やる際は自己責任でお願いします。どうなっても責任は取れませんが...まあ必要無いのでやることはないでしょうね。
 中身を取り出す処理はいずれ解説します。焦らなくても大丈夫です。

ターゲットの重複は不可

 まあ一応Qiitaで触れてるんで、追記します。次のコードはエラーになります。

  // elementArrayBufferを作りましょう
  const buf = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, buf);   // この2行は
  gl.bindBuffer(gl.ARRAY_BUFFER, null);  // エラーになる

  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
  // 型を付けないとどんなバイト列か分かんないんです。なので必要です。
main.js:96  WebGL: INVALID_OPERATION: bindBuffer:
  buffers bound to non ELEMENT_ARRAY_BUFFER targets
can not be bound to ELEMENT_ARRAY_BUFFER target

 いずれ様々なターゲットが出てくると思うんですが、その中でもelementArrayBufferは異質で、他のターゲットに一度でも紐付けられたWebGLBufferはもはやELEMENT_ARRAY_BUFFERには登録できない仕組みになっています。1回でも紐付けたことがあるとアウトです。処理がキャンセルされ、枠はnullのままになります。

 じゃあ逆にELEMENT_ARRAY_BUFFERに紐付けたことがある場合に他のターゲットに紐付けようとするとどうなるかというと、

  // 8bit? 16bit? 32bit?
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

  gl.bindBuffer(gl.ARRAY_BUFFER, buf);   // この処理は
  gl.bindBuffer(gl.ARRAY_BUFFER, null);  // 無効になる

  gl.clearColor(0,0,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT);
WebGL: INVALID_OPERATION: bindBuffer:
  element array buffers can not be bound to a different target

無効化されます。つまり、ELEMENT_ARRAY_BUFFERに一度でも紐付けられたWebGLBufferはもはやほかのターゲットには登録できない仕組みになっています。要するに、IBOか、IBOでないか、は一意に決まるというわけですね。処理がキャンセルされるだけなので、描画自体は問題なく成功します。
 このように、露骨に全部バインドからの解除をやってもエラーは一向に出ません。

  const buf2 = gl.createBuffer();
  //gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf2);
  //gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
  gl.bindBuffer(gl.ARRAY_BUFFER, buf2);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindBuffer(gl.UNIFORM_BUFFER, buf2);
  gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buf2);
  gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
  gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf2);
  gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, buf2);
  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

 そしてコメントアウトを外すとエラーの嵐になりすべての処理が無効化されます。逆に最後にこれをやってもELEMENT_ARRAY_BUFFERには登録されません。仲間外れです。やってみてください。

 補足は以上です。

次回予告

 邪霊、照覧!
 何でもないです。次回ですが、今回バイト列の用意とドローコールで同じ型を使いましたが、それだと面白くないので、違う型でやろうと思います。まあまずそんなことはしないんですが、バイト列の扱いに慣れたいので、ちょっと遊んでみましょう。不要な場合はすっとばしてアトリビュートの方に行っちゃっていいです。自由に選んでください。ではまた、次回。

今回登場した関数

createBuffer
bindBuffer
bufferData
drawElements