WebGLでシェーダーリフレクション

この記事は，[WebGL Advent Calendar 2016](http://qiita.com/advent-calendar/2016/webgl) 15日目の記事です．

WebGLでシェーダーリフレクションを利用して，自動的に頂点情報の設定をしてくれる機能を実装してみました．ここでのリフレクションとは，反射のことではなく，実行時に型などの情報を取得したりできる機能のことです．

# 有効な頂点属性数の取得
WebGLでは，getProgramParamterメソッドにシェーダープログラムオブジェクトと共に，gl.ACTIVE_ATTRIBUTESを指定すると，有効な頂点属性の数を取得できます．

```js
// 有効な頂点属性の数を取得
gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
```

# 有効な頂点属性の情報取得
次に，getActiveAttribメソッドを使うと，頂点属性の名前などが入ったWebGLActiveInfoオブジェクトを取得できます．

```js
// 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を呼び出します．

```js:VertexAttribute.js
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クラスを作ります．

```js:Material.js
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のコードが書けます．

```js
var program = gl.createProgram();
// ...
// シェーダープログラムをリンクしたりする
// ...
var material = new Material(gl, program);

// ...
// 色々処理
// ...

gl.useProgram(program);
// ...
// バッファのバインド
// ...
material.setup(gl);

// ...
// 描画
// ...
```

# まとめ
頂点バッファに頂点属性のデータが連続で格納されている前提になりますが，getActiveAttribメソッドを利用することでシェーダーを変更しても，ほとんどコードを変えずに済みます．

同様に，getProgramParameterメソッドにgl.ACTIVE_UNIFORMSを渡すと有効なユニフォームの数を取得でき，getActiveUniformでユニフォームの情報を取得できるので，そちらも利用すると，更にシェーダの変更に強いコードが書ける・・・かもしれません．