dFdxとdFdyでわずか4行のお手軽エッジ検出！

# dFdxとdFdyとは
GLSL1.1以降から[`dFdx/dFdy`](https://www.opengl.org/sdk/docs/man/html/dFdx.xhtml)という組み込み関数が使えるのですが、そこまで使う機会がないため、使い方を知っている方は少ないかもしれません。

`dFdx/dFdy`はスクリーンのX方向・Y方向の偏微分を計算するフラグメントシェーダの関数です。

偏微分というと、難しそうな印象を受けるかもしれませんが、単なるスクリーン上の画素値（本当は任意の値）の差分です。

この記事では、`dFdx/dFdy`の使い方をエッジ検出の実例を踏まえて解説します。

なお、WebGLで`dFdx/dFdy`を使用するためには、[`gl.getExtension("OES_standard_derivatives")`](https://developer.mozilla.org/ja/docs/Web/API/OES_standard_derivatives)と拡張を有効にする必要があります。

# dFdxとdFdyでお手軽エッジ検出！
`dFdx/dFdy`に関しては、難しいことを考えずに実例を見たほうが分かりやすいです。

今回は[Shadertoy](https://www.shadertoy.com/)で動作する動画のエッジ検出のシェーダを書きました。

完成版はコチラです。

- [Edge detection using dFdx/dFdy](https://www.shadertoy.com/view/Xtd3W7#)

![160729-0007.png](https://qiita-image-store.s3.amazonaws.com/0/17718/119af5dd-c1ae-e5b1-7c12-7d52c0888baf.png)

次の節ではこのエッジ検出をステップごとに順を追って解説していきます。

## 1. 動画テクスチャのフェッチ
動画テクスチャも普通のテクスチャを同じように`texture2D`でフェッチできます。

```glsl:動画テクスチャのフェッチ
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    fragColor =  texture2D(iChannel0, uv);
}
```
![160729-0002.png](https://qiita-image-store.s3.amazonaws.com/0/17718/f7deffc8-3a0c-03ee-b399-c68d454b494d.png)

## 2. グレースケースに変換
続いてグレースケールへ変換します。

`length`関数をつかって、3チャンネルの`color.rgb`を1チャンネルの`gray`に変換します。

```glsl:グレースケースに変換
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(gray), 1.0);
}
```

![160729-0003.png](https://qiita-image-store.s3.amazonaws.com/0/17718/d605bd0c-d912-1035-3678-38a05e8c14fc.png)

## 3. dFdyでY方向の偏微分をもとめる
ようやく本題のdFdx/dFdyです！

`dFdy(gray)`とすると、スクリーンのY方向に対して、grayを偏微分した値を求めることができます。

`float result = dFdy(gray)`とするだけで、スクリーンと同じサイズの別バッファに`gray`の値を格納して、同時に`result`に`gray`をY方向に偏微分した値が返ってくるイメージです。

```glsl:dFdyでY方向の偏微分をもとめる
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(dFdy(gray)) * 5.0, 1.0);
}
```

![160729-0004.png](https://qiita-image-store.s3.amazonaws.com/0/17718/346f8e12-0a15-58b1-b643-204191fa0451.png)


## 4. XとYの両方向の偏微分をもとめる
Y方向のみだとX方向のエッジが検出できないので、X方向の偏微分も求めて、それぞれの結果を合成します。

```glsl:X方向とY方向の両方の偏微分をもとめる
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(length(vec2(dFdx(gray), dFdy(gray)))), 1.0);
}
```

![160729-0005.png](https://qiita-image-store.s3.amazonaws.com/0/17718/72381f47-046e-b984-e869-07baf7d90843.png)

## 5. step関数で2値化
最後に[`step`](https://www.opengl.org/sdk/docs/man/html/step.xhtml)という組み込み関数で結果を2値化します。

`step(edge, x)`は2引数の関数で、`edge`が閾値、`x`が入力値です。要するに、`step(edge, x)`は`x < edge ? 0.0 : 1.0`を返す関数です。

シェーダで分岐をするとパフォーマンスが落ちるので、`step`などの組み込み関数は積極的に使っていきましょう！

```glsl:step関数で2値化
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(step(0.06, length(vec2(dFdx(gray), dFdy(gray))))), 1.0);
}
```

![160729-0007.png](https://qiita-image-store.s3.amazonaws.com/0/17718/119af5dd-c1ae-e5b1-7c12-7d52c0888baf.png)

わずか4行のコードでエッジ検出ができました！とてもお手軽ですね。

# 番外編: dFdx/dFdyのその他の活用事例
エッジ検出以外のdFdx/dFdyの利用方法を軽く触れます。

頂点座標をスクリーン上でXY方向に偏微分を計算して、それらの外積を求めると法線となります。次の2つはどちらもdFdxとdFdyで法線を計算した実例です。

- [wgld.org | WebGL: フラットシェーディング |](https://wgld.org/d/webgl/w087.html)
- [レイマーチングの法線をdFdxとdFdyで導出する - Qiita](http://qiita.com/gam0022/items/1878150981494fd66abe)

テクスチャーサンプリングの詳細レベルの計算（ミップマップのレベルの推定）や、異方性フィルタリングにおける異方性の軸に沿ったサンプリングのレベルの推定にも使用されます。ワールド座標をスクリーン上でdFdxとdFdyで偏微分することで、スクリーン上でのテクスチャのスケールを求めることができます。

アンチエイリアシングの実装において「変化の激しいピクセル」を抽出する際にも利用されます。エッジ検出と同じ要領でdFdxとdFdyを用いて変化の激しいピクセルを抽出した後、ピクセルを平均化するぼかし処理をすることでアンチエイリアシングが実現できます。

# 番外編: fwidthを使ったエッジ検出
上のエッジ検出のコードは[`fwidth`](https://www.opengl.org/sdk/docs/man/html/fwidth.xhtml)によって、もっと短くできます！

`fwidth(p)`は、`abs(dFdx(p)) + abs(dFdy(p))`を返す関数です。

```glsl:fwidthを使ったエッジ検出
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(step(0.07, fwidth(gray))), 1.0);
}
```

結果もほぼ同じですね。
![160729-0008.png](https://qiita-image-store.s3.amazonaws.com/0/17718/0311b1b8-ec65-89ed-4764-4a0a43af0af3.png)

# まとめ
偏微分というと、とっつきにくい印象があるかもしれませんが、要するにピクセル単位の差分です。`dFdx/dFdy`がどういうものか、なんとなく分かっていただけたでしょうか？

フラグメントシェーダでは隣のピクセルの値を参照できないので、通常は偏微分を計算するためには2パス必要となってしまうのですが、`dFdx/dFdy`を使うと1パスで偏微分を計算できます！これは地味にすごいと思います[^1]。

みなさんも`dFdx/dFdy`を使いこなしていきましょう！

[^1]: 個人的には`dFdx/dFdy`がどういう実装なのか気になるのですが、調べても良く分からなかったので、どなたか教えて欲しいです。結果をよく見ると2x2のブロックとなって、解像度が半分になっていますね。2x2のブロック単位でフラグメントシェーダがまとめて実行されて、`dFdx/dFdy`の箇所で同期待ちをしているのではないかと思っています。2x2のブロック内は同じ値が返ってくるので、ブロックノイズのようになるのでしょう。