色んな型で作ってみる
色んな型を試そう

前回説明したように、インデックスバッファを使うとインデックスだけを指定することで自由に大量の三角形を描画できます。ところで描画時にはUNSIGNED_BYTEであることを明示しました。ここで違う型を使ったら一体どうなるでしょう。また用意するときにも違う型を使ったら?それをやりましょう。最も実用上は同じ型でやるんで何の問題も無いんですが、実際に扱ってるのがバイト列であるという認識があるだけで、この先の理解がしやすくなるので、そのために一節設けました。この章はこれで終わりです。次の章からアトリビュートをやります。ちょっとずつやっていきます...長い道のりになると思います。
コード全文
コードの概要
今回は次のように点を選びます。3x3状に9つ。ここから12個の点を選んで4枚の三角形を描画します。

なのでインデックスとしては0,3,1, 1,5,2, 3,6,7, 7,8,5が得られればいいんですが、これを5通りの方法で得ようと思います。作るときの型付配列が、Uint8, Uint16, Uint32の3通り、フェッチして1つのインデックスを生成するのに使われるバイト数が、1バイト, 2バイト, 4バイトとあるので合計9通りあるんですが、全部やっても仕方ないので、同じ型で1つ、違う型で4つ用意しました。それで充分でしょう。
indexBufferの生成機構もメソッド化して便利にしました。
// バッファ登録を簡易化
const createIndexBuffer = (src) => {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, src, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return buf;
}
bindしてsrcを放り込んで、IBOを返すだけです。返されたバッファをbindすることで発動します。ソースは作る際にすでに型付配列としているので、bufferDataにはそのまま放り込めます。今見たらuPos[0]は描画のたびに登録していますね...まあいいか。ドローコールのところがhalfと分かれていますが、これはスケッチのサイトを見ると分かりますがhalfにチェックを入れると下半分だけ描画されます。これはバイトオフセットの実験です。
じゃあさっそく見て行こうか。楽しみ~。
いろんな型で描画する
様々な型付配列でデータを用意し、様々な型指定でフェッチします。たとえば4バイト整数で使うなら4バイトずつフェッチします(0,1,2,3で1つ、4,5,6,7で1つ、えっほ、えっほ、...)。そういうわけでさっそく整数ができるところを見に行きましょうね。
BYTE-BYTE
まずUint8Arrayで作ってUNSIGNED_BYTEで使います。前回やりましたね。まあ復習しましょう。
const indices_0 = new Uint8Array([
0, 3, 1, 1, 5, 2, 3, 6, 7, 7, 8, 5
]);
これで12バイトです。これをbindしてdrawElementsで1バイト非負整数の型を指定して描画します。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_BYTE, 0);
下半分だけ描画するには、6バイト目から11バイト目が要るので、オフセットを6(バイト)に指定します。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_BYTE, 6);
SHORT-BYTE
次に、Uint16Arrayで作ってUNSIGNED_BYTEで使いましょう。どういうことかというと、2バイトの整数を6つ並べて12バイトとし、そこに1バイトずつフェッチしていって目的の12個を得るんですよ。たとえば最初の0,3を得る場合、この0と3を2バイトの整数に落とし込みます。ここで重要になってくるのがリトルエンディアンの考え方です。これは2バイト以上の数を扱う際に、大きい方のバイトから並べるか、それとも小さい方のバイトから並べるか、ということです。Uint16Arrayは2バイトの整数が並んでいるんですが、各々の整数をバイト列としてどう見るかにより、フェッチの際に1バイトずつ見るにあたりどんな順で取得するかには任意性があるわけです。
たとえば、3 + 256*4 + 256*256*7 + 256*256*256*1をリトルエンディアンで見る場合、1バイトずつフェッチした結果は3, 4, 7, 1であり、ビッグエンディアンで見た場合の1バイトずつフェッチした結果は1, 7, 4, 3となります。なので然るべく数を用意しないと意図した結果にならないわけです。
それでWebGLはどっちかというとリトルエンディアンです(何回強調するねん)。ここに記述があります。
まず、DataView を用いた JSON データから動的に配列バッファーを作成します。true の用法に注意してください。WebGL は私達のデータがリトルエンディアンであることを予期しています。
そういうわけなので、目的の配列はこちらです:
const indices_1 = new Uint16Array([
0 + 256*3, 1 + 256*1, 5 + 256*2,
3 + 256*6, 7 + 256*7, 8 + 256*5
]);
これをWebGLの仕組みに従って1バイトずつフェッチすると、先ほどと同じ整数列が復元されます。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_BYTE, 0);
1バイトずつなので下半分にする方法は一緒です。ゆえに割愛。
INT-BYTE
INTで用意してBYTEで使う。先ほどの内容が理解できていれば、結果を見るだけで分かるかと思います。
const indices_2 = new Uint32Array([
0 + 256*3 + 256*256*1 + 256*256*256*1,
5 + 256*2 + 256*256*3 + 256*256*256*6,
7 + 256*7 + 256*256*8 + 256*256*256*5
]);
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_BYTE, 0);
BYTE-SHORT
ここからはBYTE以外の型で整数を作る話です。まずUint8Arrayで作ってUNSIGNED_SHORTで使います。まあ違う型で使うっていうのは基本しないんですが、遊びなので...フェッチでもリトルエンディアンが重要になってきます。どういうことかというと、まず配列の各々の数をリトルエンディアンに従ってバイト列にします。そうしたうえで1バイトずつ取得し、指定した型に応じて整数を復元するわけです。今の場合だとたとえば3を作るには「3+256*0」という形にしないといけないですから、「目的の整数 0」という形で延々とつなげていくことになります。つまり、こう:
const indices_3 = new Uint8Array([
0,0, 3,0, 1,0, 1,0, 5,0, 2,0,
3,0, 6,0, 7,0, 7,0, 8,0, 5,0
]);
これをUNSIGNED_SHORTを作るためにフェッチする場合、2バイトずつ取得したうえで、リトルエンディアンで復元するので、偶数番目の0が無視されて整数列が復元されます。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_SHORT, 0);
なおcountはすべて12です。halfも、countはすべて6です。作られる整数の個数は同じですからね。それで下半分の場合ですが、
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_SHORT, 12);
となります。6ではないです。バイトオフセットはバイト数で指定するので、2バイト6つ分ずらすなら2x6で12というわけですね。
SHORT-INT
最後に、SHORTで作ってINTで使う場合です。実は配列としては全く同じです。
const indices_4 = new Uint16Array([
0,0, 3,0, 1,0, 1,0, 5,0, 2,0,
3,0, 6,0, 7,0, 7,0, 8,0, 5,0
]);
それで、各々の整数がまず2バイト整数としてリトルエンディアンでバイト列にされ、そのうえで4バイトフェッチして、UNSIGNED_INTにされて使われます。もちろんその際もリトルエンディアンに基づいて復元されるので、結局同じ見た目の配列になります(型付配列としては異なるものです)。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_INT, 0);
なお、下半分が欲しい場合は、4バイトx6つで24バイトずらします。
gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_INT, 24);
仕組みについては以上となります。お疲れ様でした。
DataViewって何?
さっきのMDNのリンクでDataViewというのが出てきたと思います。実はUint16ArrayやUint32Arrayそれ自体のバイト列としての扱いは環境依存です。WebGLはリトルエンディアンを貫いていますが、型付配列自体にはその情報はありません。そこでDataViewです。これはそれ自体がバイト列であり、ここにたとえば16bitの整数を入れる場合、あるいは32bitの小数を入れる場合でさえ、エンディアンを指定してどっちのバイトから並ぶかを明示できます。この先、同じバイトデータに整数や小数をごっちゃまぜにして格納したくなるんですが、その際にたとえば小数なんかをリトルエンディアンになるようにIEEEの流儀に従ってちまちまやるのは非効率です。しかしDataViewを使えば、勝手にリトルエンディアンに従って並べてくれます。とても便利です。
パターン4(UNSIGNED_INTで使う)で、代わりにこれを使えます:
// 48バイト。
const dataView4 = new DataView(new ArrayBuffer(48));
for(let i=0; i<12; i++){
const n = indices_0[i];
// リトルエンディアン
dataView4.setUint32(4*i, n, true);
}
DataViewの作成にはたとえば空っぽのバイト列(ArrayBuffer)が必要です。そこで48バイト分用意しました。ここに、4バイトのオフセットずつ、indices_0にある数を「true(リトルエンディアン)」で格納していきます。引数が省略されるとデフォルトのビッグエンディアンにされるので注意ですね。indices_4の代わりにこれをbufferDataしても全く同じ結果が得られます。
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, dataView4, gl.STATIC_DRAW);
アトリビュートのインターリーブの項で多分再登場するので、意識の隅に置いといてください。
以上です。はぁ、お疲れ様でした。お茶でも飲んで一休みしましょう。静岡駅地下の美味しいお茶が飲めるスペースがあるんですよ。一茶と言います。お越しの際は是非お立ち寄りください。こちらです。

次からはようやくアトリビュートを説明できそうです。wgldはカメラも用意するんでしたっけ。3Dまで行けるんですかね。まあ無理でしょうね。こっちは本格的にやるつもりはないので、ゆるゆると、のんびりやりましょう。道草を食いながら歩くのは楽しいものです🍵
余談:Int16Array?
まあ重箱の隅をつつくような話なんですが、wgldのインデックスバッファのサイトではInt16Arrayを使っています。こちらです。原因は分かんないんですが、「整数」をそのままInt16に脳内変換したらこうなったんでしょうか...まあこのサイト、今はもう管理してないっぽいので、あんま突っ込んでも仕方ないんですよね。管理人さんは今では普通にUint使ってるかもですし。
結論から言うと、Int16でも挙動に問題はないです。使うときにUNSIGNED_SHORTをきちんと指定すれば、作るときに用いた非負整数が問題なく復元されます...が、型付配列の段階では大きい数はしっかり負の数になっています(当然)。インデックスを指定するのに負の数が入った配列を使うのは具合が悪いので、特別な事情が無い限りは素直にUNSIGNEDを使いましょう。