WebGLのattributeで行列(mat3, mat4)を使用する

この記事では、WebGLのattributeで行列(`mat3`, `mat4`)を使用する方法について解説します。
具体的な使用例として、拡張機能のGeometry Instancingを使用しているときに個々のインスタンスの移動・回転・スケールを表す行列をattributeとして使用する場面を考えます。
サンプルでは、トーラスをGeometry Instancingで大量に描画し、各トーラスの移動・回転・スケールを行列形式のattributeでシェーダーに渡しています。
![SampleWebGlMatAttribute.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/81deb233-aea3-8b04-ce62-398c2261585a.png)

動くサンプルとソースコードは以下に置いてあります。
[aadebdeb/SampleWebGlMatAttribute: Sample of matrix attributes in WebGL](https://github.com/aadebdeb/SampleWebGlMatAttribute)

このサンプルでは、数学ライブラリとして[glMatrix](https://glmatrix.net/)を使用しています。

--- 

今回使用する頂点シェーダーは以下のようになります。インスタンスごとのモデル行列`aInstanceMatrix`と法線をオブジェクト座標からワールド座標に変換する行列`aInstanceNormalMatrix`が行列形式のattributeになります。

```glsl
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute mat4 aInstanceMatrix; // インスタンスのモデル行列
attribute mat3 aInstanceNormalMatrix; // インスタンスの法線モデル行列
attribute vec3 aInstanceColor; // インスタンスの色

uniform mat4 uViewProjectionMatrix;

varying vec3 vColor;
varying vec3 vWorldNormal;

void main() {
  gl_Position = uViewProjectionMatrix * aInstanceMatrix * vec4(aPosition, 1.0);
  vColor = aInstanceColor;
  vWorldNormal = aInstanceNormalMatrix * aNormal;
}
```

JavaScript側では、最初にデータの生成を行います。ランダムに移動・回転・スケールを決定した行列(`instanceMat`)をインスタンスごとに生成して、その行列を一つの配列(`instanceMatArray`)にまとめます。その配列をもとにVBOを作成します。

```js
const instanceMatArray = [];
const instanceNormalMatArray = [];
for (let i = 0; i < torusNum; ++i) {
  const instanceMat = mat4.create(); // Float32Array
  const rotation = quat.create();
  quat.fromEuler(rotation, 360 * Math.random(), 360 * Math.random(), 360 * Math.random());
  const translation = vec3.fromValues(50.0 * (2.0 * Math.random() - 1.0), 50.0 * (2.0 * Math.random() - 1.0), 50.0 * (2.0 * Math.random() - 1.0));
  const scale = vec3.fromValues(0.5 + 1.5 * Math.random(), 0.5 + 1.5 * Math.random(), 0.5 + 1.5 * Math.random());
  mat4.fromRotationTranslationScale(instanceMat, rotation, translation, scale);
  instanceMatArray.push(...instanceMat);
  const instanceNormalMat = mat3.create(); // Float32Array、法線モデル行列はモデル行列の逆行列の転置長列
  mat3.fromMat4(instanceNormalMat, instanceMat);
  mat3.transpose(instanceNormalMat, instanceNormalMat);
  mat3.invert(instanceNormalMat, instanceNormalMat);
  instanceNormalMatArray.push(...instanceNormalMat);
}
const instanceMatVbo = createVbo(gl, new Float32Array(instanceMatArray));
const instanceNormalMatVbo = createVbo(gl, new Float32Array(instanceNormalMatArray));
```

作成したVBOをattributeとして設定していきます。行列のattributeの場合は行列の各列ごとに設定を行います。`getAttribLocation`で取得したインデックスが行列の1列目用、その次のインデックスが2列目用、3つ目が3列目用...というようになります。さきほど生成したVBOの配列に合わせて`vertexAttribPointer`の`stride`引数と`offset`引数を適切に設定します。
ここでは`aInstanceMatrix`が`mat4`なので4列分、`aInstanceNormalMatrix`が`mat3`なので3列分の設定を行っています。

```js
const extInstancedArray = gl.getExtension('ANGLE_instanced_arrays')

function getAttribLocs(gl, program, names) {
  const locs = new Map();
  names.forEach(name => locs.set(name, gl.getAttribLocation(program, name)));
  return locs;
}
const attribLocs = getAttribLocs(gl, program, ['aPosition', 'aNormal', 'aInstanceMatrix', 'aInstanceNormalMatrix', 'aInstanceColor']);

gl.bindBuffer(gl.ARRAY_BUFFER, instanceMatVbo);
gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix'));
gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 1);
gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 2);
gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 3);
gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix'), 4, gl.FLOAT, false, 64, 0); // 64 = 16 * 4byte
gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 1, 4, gl.FLOAT, false, 64, 16);
gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 2, 4, gl.FLOAT, false, 64, 32);
gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 3, 4, gl.FLOAT, false, 64, 48);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix'), 1);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 1, 1);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 2, 1);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 3, 1);

gl.bindBuffer(gl.ARRAY_BUFFER, instanceNormalMatVbo);
gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix'));
gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix') + 1);
gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix') + 2);
gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix'), 3, gl.FLOAT, false, 36, 0); // 36 = 9 * 4byte
gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix') + 1, 3, gl.FLOAT, false, 48, 12);
gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix') + 2, 3, gl.FLOAT, false, 48, 24);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix'), 1);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix') + 1, 1);
extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix') + 2, 1);
```

準備が終わったら、`drawElementsInstancedANGLE`で描画を実行します。

```js
extInstancedArray.drawElementsInstancedANGLE(gl.TRIANGLES, torus.indices.length, gl.UNSIGNED_SHORT, 0, torusNum);
```

---

この記事では`mat3`、`mat4`のattributeを使用する方法について記述しました。
今回は使用しませんでしたが、`mat2`のattributeでも同様の方法が利用できると思います。

## 参考

- [javascript \- WebGL: How to bind values to a mat4 attribute? \- Stack Overflow](https://stackoverflow.com/questions/38853096/webgl-how-to-bind-values-to-a-mat4-attribute)
- [LearnOpenGL \- Instancing](https://learnopengl.com/Advanced-OpenGL/Instancing)