はじめに
アトリビュートは難しいんですが、いつまでもそれ言っていても始まらないので、徐々に準備を始めて行こうと思います。最初の方でグローバルステートについてちょっと説明します。
引き続き、p5.jsアパートメントを間借りしてwebGLのお勉強をさせていただいています。p5.jsのwebGLに嫌気がさしたからそうしていると思うでしょう。違います。隠蔽されている部分についての知識が深まれば、バグ対応もしやすくなりますし、アドバンストなことがしたいと思ったときに便利だからです。そういう意図でやっています。
今回のテーマはインデックスバッファです。まず描画の仕組みを何回でも説明します。
gl.drawArrays(gl.TRIANGLES, 0, 6);
という場合、整数0,1,2,3,4,5を使って描画します。TRIANGLESですから、0,1,2で三角形を作り、3,4,5で三角形を作ります。また、
gl.drawArrays(gl.TRIANGLE_FAN, 1, 7);
の場合、1,2,3,4,5,6を使って描画します。1,2,3, 1,3,4, 1,4,5, 1,5,6という4つの三角形が描画されます。ラスタライズ、値の補間も行われ、描画領域の色が決まります。整数に対して位置が決まるんですが、今は整数しか使っていません。いずれ、この整数に対して登録したvec2やvec3の値が使える様になったりしますが、今は整数だけです。位置を決めるのはバーテックスシェーダ、描画領域の色を決めるのはフラグメントシェーダの仕事です。
このdrawArraysという命令は見ての通り、連番でしか整数を指定できません。これを連番でなく指定できるようにするのがインデックスバッファという仕組みです。それはあらかじめデータを用意しておくとします。
[1,2,3, 3,2,4, 7,8,9, 9,8,10]
とかです。それで、12だけ指定します。オフセットもありますね...まあ0とします。それで、0番から11番までの1,2,3,3,2,4,7,8,9,9,8,10という整数列ができて、これに対するTRIANGLESやTRIANGLE_FANが実行されます(この場合にFANを使ったら変なことになりそうですが)。つまり連番でない整数配列で処理を実行するための仕組みがインデックスバッファです。同じ頂点が再利用できるので、バッファの節約ができて便利です。もっともアトリビュートを用意した後の話ですが...
グローバルステート
glはグローバルの状態を隠蔽しています。あらゆる情報を隠蔽しています。それをグローバルステートと言います。自分も理解が十分でないんですが、今現時点でこんな感じだろうというのをここで説明しようと思います。どっちかというと自分の理解のためです。理解が進んだらこのモデルも破棄して、作り直すことになるでしょう。
globalState = {
bindingBuffer:{
arrayBuffer: null,
elementArrayBuffer: null,
uniformBuffer: null,
...//(他にもいろいろ)
},
//(他にもめっちゃいろいろ)
}
グローバル変数を用意してコードを書くでしょう。ああいうのが背後にあって、gl命令というかgl関数でいじられているんですが、外からは見えないようになっています。それがグローバルステートです。問い合わせることは可能ですが、負荷がかかるのでこっちで管理した方が楽な場合も多いです。とはいえ見えずとも存在を認識しておくだけで仕組みを理解しやすくなります。
bindingBufferというのはバインドされているwebGLBufferについての情報です。見ての通り、arrayBuffer枠(アトリビュートで使う)、elementArrayBuffer枠(エレメント描画で使う、つまりこの記事)、uniformBuffer枠(UBOで使う、理解不十分)などの枠があります。それぞれの枠にはひとつまでしかバインド(要するに値をセットするってこと)できません。違うものをバインドすれば上書きされ、nullをセットすればnullになります。デフォルトではすべてnullとなっています。
gl関数では、このバッファに対する処理を実行する関数がちょいちょいあって、その際にはnullでない値がセット、というかバインドされていることがマストとなっています。ゆえに引数に対象が出てきません。それが分かりにくさの原因と呼ばれることもちょいちょいあります:
これとか
なのできちんと意識しましょう、というお話です。
コード全文
とりあえずコードです。今回はアルファベットのFと数字の2を描画します。
play with indexBuffer
// indexBufferで遊ぼう。
// Fを描く
/*
webglの裏側
globalState = {
bindingBuffer:{
arrayBuffer:null,
elementArrayBuffer:null, // ←今回はここにバッファを紐付けようと思います
uniformBuffer:null,
...
}
}
*/
function setup() {
createCanvas(400, 400, WEBGL);
const vs_drawF =
`#version 300 es
uniform vec2 uPos[24];
uniform float uShift;
void main(){
vec2 p = uPos[gl_VertexID];
p = (p - vec2(1.5, 2.5)) / 8.0;
p.x += uShift;
gl_Position = vec4(p, 0.0, 1.0);
}
`;
const fs_drawF =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
`;
const gl = this._renderer.GL;
const pg = createShaderProgram(gl, {vs:vs_drawF, fs:fs_drawF});
// 頂点の位置をuniformで決める。いずれアトリビュートでもできるようになる。
const positionArray = [];
for(let y=0; y<6; y++){
for(let x=0; x<4; x++){
positionArray.push(x, y);
}
}
// インデックスの配列。これとTRIANGLESを組み合わせる。
const indexArray = [
// F. 54 indices
0,1,4, 4,1,5, 4,5,8, 8,5,9, 8,9,12, 12,9,13,
9,10,13, 13,10,14, 10,11,14, 14,11,15, 12,13,16, 16,13,17,
16,17,20, 20,17,21, 17,18,21, 21,18,22, 18,19,22, 22,19,23,
// 2. 66 indices
0,1,4, 4,1,5, 1,2,5, 5,2,6, 2,3,6, 6,3,7, 4,5,8, 8,5,9,
8,9,12, 12,9,13, 9,10,13, 13,10,14, 10,11,14, 14,11,15,
14,15,18, 18,15,19, 16,17,20, 20,17,21, 17,18,21, 21,18,22,
18,19,22, 22,19,23
];
// この時点ではなんにでも使える
const buf = gl.createBuffer();
// 今回はELEMENT_ARRAY_BUFFERという、インデックス描画用のセッティングをします。
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
// なお複数のターゲットに紐付けようとするとエラーが発生します。やっちゃだめよ。
//gl.bindBuffer(gl.ARRAY_BUFFER, buf);
// この処理は紐ついてるelementArrayBufferが対象なので引数にbufは出てきません。
// usageは「決していじらない」という意味の「STATIC_DRAW」を指定。
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indexArray), gl.STATIC_DRAW);
// 別に解除する必要はないが、基本的にあそこはnullに保っておくのがマナーです
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// ↑ これでglobalState.bindingBuffer.elementArrayBuffer = null; と同じ処理。
gl.useProgram(pg);
setUniformValue(gl, pg, "2fv", "uPos[0]", positionArray);
gl.clearColor(0.2, 0.3, 0.4, 1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
// 使うときにバインドするとそれが使われる仕組み
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
// 配列はUNSIGNED_BYTEで登録したので、
// ドローコールもUNSIGNED_BYTEで実行します。
setUniformValue(gl, pg, "1f", "uShift", -0.25);
gl.drawElements(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 0);
setUniformValue(gl, pg, "1f", "uShift", 0.25);
gl.drawElements(gl.TRIANGLES, 66, gl.UNSIGNED_BYTE, 54);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
gl.flush();
}
function createShaderProgram(gl, params = {}){
const {vs, fs} = params;
const vsShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vsShader, vs);
gl.compileShader(vsShader);
if(!gl.getShaderParameter(vsShader, gl.COMPILE_STATUS)){
console.log("vertex shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(vsShader));
return null;
}
const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fsShader, fs);
gl.compileShader(fsShader);
if(!gl.getShaderParameter(fsShader, gl.COMPILE_STATUS)){
console.log("fragment shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(fsShader));
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
gl.linkProgram(program);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
console.log("programのlinkに失敗しました");
console.error(gl.getProgramInfoLog(program));
return null;
}
// uniform情報を作成時に登録してしまおう
program.uniforms = getActiveUniforms(gl, program);
return program;
}
function getActiveUniforms(gl, pg){
const uniforms = {};
// active uniformの個数を取得。
const numActiveUniforms = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
console.log(`active uniformの個数は${numActiveUniforms}個です`);
for(let i=0; i<numActiveUniforms; i++){
// uniformの取得に使う変数はactiveUniformの個数に応じた整数。
// たとえば4つであれば0,1,2,3で取得できる。
const uniform = gl.getActiveUniform(pg, i);
console.log(uniform);
// locationはgetUniformLocationで取得できるがこれは整数ではない。内部で使われる
// オブジェクトである。uniformの登録にこのオブジェクトは必須。
const location = gl.getUniformLocation(pg, uniform.name);
// これはuniformに含まれていないので、付与して外で使えるようにする。
uniform.location = location;
uniforms[uniform.name] = uniform;
}
return uniforms;
}
function setUniformValue(gl, pg, type, name){
const {uniforms} = pg;
// 存在しない場合はスルー
if(uniforms[name] === undefined) return;
// 存在するならlocationを取得
const location = uniforms[name].location;
// nameのあとに引数を並べる。そのまま放り込む。
const args = [...arguments].slice(4);
switch(type){
case "1f": gl.uniform1f(location, ...args); break;
case "2f": gl.uniform2f(location, ...args); break;
case "3f": gl.uniform3f(location, ...args); break;
case "4f": gl.uniform4f(location, ...args); break;
case "1fv": gl.uniform1fv(location, ...args); break;
case "2fv": gl.uniform2fv(location, ...args); break;
case "3fv": gl.uniform3fv(location, ...args); break;
case "4fv": gl.uniform4fv(location, ...args); break;
case "1i": gl.uniform1i(location, ...args); break;
case "2i": gl.uniform2i(location, ...args); break;
case "3i": gl.uniform3i(location, ...args); break;
case "4i": gl.uniform4i(location, ...args); break;
case "1iv": gl.uniform1iv(location, ...args); break;
case "2iv": gl.uniform2iv(location, ...args); break;
case "3iv": gl.uniform3iv(location, ...args); break;
case "4iv": gl.uniform4iv(location, ...args); break;
}
if(type === "matrix2fv"||type==="matrix3fv"||type==="matrix4fv"){
const v = (args[0] instanceof Float32Array ? args[0] : new Float32Array(args[0]));
switch(type){
case "matrix2fv": gl.uniformMatrix2fv(location, false, v); break;
case "matrix3fv": gl.uniformMatrix3fv(location, false, v); break;
case "matrix4fv": gl.uniformMatrix4fv(location, false, v); break;
}
}
}
実行結果:
バッファを作る
webGLBufferという概念があります。よくわかってないです。描画領域をこれによって確保している、というイメージを持ってます。インデックス描画のための配列の格納に使ったり、アトリビュート用の値の列を入れるのに使ったりします。こういうのは使ってるうちに理解が深まるものなので、あんま考えなくていいです。
まずこれを作ります。
// この時点ではなんにでも使える
const buf = gl.createBuffer();
// 今回はELEMENT_ARRAY_BUFFERという、インデックス描画用のセッティングをします。
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
// なお複数のターゲットに紐付けようとするとエラーが発生します。やっちゃだめよ。
//gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.createBuffer()を使うとwebGLBufferがクリエイトされます。これをグローバルステートにバインドします。先ほどの、
globalState = {
bindingBuffer:{
arrayBuffer: null,
elementArrayBuffer: null, // ←今回はここにバッファを紐付けようと思います
uniformBuffer: null,
...
}
}
を見ながら説明しますが、arrayBufferのところにセットするなら
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
としますし、elementArrayBufferのところにセットするなら、
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
とします。第一引数はターゲットといって、これでどこにバインドするか決めます。なお、複数の異なるターゲットにバインドすることはできません。上のコードでコメントアウトを外すと、INVALID_OPERATIONをくらいます。
やらないでね。
今回はインデックスバッファとして使いたいのでELEMENT_ARRAY_BUFFERを指定します。以降は、ここにしかバインドできません。
やりたいこと。描きたいもの。
とりあえずシェーダを見ます。
#version 300 es
uniform vec2 uPos[24];
uniform float uShift;
void main(){
vec2 p = uPos[gl_VertexID];
p = (p - vec2(1.5, 2.5)) / 8.0;
p.x += uShift;
gl_Position = vec4(p, 0.0, 1.0);
}
#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
uPos[24]には整数に応じた位置情報が入ってます。どう入ってるかというと、
(0,0), (1,0), (2,0), (3,0), (1,1), (2,1), ..., (3,5)
が入っています。
// 頂点の位置をuniformで決める。いずれアトリビュートでもできるようになる。
const positionArray = [];
for(let y=0; y<6; y++){
for(let x=0; x<4; x++){
positionArray.push(x, y);
}
}
こういうユニフォームを使った力業に頼らなくても描画ができるようになるにはアトリビュートが必要ですが、いくつも同時には説明できないので、とりあえず後回しです。で、これにより次のような図形を描画したいです。
こんな感じで頂点の並び順を指定して、Fの字を作ります。あるいは、
こんな感じで頂点の並び順を指定して、2の字を作りたいです。これらは同じ頂点を複数回、それも複雑に使うので、drawArraysではできません。そこでインデックス描画です。
インデックス配列を登録する
なにはともあれ、まずはここに出てくるインデックスの並びを全部まとめて配列にぶち込んでしまいます。
// インデックスの配列。これとTRIANGLESを組み合わせる。
const indexArray = [
// F. 54 indices
0,1,4, 4,1,5, 4,5,8, 8,5,9, 8,9,12, 12,9,13,
9,10,13, 13,10,14, 10,11,14, 14,11,15, 12,13,16, 16,13,17,
16,17,20, 20,17,21, 17,18,21, 21,18,22, 18,19,22, 22,19,23,
// 2. 66 indices
0,1,4, 4,1,5, 1,2,5, 5,2,6, 2,3,6, 6,3,7, 4,5,8, 8,5,9,
8,9,12, 12,9,13, 9,10,13, 13,10,14, 10,11,14, 14,11,15,
14,15,18, 18,15,19, 16,17,20, 20,17,21, 17,18,21, 21,18,22,
18,19,22, 22,19,23
];
見比べてみてください。一緒ですね。これをどう利用するかというと、さっき作ったバッファに入れます。
// この処理は紐ついてるelementArrayBufferが対象なので引数にbufは出てきません。
// usageは「決していじらない」という意味の「STATIC_DRAW」を指定。
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indexArray), gl.STATIC_DRAW);
// 別に解除する必要はないが、基本的にあそこはnullに保っておくのがマナーです
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// ↑ これでglobalState.bindingBuffer.elementArrayBuffer = null; と同じ処理。
bufferDataという関数は、配列を用意したバッファに紐付ける処理で、アトリビュートの方でも出てきます。初登場です(実はバイト単位でスペースだけ確保する裏技もあるんですが、ややこしくなるのでスルー)。配列は型付配列です。インデックスは0以上の整数ですから、ここに出てくる可能性があるのは、
Uint8Array, Uint16Array, Uint32Array
だけです。だけ、と思ってOKです。これしか出てこないです。で、例によってjsの配列では使えないので、それに基づいて生成しています。Uint8Arrayには0~255の整数が入ります。今回は小さいので、これでいいでしょう。
STATIC_DRAWのところはusageといって、ぶっちゃけよくわかんないんですが、ここをちゃんと指定すると処理系がよしなに取り計らってくれるそうです。ほんとに?さぁ。ベンチマークでも取らない限り分かんないと思います。それはさておき、今回は当然ですが「一切いじらない」ので、そういう意味の「STATIC_DRAW」を指定します。
順番が前後しますが、この処理の対象のwebGLBufferがどれなのか説明します。これです↓
globalState = {
bindingBuffer:{
arrayBuffer: null,
elementArrayBuffer: buf, // これ!!!
uniformBuffer: null,
...
}
}
bindBuffer(gl.ELEMENT_ARRAY_BUFFER,buf)でここにバッファが入るでしょう。gl.buffewDataの対象はこれです。第一引数でgl.ELEMENT_ARRAY_BUFFERを指定しているので、グローバルステートのbindingBuffer.elementArrayBufferが対象となるわけです。ここがARRAY_BUFFERならarrayBufferの方が対象となります。そういう仕組みなので、バッファ自体を指定することは無いのです。また、nullの場合エラーを食らいます。
データをバッファしたら、そのあとでnullをバインドしておきます。これで上のあそこにはnullが入ります。このコードの場合は別にはめっぱなしでもいいんですが、変な事故を防ぐために、基本的にwebGLBufferはバインドが終わったら外しておくのが基本です。webGLBufferでない場合は、その限りではありませんが...
webGLBufferをconsole.logに入れるな
文字通りの意味です。入れないでください。フリーズします。確実に。中身が見たいなって思うときがあるかもしれませんが、堪えてください。ダメ絶対。
やらないでね。
(追記:p5.jsで追加アトリビュートを使う際にp5.RenderBufferという、要するにp5が用意したwebGLBufferのラッパ的な何か、を使うんですが、これをconsole.logしようとして痛い目にあったことがあります。そういうことがあるので、console.logに入れてはいけないオブジェクトがあるということは知っておくといいと思います。少なくとも自分はその時生まれて初めてそういうことを知りました。)
インデックスバッファを描画に使う
これで作成したwebGLBufferをインデックスバッファとして使う準備が整いました。早速使います。
gl.useProgram(pg);
setUniformValue(gl, pg, "2fv", "uPos[0]", positionArray);
gl.clearColor(0.2, 0.3, 0.4, 1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
// 使うときにバインドするとそれが使われる仕組み
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
// 配列はUNSIGNED_BYTEで登録したので、
// ドローコールもUNSIGNED_BYTEで実行します。
setUniformValue(gl, pg, "1f", "uShift", -0.25);
gl.drawElements(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 0);
setUniformValue(gl, pg, "1f", "uShift", 0.25);
gl.drawElements(gl.TRIANGLES, 66, gl.UNSIGNED_BYTE, 54);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
gl.flush();
とりあえずプログラムを走らせて、位置データのuniformを登録しておきます。なお説明を省きましたがuniformの登録処理を簡略化しました。uniformを理解していればわかるので特に説明はしません。プログラムにuniformの情報を入れたり、いろいろやってます。いちいちlocation取得するの面倒ですからね。
いつものように単色で塗りつぶし、次にバインド処理です。
// 使うときにバインドするとそれが使われる仕組み
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buf);
これであそこにまたbufがセットされます。そのうえでドローコールを実行します。インデックスバッファを利用する場合は、drawElementsを使います。
setUniformValue(gl, pg, "1f", "uShift", -0.25);
gl.drawElements(gl.TRIANGLES, 54, gl.UNSIGNED_BYTE, 0);
uShiftは左にずらすためのものです。drawElementsを使うと、整数列がそのときバインドされているインデックスバッファに登録した整数列の内容に基づいて決定されます。54というのは単純に「いくつの整数を使うか」なんですが、そのあとのUNSIGNED_BYTEにある通り、バイト単位でフェッチして整数を決定します。ここ重要なんですが、drawElementsの引数がUNSIGNED_BYTEなので54個のバイト単位の整数を使います。で、あとの0ですが、これはインデックスバッファサイドの0バイトオフセットという意味です。つまり54の方は「個数」であとの0は「バイト数」での指定となります。
これで「F」が描画され、次のこれで「2」が描画されます。
setUniformValue(gl, pg, "1f", "uShift", 0.25);
gl.drawElements(gl.TRIANGLES, 66, gl.UNSIGNED_BYTE, 54);
末尾の54は作成に要したUint8Arrayの方で54バイト進めたという意味で、バイト整数なので54個という意味でOKです。そこから先を、66個のUNSIGNED_BYTEとして解釈して66個の、まあそのままですが、使われます。これで整数列が決定され、描画されます。
基本的に、インデックスバッファの作成に使ったときの型をここではそのまま指定します。すなわち、
Uint8Array: UNSIGNED_BYTE, Uint16Array: UNSIGNED_SHORT, Uint32Array: UNSIGNED_INT
です。同じものを使えば変な事故は起こりません。安全です。ただバイト数と個数の違いには注意が必要です。たとえばUint16ArrayとUNSIGNED_SHORTで同じことをする場合、オフセットは2倍の108となります。そこ以外は一緒です。
また、レファレンスにも書いてありますが、67とか指定した場合、描画に失敗します。
setUniformValue(gl, pg, "1f", "uShift", 0.25);
// 描画不履行
gl.drawElements(gl.TRIANGLES, 67, gl.UNSIGNED_BYTE, 54);
はみ出すのは厳禁ということです。一切描画されません。きちんと指定しましょう。まあ同じ配列で複数のインデックス配列を使うケースはおそらくまれなので、あんま気にしなくていいかもしれません。
このように、エレメント描画ではそのときのグローバルステートに基づいて、紐ついたインデックスバッファ、というかbindingBuffer.elementArrayBufferが使われます。これを切り替えることで別のバッファが使われたりします。またnullであれば、描画に失敗します。
描画が終わったので、バインドを切っています。これもスケッチによっては不要な処理なので、臨機応変に対応しましょう。
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
おわりに
インデックスバッファが本領発揮するのは3Dにおけるポリゴン描画です。頂点データの個数を大幅に削減できるので必須です。例のサイトでもそういうのを説明してからトーラスとか描画しているんですが、3D描画はやることが多すぎて本質が隠れてしまうので、この記事ではインデックスバッファの利用法に焦点を置いて説明しようと思いました。
なお、自分はこれまでwebGLでいろいろ書いてきたんですが(p5の枠を無視して...できないことが多すぎるので)、理解してないことが多すぎることに気づいたので、今こうやって基礎の部分を固めています。フィーリングでどうにかなる部分も多いんですが、基礎は大事です。基礎というのは「ここって要するにどうなっているのか」というのをカテゴリーごとに徹底的に追及するということです。wgldでもいいんですが、あれはしっかり読むと文章量の割に穴が大きいので、レファレンスなどで相当補わないとしっかり理解するのは難しいし、誤解も発生しやすいことが、いろいろ勉強してようやくわかってきました。それでやっています。しかしどっちかというとそれが楽しいからやっている側面が大きいです。自分には映えるものを作るといったことができないので(作りたい気持ちも無いので)、今後もこうやって基礎を調べる的なことを延々とやり続けるつもりです。作品作りは、したい人がすればいいと思います。
ここまでお読みいただいてありがとうございました。
(まあそうはいってもサンプルは作るけどね...動く作品もあった方がいいと思いました:indexBuffer loop)