この記事は,WebGL Advent Calendar 2016 15日目の記事です.
WebGLでシェーダーリフレクションを利用して,自動的に頂点情報の設定をしてくれる機能を実装してみました.ここでのリフレクションとは,反射のことではなく,実行時に型などの情報を取得したりできる機能のことです.
有効な頂点属性数の取得
WebGLでは,getProgramParamterメソッドにシェーダープログラムオブジェクトと共に,gl.ACTIVE_ATTRIBUTESを指定すると,有効な頂点属性の数を取得できます.
// 有効な頂点属性の数を取得
gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
有効な頂点属性の情報取得
次に,getActiveAttribメソッドを使うと,頂点属性の名前などが入ったWebGLActiveInfoオブジェクトを取得できます.
// indexは0から上記で取得した数未満の範囲を指定する
var attribute = gl.getActiveAttrib(program, index);
console.log(attribute.name); // 頂点属性の名前
console.log(attribute.size); // GLSL ES 1.0の場合,頂点属性のサイズは常に1
console.log(attribute.type); // FLOAT,FLOAT_VECn,FLOAT_MATnなど (nは2から4)
頂点属性に関する情報の算出
この情報を元に,enableVertexAttribArrayメソッドや,vertexAttribPointerに設定する値を求めていきます.
ただし,stride(1頂点あたりのバイトサイズ)やoffset(1頂点分のデータの先頭からのバイトオフセット)は,
別途設定します.
以下のコードは,頂点属性の情報をまとめて管理するためのクラスです.
setupメソッドにWebGLRenderingContextオブジェクトを渡すと,設定されている値でenableVertexAttribArrayや,vertexAttribPointerを呼び出します.
class VertexAttribute
{
// gl : WebGLRenderingContext
// program : シェーダープログラム
// index : 有効な頂点属性のインデックス
constructor(gl, program, index)
{
// 有効な頂点属性の情報を取得
var attribute = gl.getActiveAttrib(program, index);
// 頂点属性の位置を求める
this.index = gl.getAttribLocation(program, attribute.name);
// 行列用
this.count = 1;
// 型によってsizeやcountを設定する
switch(attribute.type)
{
case gl.FLOAT:
this.size = 1;
break;
case gl.FLOAT_MAT2:
this.count = 2;
// fall-through
case gl.FLOAT_VEC2:
this.size = 2;
break;
case gl.FLOAT_MAT3:
this.count = 3;
// fall-through
case gl.FLOAT_VEC3:
this.size = 3;
break;
case gl.FLOAT_MAT4:
this.count = 4;
// fall-through
case gl.FLOAT_VEC4:
this.size = 4;
break;
}
// GLSL ESの場合,gl.FLOAT限定
this.type = gl.FLOAT;
this.normalized = gl.FALSE;
this.stride = 0;
this.offset = 0;
}
// 頂点属性の型を元にバイトサイズを返す
byte_size()
{
// FLOATのみでFLOATは4バイトという前提
return this.size * 4 * this.count;
}
// 頂点属性の設定を行う
setup(gl)
{
for(var i = 0; i < this.count; ++i)
{
gl.enableVertexAttribArray(this.index + i);
gl.vertexAttribPointer(
this.index + i,
this.size,
this.type,
this.normalized,
this.stride,
this.offset + 4 * this.size * i
);
}
}
}
頂点属性をまとめて管理する
ここでは,1つのバッファに全ての頂点属性の値がまとまっている前提で,上記のクラスをまとめるMaterialクラスを作ります.
class Material
{
// gl : WebGLRenderingContext
// program : シェーダープログラム
constructor(gl, program)
{
// 有効な頂点属性の数を取得
var n = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
// 有効な頂点属性分の配列を用意
this.attributes = new Array(n);
// 頂点属性を生成し,strideの計算と同時にoffsetを設定していく
var stride = 0;
for(var i = 0; i < n; ++i)
{
this.attributes[i] = new VertexAttribute(gl, program, i);
this.attributes[i].offset = stride;
stride += this.attributes[i].byte_size();
}
// strideを設定する
for(var i = 0; i < n; ++i)
{
this.attributes[i].stride = stride;
}
}
setup(gl)
{
// 全ての頂点属性の設定を行う
this.attributes.forEach(function(attribute){
attribute.setup(gl);
});
}
}
記述例
上記のクラスを使うと,こんな風にWebGLのコードが書けます.
var program = gl.createProgram();
// ...
// シェーダープログラムをリンクしたりする
// ...
var material = new Material(gl, program);
// ...
// 色々処理
// ...
gl.useProgram(program);
// ...
// バッファのバインド
// ...
material.setup(gl);
// ...
// 描画
// ...
まとめ
頂点バッファに頂点属性のデータが連続で格納されている前提になりますが,getActiveAttribメソッドを利用することでシェーダーを変更しても,ほとんどコードを変えずに済みます.
同様に,getProgramParameterメソッドにgl.ACTIVE_UNIFORMSを渡すと有効なユニフォームの数を取得でき,getActiveUniformでユニフォームの情報を取得できるので,そちらも利用すると,更にシェーダの変更に強いコードが書ける・・・かもしれません.