WebGL2で深度値をテクスチャに書き出す&読み出す

WebGLで[シャドウマッピング](https://wgld.org/d/webgl/w051.html)や[被写界深度フィルター](https://wgld.org/d/webgl/w059.html)を実装する場合に、深度値をテクスチャに書き出したものが必要になるようです。

WebGL2からはフレームバッファに使う深度バッファをカラーバッファと同じようにテクスチャを使用できるようになり、これによりカラーバッファと深度バッファを同時に取得できるようになったようなので試してみました。
（正確にはWebGL1でも拡張機能`WEBGL_depth_texture`を有効にすることで深度バッファにテクスチャを利用できるようですが、以下で説明する方法と同じようにできるかはわかりません。）

---

サンプルでは以下のように左が手前、右が後ろになるように２つの三角形を描画しています。

![Sample01_ColorTexture.png](https://qiita-image-store.s3.amazonaws.com/0/61569/5aa4ece5-3e49-cd7c-c641-759d88fb7988.png)


このシーンの深度値を表示すると以下のようになります。
深度値の範囲は[0, 1]で手前が0(黒)、奥が1(白)になります


![Sample01_DepthTexture.png](https://qiita-image-store.s3.amazonaws.com/0/61569/c1b5eca6-aae4-16d0-40d5-2140ddca301c.png)

サンプルコード全体は[こちら](https://github.com/aadebdeb/Sample_WebGL2_Depth_Texture/blob/master/Sample_Depth_Texture.html
)から、実際に動作しているものは[こちら](https://aadebdeb.github.io/Sample_WebGL2_Depth_Texture/Sample_Depth_Texture.html)から確認できます。

処理の内容は、次のようなよくあるポストエフェクトの流れです。

1. 深度バッファをテクスチャにしたフレームバッファを作成する
2. 1で作成したフレームバッファに三角形をオフスクリーンレンダリングする
3. canvas全体を覆うポリゴンに深度テクスチャをそのままレンダリングする

ソースコード全体は長くWebGLの定型的な処理も多いので、深度値をテクスチャに書き込む&読み込むことに関係があるところだけを抜粋して解説していきます。

まず、フレームバッファを作成している箇所です。
`gl.DEPTH_ATTACHMENT`で設定している深度バッファをカラーバッファと同じようにテクスチャにしています。

```js
// creates framebuffer
const frameBuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
const colorTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);
const depthTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT16, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
```

`depthTexture`を作成する際の`gl.texImage2D`の第7引数は`gl.DEPTH_COMPONENT`に、第３引数と第8引数のフォーマットの設定には適切な組み合わせを用いる必要があります。([この記事](https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html)が参考になります。)
例えば、以下のようにしても深度バッファとして使用することができます。このあたりは必要な精度に応じて組み合わせを選択するのだと思います。

```js
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, width, height, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
```

`gl.texParameteri`でフィルター設定に`gl.NEAREST`を使用していますが、これはこうしないとテクスチャを参照する際にエラーがでるためです。おそらく後述するようにシェーダーで`texelFetch`関数を用いてテクスチャをサンプリングしているためだと思います。

次に作成したフレームバッファに２つの三角形をオフスクリーンレンダリングします。
レンダリング前に深度値を1.0で初期化しています。

```js
// renders triangles
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(triangleProgram);
gl.bindVertexArray(triangleVertexArray);
gl.uniform3fv(triangleUniformLocations['u_offset'], [-0.3, 0.0, -0.5]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.uniform3fv(triangleUniformLocations['u_offset'], [0.3, 0.0, 0.5]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
```

オフスクリーンレンダリングした結果のデプステクスチャをuniformでシェーダーに渡して画面全体の覆うポリゴンにレンダリングします。

```js
// renders a depth or color texture
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(quadProgram);
gl.bindVertexArray(quadVertexArray);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.uniform1i(quadUniformLocations['u_colorTexture'], 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.uniform1i(quadUniformLocations['u_depthTexture'], 1);
gl.uniform1i(quadUniformLocations['u_showDepth'], showDepth.checked);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
```

フラグメントシェーダーは以下のようになっています。
GLSL ES 3.0から使える`texelFetch`で対応した座標をテクスチャからサンプルして、そのまま出力しています。
`u_showDepth`でカラーテクスチャを使うか、デプステクスチャを使うかを変更できるようにしています。

```glsl
#version 300 es
precision highp float;

out vec4 out_color;
uniform sampler2D u_colorTexture;
uniform sampler2D u_depthTexture;
uniform bool u_showDepth;

void main(void) {
  if (u_showDepth) {
    float d = texelFetch(u_depthTexture, ivec2(gl_FragCoord.xy), 0).r;
    out_color = vec4(vec3(d, d, d), 1.0);
  } else {
    vec3 c = texelFetch(u_colorTexture, ivec2(gl_FragCoord.xy), 0).rgb;
    out_color = vec4(c, 1.0);
  }
}
```


---

次に以下のように透視プロジェクション行列を使用している場合を考えたいと思います。
このシーンでは左から右に段々と奥になるように三角形を配置しています。
ソースコードは[こちら](https://github.com/aadebdeb/Sample_WebGL2_Depth_Texture/blob/master/Sample_Depth_Texture_Perspective.html)から、実際に動作しているものは[こちら](https://aadebdeb.github.io/Sample_WebGL2_Depth_Texture/Sample_Depth_Texture_Perspective.html)から確認できます。

![Sample02_ColorTexture.png](https://qiita-image-store.s3.amazonaws.com/0/61569/083fd606-df6e-3b09-9a59-1c52ad7abfcb.png)

このシーンの深度値をさきほどと同じ方法でそのまま表示すると、次のようなほとんど真っ白な画面が表示されます。

![Sample02_DepthTexture.png](https://qiita-image-store.s3.amazonaws.com/0/61569/aa8bf115-0a1a-b8f3-fefc-934e4570f648.png)

これは透視プロジェクション行列を用いた変換では深度値を非線形に保存しているからです。
（これは手前側ほど精度を高く深度値を保存するためです。）
このままではいろいろと使用しづらいと思うので、[この記事](http://blog.bongiovi.tw/webgl-depth-texture/)を参考に線形に変換してみます。

具体的には以下のような変換をフラグメントシェーダーで行います。
`u_near`と`u_far`にはそれぞれカメラのニアークリップとファークリップの値となっています。


```glsl
#version 300 es
precision highp float;

out vec4 out_color;
uniform sampler2D u_colorTexture;
uniform sampler2D u_depthTexture;
uniform bool u_showDepth;
uniform bool u_linearDepth;
uniform float u_near;
uniform float u_far;

void main(void) {
  if (u_showDepth) {
    float d = texelFetch(u_depthTexture, ivec2(gl_FragCoord.xy), 0).r;
    if (u_linearDepth) {
      d = (2.0 * u_near) / (u_far + u_near - d * (u_far - u_near));
    }
    out_color = vec4(vec3(d, d, d), 1.0);
  } else {
    vec3 c = texelFetch(u_colorTexture, ivec2(gl_FragCoord.xy), 0).rgb;
    out_color = vec4(c, 1.0);
  }
}
```

線形変換すると、深度値は以下のように表示されます。

![Sample02_DepthTexture_Linear.png](https://qiita-image-store.s3.amazonaws.com/0/61569/99dc4c4e-4392-f348-e136-339f348148a1.png)