WebGL2でDeferred Rendering

WebGL2でDeferred Renderingを実装してみたので解説したいと思います。はじめにお断りをさせていただくと、この記事は試してみた系の記事なので最適化・効率化はしていません。本格的に使用する場合はバッファーの効率的な利用や必要な精度などを検討する必要があると思います（私は詳しくないのでよくわかってないですが...）。

まず、Forward Renderingと呼ばれる通常のレンダリングについて確認します。Forward Rederingでは各3Dオブジェクトについて頂点シェーダーで頂点の位置を計算し、フラグメントシェーダーで法線やライトの情報をもとにライティングを行います。
Deferred Renderingでも頂点シェーダーで各3Dオブジェクトについて頂点の位置を計算しますが、位置や法線、色などライティングに必要な情報をGBufferというバッファに書き出すだけで、各3Dオブジェクトごとにはライティングの処理を行いません。すべての3Dオブジェクトの情報をGBufferに書き出した後に、その情報をもとにポストエフェクト的にライティング処理を行います。フラグメントシェーダーでのライティングの計算を減らせるので大量のライトを扱えたり、GBufferの情報をもとにポストエフェクトを加えることができるという利点があります。

今回作成したサンプルはGithubに置いておきました。ソースコード全体はそちらを確認してください。
https://github.com/aadebdeb/Sample_WebGL2_DeferredRendering

まずはGBufferを作成します。今回はPhong Shadingでライティング処理を行うので、位置・法線・色（アルベド、スペキュラーの強度）を保存するバッファーをそれぞれ作成しています。今回は深度テスト以外では使用しませんが、深度バッファーもテクスチャにしておきます。Multiple Render Targets(MRT)という一度のドローコールで複数のバッファーに出力する機能を`gl.drawBuffers`で利用できるようにします。

```js
function createTexture(gl, sizeX, sizeY, internalFormat, format, type) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, sizeX, sizeY, 0, format, type, null);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.bindTexture(gl.TEXTURE_2D, null);
  return texture;
}

const createGBuffer = function(sizeX, sizeY) {
  const framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

  // 位置バッファーの作成
  const positionTexture = createTexture(gl, sizeX, sizeY, gl.RGBA32F, gl.RGBA, gl.FLOAT);
  gl.bindTexture(gl.TEXTURE_2D, positionTexture);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, positionTexture, 0);

  // 法線バッファーの作成
  const normalTexture = createTexture(gl, sizeX, sizeY, gl.RGBA32F, gl.RGBA, gl.FLOAT);
  gl.bindTexture(gl.TEXTURE_2D, normalTexture);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, normalTexture, 0);

  // 色バッファーの作成
  const colorTexture = createTexture(gl, sizeX, sizeY, gl.RGBA32F, gl.RGBA, gl.FLOAT);
  gl.bindTexture(gl.TEXTURE_2D, colorTexture);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, colorTexture, 0);

  // 深度バッファーの作成
  const depthTexture = createTexture(gl, sizeX, sizeY, gl.DEPTH_COMPONENT32F, gl.DEPTH_COMPONENT, gl.FLOAT);
  gl.bindTexture(gl.TEXTURE_2D, depthTexture);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);

  // Multiple Render Targetsで位置バッファー、法線バッファー、色バッファーに一度のドローコールで出力できるようにする
  gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.bindTexture(gl.TEXTURE_2D, null);
  return {
    framebuffer: framebuffer,
    positionTexture: positionTexture,
    normalTexture: normalTexture,
    colorTexture: colorTexture,
    depthTexture: depthTexture
  };
};
```

作成したGBufferを出力先のフレームバッファにして、各3Dオブジェクトのレンダリングを行います。
頂点シェーダーではForward Rendringのときと同じように頂点の位置を行列で変換しています。フラグメントシェーダーでは単純にワールド座標系の頂点位置、頂点法線、そして色をMRTでそれぞれバッファーに書き出しています。

```glsl:頂点シェーダー
#version 300 es

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

out vec3 v_position;
out vec3 v_normal;

uniform mat4 u_modelMatrix;
uniform mat4 u_normalMatrix;
uniform mat4 u_mvpMatrix;

void main(void) {
  vec4 position = vec4(i_position, 1.0);
  v_position = (u_modelMatrix * position).xyz;
  v_normal = (u_normalMatrix * vec4(i_normal, 0.0)).xyz;
  gl_Position = u_mvpMatrix * position;
}
```

```glsl:フラグメントシェーダー
#version 300 es

precision highp float;

in vec3 v_position;
in vec3 v_normal;

layout (location = 0) out vec3 o_position;
layout (location = 1) out vec3 o_normal;
layout (location = 2) out vec4 o_color;

uniform vec4 u_color;

void main(void) {
  o_position = v_position;
  o_normal = normalize(v_normal);
  o_color = u_color;
}
```

すべての3Dオブジェクトのレンダリングが終わった後のGBufferは以下のようになります。左上から時計回りに位置バッファー、法線バッファー、色バッファーのスペキュラー成分、色バッファーのアルベド成分を表しています。
![gbuffer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/730c5096-5efc-9d69-a0b7-ee80221b0e8f.png)

深度バッファーは以下のようになっています。
(確認しやすいように値を変換しています。変換方法については[別記事](https://qiita.com/aa_debdeb/items/128ccb6fa245b6f618b3)に書いてあるので割愛します。)

![depth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/f98686a7-772c-6414-1828-ffc5fe03ddf5.png)


スクリーンと同じ大きさの四角形メッシュを用意して、GBufferの情報をもとにポストエフェクト的にライティング処理を行います。以下はライティング処理を行うフラグメントシェーダーのコードです。GBufferの内容は`uniform sampler2D`でテクスチャとして渡しています。テクスチャから各ピクセルのライティングに必要な情報を取得できます。

```glsl
#version 300 es

precision highp float;

in vec2 v_uv;

out vec4 o_color;

uniform sampler2D u_positionTexture; // 位置バッファー
uniform sampler2D u_normalTexture; // 法線バッファー
uniform sampler2D u_colorTexture; // 色バッファー
uniform vec3 u_lightDir;
uniform vec3 u_lightColor;
uniform vec3 u_cameraPos;

void main(void) {
  vec4 color = texture(u_colorTexture, v_uv);
  vec3 albedo = color.xyz;
  if (albedo == vec3(0.0)) { // 色がない場合は背景を表示できるようにする（ここはもう少しやりようがある気がする）
    discard;
  }
  vec3 position = texture(u_positionTexture, v_uv).xyz;
  vec3 normal = texture(u_normalTexture, v_uv).xyz;
  float specIntensity = color.w;
  vec3 viewDir = normalize(u_cameraPos - position);

  vec3 lightDir = normalize(u_lightDir);
  vec3 reflectDir = reflect(-lightDir, normal);

  vec3 diffuse = albedo * u_lightColor * max(0.0, dot(lightDir, normal));
  vec3 specular =  albedo * pow(max(0.0, dot(viewDir, reflectDir)), specIntensity);
  o_color = vec4(diffuse + specular, 1.0);
}
```

ライティングした結果は以下のようになります。
![lighting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/7a93082a-7c9e-b800-4012-3fbbf7d3b431.png)

---

この記事ではWebGL2でDeferred Renderingを行う方法について解説しました。やっていることは意外と単純なので、実装するだけならそこまで難しくなさそうです。しかし、本格的に利用するとなるとバッファーの最適化やライト情報の渡し方、透明オブジェクトやアンチエイリアスの扱い方などいろいろ検討することが多く難しそうに感じました。