dFdxとdFdyとは
GLSL1.1以降からdFdx/dFdy
という組み込み関数が使えるのですが、そこまで使う機会がないため、使い方を知っている方は少ないかもしれません。
dFdx/dFdy
はスクリーンのX方向・Y方向の偏微分を計算するフラグメントシェーダの関数です。
偏微分というと、難しそうな印象を受けるかもしれませんが、単なるスクリーン上の画素値(本当は任意の値)の差分です。
この記事では、dFdx/dFdy
の使い方をエッジ検出の実例を踏まえて解説します。
なお、WebGLでdFdx/dFdy
を使用するためには、gl.getExtension("OES_standard_derivatives")
と拡張を有効にする必要があります。
dFdxとdFdyでお手軽エッジ検出!
dFdx/dFdy
に関しては、難しいことを考えずに実例を見たほうが分かりやすいです。
今回はShadertoyで動作する動画のエッジ検出のシェーダを書きました。
完成版はコチラです。
次の節ではこのエッジ検出をステップごとに順を追って解説していきます。
1. 動画テクスチャのフェッチ
動画テクスチャも普通のテクスチャを同じようにtexture2D
でフェッチできます。
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
fragColor = texture2D(iChannel0, uv);
}
2. グレースケースに変換
続いてグレースケールへ変換します。
length
関数をつかって、3チャンネルのcolor.rgb
を1チャンネルのgray
に変換します。
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);
}
3. dFdyでY方向の偏微分をもとめる
ようやく本題のdFdx/dFdyです!
dFdy(gray)
とすると、スクリーンのY方向に対して、grayを偏微分した値を求めることができます。
float result = dFdy(gray)
とするだけで、スクリーンと同じサイズの別バッファにgray
の値を格納して、同時にresult
にgray
を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);
}
4. XとYの両方向の偏微分をもとめる
Y方向のみだとX方向のエッジが検出できないので、X方向の偏微分も求めて、それぞれの結果を合成します。
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);
}
5. step関数で2値化
最後にstep
という組み込み関数で結果を2値化します。
step(edge, x)
は2引数の関数で、edge
が閾値、x
が入力値です。要するに、step(edge, x)
はx < edge ? 0.0 : 1.0
を返す関数です。
シェーダで分岐をするとパフォーマンスが落ちるので、step
などの組み込み関数は積極的に使っていきましょう!
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);
}
わずか4行のコードでエッジ検出ができました!とてもお手軽ですね。
番外編: dFdx/dFdyのその他の活用事例
エッジ検出以外のdFdx/dFdyの利用方法を軽く触れます。
頂点座標をスクリーン上でXY方向に偏微分を計算して、それらの外積を求めると法線となります。次の2つはどちらもdFdxとdFdyで法線を計算した実例です。
テクスチャーサンプリングの詳細レベルの計算(ミップマップのレベルの推定)や、異方性フィルタリングにおける異方性の軸に沿ったサンプリングのレベルの推定にも使用されます。ワールド座標をスクリーン上でdFdxとdFdyで偏微分することで、スクリーン上でのテクスチャのスケールを求めることができます。
アンチエイリアシングの実装において「変化の激しいピクセル」を抽出する際にも利用されます。エッジ検出と同じ要領でdFdxとdFdyを用いて変化の激しいピクセルを抽出した後、ピクセルを平均化するぼかし処理をすることでアンチエイリアシングが実現できます。
番外編: fwidthを使ったエッジ検出
上のエッジ検出のコードはfwidth
によって、もっと短くできます!
fwidth(p)
は、abs(dFdx(p)) + abs(dFdy(p))
を返す関数です。
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);
}
まとめ
偏微分というと、とっつきにくい印象があるかもしれませんが、要するにピクセル単位の差分です。dFdx/dFdy
がどういうものか、なんとなく分かっていただけたでしょうか?
フラグメントシェーダでは隣のピクセルの値を参照できないので、通常は偏微分を計算するためには2パス必要となってしまうのですが、dFdx/dFdy
を使うと1パスで偏微分を計算できます!これは地味にすごいと思います1。
みなさんもdFdx/dFdy
を使いこなしていきましょう!
-
個人的には
dFdx/dFdy
がどういう実装なのか気になるのですが、調べても良く分からなかったので、どなたか教えて欲しいです。結果をよく見ると2x2のブロックとなって、解像度が半分になっていますね。2x2のブロック単位でフラグメントシェーダがまとめて実行されて、dFdx/dFdy
の箇所で同期待ちをしているのではないかと思っています。2x2のブロック内は同じ値が返ってくるので、ブロックノイズのようになるのでしょう。 ↩