【WebGL2】gl_FragDepthを使ってフラグメントシェーダーで深度を書き込む

WebGL2では`gl_FragDepth`を使うとフラグメントシェーダーで深度を書き込めるらしいので試してみました。

サンプルはGitHubに置いておきました。
[aadebdeb/Sample\_WebGL2\_FragDepth: Sample of Writing Depth in Fragment Shader by gl\_FragDepth](https://github.com/aadebdeb/Sample_WebGL2_FragDepth)

まずは`gl_Position`で設定される深度について確認しておきます。頂点シェーダーでは`gl_Position`にクリップ空間の座標を設定しています。その後WebGL側で、`gl_Position.xyz`を`gl_Position.w`を割ることで正規化デバイス座標に変換しています。この座標の値が$-1.0 \leq z \leq 1.0$にあるとレンダリング対象になります。$-1$が最も手前、$1$が最も奥にあるということになります。さらに、正規化デバイス座標はビューポート変換によりビューポート座標に変換されます。$z$の値は$0.5z + 0.5$という変換が行われるので、レンダリング対象の値の範囲は$0.0 \leq z \leq 1.0$になります。この値が深度として扱われます。

例えば以下のような頂点シェーダーでは、クリップ空間では$z = 0.0, w = 1.0$なため、正規化デバイス座標のz値は$z / w = 0.0 / 1.0 = 0.0$、ビューポート座標のz値つまり深度値は$0.5z + 0.5 = 0.5 \times 0.0 + 0.5 = 0.5$になります。

```glsl
#version 300 es

layout (location = 0) in vec2 position;

out vec2 v_position;

void main(void) {
  v_position = position;
  gl_Position = vec4(position, 0.0, 1.0);
}
```

`gl_FragDepth`には、ビューポート座標のz値を書き込みます。

---

それでは行列変換を用いない単純な2Dで試してみます。サンプルは[こちら](https://aadebdeb.github.io/Sample_WebGL2_FragDepth/2d.html)になります。今回は先ほどの頂点シェーダーで2つの同じサイズの四角形を同じ位置に重ねるように描画してます。1つの四角形は`gl_FragDepth`で深度を設定する以下のフラグメントシェーダーを使い、もう1つの四角形はフラグメントシェーダーでは深度を設定しません。`fbm`関数について補足すると、フラクタルブラウンモーションという$[0, 1]$の範囲のノイズを生成する関数です。

```glsl
#version 300 es

precision highp float;

in vec2 v_position;

out vec4 o_color;

uniform vec3 u_color;
uniform float u_time;

float random(vec3 x){
  return fract(sin(dot(x,vec3(12.9898, 78.233, 39.425))) * 43758.5453);
}

float valuenoise(vec3 x) {
  vec3 i = floor(x);
  vec3 f = fract(x);

  vec3 u = f * f * (3.0 - 2.0 * f);

  return
  mix(
    mix(
      mix(random(i), random(i + vec3(1.0, 0.0, 0.0)), u.x),
      mix(random(i + vec3(0.0, 1.0, 0.0)), random(i + vec3(1.0, 1.0, 0.0)), u.x),
      u.y
    ),
    mix(
      mix(random(i + vec3(0.0, 0.0, 1.0)), random(i + vec3(1.0, 0.0, 1.0)), u.x),
      mix(random(i + vec3(0.0, 1.0, 1.0)), random(i + vec3(1.0, 1.0, 1.0)), u.x),
      u.y
    ), 
    u.z
  );
}

float fbm(vec3 x) {
  float sum = 0.0;
  float amp = 0.5;
  for (int i = 0; i < 5; i++) {
    sum += amp * valuenoise(x);
    amp *= 0.5;
    x *= 2.01;
  }
  return sum;
}

void main(void) {
  o_color = vec4(u_color, 1.0);
  gl_FragDepth = fbm(vec3(v_position + vec2(100.0), u_time * 0.3));
}
```

結果は以下のようになります。青色の四角形は`gl_FragDepth`で深度値を設定し、赤色の四角形は`gl_FragDepth`で値を設定していません（つまり深度値は`gl_Position`から求める値になります）。先に述べたように赤色の四角形の深度値は$0.5$になるので、`fbm`関数の値が$0.5$より小さいところでは赤色で、$0.5$より大きいところは青色で表示されています。
![2d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/8209fed8-bc83-3b12-a495-50c2ed9e4ec8.png)

---

次に3Dの場合です。サンプルは[こちら](https://aadebdeb.github.io/Sample_WebGL2_FragDepth/3d.html)になります。3Dでも2枚の同じ大きさの四角形を同じ位置に重ねています。頂点シェーダーでは以下のように行列で座標変換しています。

```glsl
#version 300 es

layout (location = 0) in vec3 position;

out vec3 v_position;

uniform mat4 u_mvpMatrix;

void main(void) {
  v_position = position;
  gl_Position = u_mvpMatrix * vec4(position, 1.0);
}
```

`gl_FragDepth`で深度値を書き込むフラグメントシェーダーは以下のようになります。`fbm`関数でz値をずらした後に行列でクリップ空間に変換しています。`gl_FragDepth`に書き込むのはビューポート座標のz値なので、wの値で割ってから$[0, 1]$の範囲に変換しています。

```glsl
#version 300 es

precision highp float;

in vec3 v_position;

out vec4 o_color;

uniform mat4 u_mvpMatrix;
uniform vec3 u_color;
uniform float u_time;

// fbm関数については省略

void main(void) {
  o_color = vec4(u_color, 1.0);
  float offset = fbm(vec3(v_position.xy + vec2(100.0), u_time * 0.3)) * 2.0 - 1.0;
  vec4 position = u_mvpMatrix * vec4(v_position + vec3(0.0, 0.0, offset), 1.0);
  gl_FragDepth = (position.z / position.w) * 0.5 + 0.5;
}
```

結果は以下のようにz値の値に応じて赤と青がまだらに表示されます。
![3d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/4b112777-8814-a3ce-b1cd-3abfeb7d97e3.png)


---

`gl_FragDeph`を使ってフラグメントシェーダーで深度を書き込む方法を紹介しました。具体的に何に使えるかというと難しいですが、WebGLで複雑なことをする場合に必要になるかもしれません。個人的には、ポリゴンベースの3Dオブジェクトとオブジェクトスペースレイマーチングで描画した3Dオブジェクトを組み合わせるときや、Deferred Renderingで深度値を操作するときに使用できるかもしれないと考えています。