WebGL2のGeometry InstancingとTransform Feedbackで大量のメッシュを動かす

WebGL2のTransform Feedbackを調べると大量のパーティクルを動かす例をよく見かけますが、これにGeometry Instancingを組み合わせると大量のメッシュを動かすことができるのではと思い、試してみました。

作ったものは以下のようになります。球メッシュに重力で力を加えて地面と壁で反射するようにしてまいます。
https://aadebdeb.github.io/Sample_WebGL2_GeometryInstancing_with_TransformFeedback/index.html

![WebGL2_GeometryInstancing_TransformFeedback.gif](https://qiita-image-store.s3.amazonaws.com/0/61569/5f889344-5323-ec98-9b3f-eab0798efdeb.gif)

ソースコードはGithubに置いておきました。
https://github.com/aadebdeb/Sample_WebGL2_GeometryInstancing_with_TransformFeedback

---

最初に初期化処理おこないます。

まず、球メッシュの頂点の位置と法線を格納するVBOと頂点インデックスを格納するIBOを作成します。この球メッシュをGeometry Instancingで大量に描画します。

```js
sphere = createSphere(data.sphere.radius, data.sphere.thetaSegment, data.sphere.phiSegment);
sphereIbo = createIbo(gl, sphere.indices);
vertexPositionVbo = createVbo(gl, sphere.positions);
vertexNormalVbo = createVbo(gl, sphere.normals);
```

その後、各球の位置と速度、色を格納するVBOを作成します。位置と速度はTransform Feedbackで更新するのでVBOを2つ作成しておきます。`R`のほうが読み込み用(read)、`W`のほうが書き込み用(write)です。
Transform Feedbackで位置と速度の更新をおこなうたびに`swapParticleVbos`関数で書き込み用と読み込み用のVBOを入れ替えます。

```js
sphereNum = data.num;
const positions = new Float32Array(Array.from({length: sphereNum * 3}, () => {
  return (2.0 * Math.random() - 1.0);
}));
const velocities = new Float32Array(sphereNum * 3);
for (let i = 0; i < sphereNum; i++) {
  velocities[3 * i] = (2.0 * Math.random() - 1.0) * 0.5;
  velocities[3 * i + 1] = 0.0;
  velocities[3 * i + 2] = (2.0 * Math.random() - 1.0) * 0.5;
}
const colors = new Float32Array(sphereNum * 3);
for (let i = 0; i < sphereNum; i++) {
  const rgb = hsvToRgb(Math.floor(Math.random() * 360.99), 1, 1);
  colors[3 * i] = rgb[0];
  colors[3 * i + 1] = rgb[1];
  colors[3 * i + 2] = rgb[2];
}
positionVboR = createVbo(gl, positions, gl.DYNAMIC_COPY);
velocityVboR = createVbo(gl, velocities, gl.DYNAMIC_COPY);
positionVboW = createVbo(gl, new Float32Array(sphereNum * 3), gl.DYNAMIC_COPY);
velocityVboW = createVbo(gl, new Float32Array(sphereNum * 3), gl.DYNAMIC_COPY);
colorVbo = createVbo(gl, colors);

const swapParticleVbos = function() {
  const tmpP = positionVboR;
  const tmpV = velocityVboR;
  positionVboR = positionVboW;
  velocityVboR = velocityVboW;
  positionVboW = tmpP;
  velocityVboW = tmpV;
}
```

初期化処理を行った後にレンダリングループでTransform Feedbackによる各球の位置と速度の更新、Geometry Instancingによるレンダリングを繰り返しおこないます。

Transform Feedbackで位置と速度の更新をおこなうJavaScriptのコードと頂点シェーダーは以下のようになります。`positionVboR`と`velocityVboR`を入力にして、`positionVboW`と`velocityVboW`が出力になるようにします。更新が終了したら`swapParticleVbos`でVBOを入れ替えます。

```js
gl.useProgram(updateProgram);
gl.uniform1f(updateUniforms['u_deltaTime'], elapsedTime);
[positionVboR, velocityVboR].forEach((vbo, i) => {
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
  gl.enableVertexAttribArray(i);
  gl.vertexAttribPointer(i, 3, gl.FLOAT, false, 0, 0);
});
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionVboW);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, velocityVboW);
gl.enable(gl.RASTERIZER_DISCARD);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, sphereNum);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
swapParticleVbos();
```

```glsl

#version 300 es

layout (location = 0) in vec3 i_position;
layout (location = 1) in vec3 i_velocity;

out vec3 o_position;
out vec3 o_velocity;

uniform float u_deltaTime;

vec3 GRAVITY_FORCE = vec3(0.0, -9.8, 0.0);
float MASS = 10.0;

void main(void) {
  vec3 velocity = i_velocity;
  vec3 position = i_position;

  velocity += u_deltaTime * GRAVITY_FORCE / MASS;
  position += u_deltaTime * velocity;

  if (position.x <= -1.0 || position.x >= 1.0) {
    velocity.x *= -1.0;
    position.x += u_deltaTime * velocity.x;
  }
  if (position.y <= -1.0 || position.y >= 1.0) {
    velocity.y *= -1.0;
    position.y += u_deltaTime * velocity.y;
  }
  if (position.z <= -1.0 || position.z >= 1.0) {
    velocity.z *= -1.0;
    position.z += u_deltaTime * velocity.z;
  }

  o_position = position;
  o_velocity = velocity;
}
```

Geometry Instancingでレンダリングを行うためのJavaScriptのコードと頂点シェーダーは以下のようになっています。Transform Feedbackで先ほど出力先にした`positionVboR`(すでにswap済みなので`W`ではなく`R`)をそのままレンダリングに利用しています。頂点シェーダーでは`position`に球メッシュの各頂点の位置(vertexPositionVbo内の値)が、`instancePosition`に球の中心位置(`positionVboR`内の値)が入っているので、加算して最終的な頂点位置を求めています。

```js
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0.0, 0.0, canvas.width, canvas.height);
gl.useProgram(renderProgram);
gl.uniformMatrix4fv(renderUniforms['u_mvpMatrix'], false, mvpMatrix.elements);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereIbo);
[vertexPositionVbo, vertexNormalVbo, colorVbo, positionVboR].forEach((vbo, i) => {
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
  gl.enableVertexAttribArray(i);
  gl.vertexAttribPointer(i, 3, gl.FLOAT, false, 0, 0);
});
gl.vertexAttribDivisor(2, 1);
gl.vertexAttribDivisor(3, 1);
gl.drawElementsInstanced(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0, sphereNum);
```

```glsl
#version 300 es

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec3 color;
layout (location = 3) in vec3 instancePosition;

out vec3 v_color;
out vec3 v_normal;

uniform mat4 u_mvpMatrix;

void main(void) {
  v_color = color;
  v_normal = (u_mvpMatrix * vec4(normal, 0.0)).xyz;
  vec3 pos = position + instancePosition * 500.0;
  gl_Position = u_mvpMatrix * vec4(pos, 1.0);
}
```

---

パフォーマンスの比較用に同じ操作を[Geometry Instancingのみ](https://aadebdeb.github.io/Sample_WebGL2_GeometryInstancing_with_TransformFeedback/benchmark/only_instancing.html)、[Transform Feedbackのみ](https://aadebdeb.github.io/Sample_WebGL2_GeometryInstancing_with_TransformFeedback/benchmark/only_transformfeedback.html)、[Geometry InstancingもTransform Feedbackもなし](https://aadebdeb.github.io/Sample_WebGL2_GeometryInstancing_with_TransformFeedback/benchmark/no_instancing_and_transformfeedback.html)で行うバージョンを作りました。

こうしてみるとGeometry Instancingの有無はパフォーマンスに影響しますが、Transform Feedbackはそこまで変わらないですね...