床井研究室

暑い

暑いです。めっちゃ暑いです。意味わからんくらい暑いです。しかしデマンド超過防止のために部屋の空調切ってたので、たまらずに冷却材を投入しました。売店のガリガリ君専用冷凍庫、半分が梨味で埋まってたように見えたんですけど、問題ないんでしょうか。そんなこと気にしてたら体温が上がりますね。梨味おいしいです。一本食べたら飽きるけど。

ガリガリ君梨味

フィルタを書いてみる

昨日作ったサンプルプログラム

のフラグメントシェーダはこんな具合になってました。

#version 330

uniform sampler2DRect image;

layout (location = 0) out vec4 fc;

void main(void)
{
  fc = texture(image, gl_FragCoord.xy);
}

これで、こういう絵が出たとします。

もとのそらまめくん

これはテクスチャからサンプリングした色を、そのままカラーバッファの画素値として出力しています。そこで、サンプリングした色から、その右隣の画素の色を引いてみます。引き算の値が負だと黒と区別がつかないので、GLSL の組み込み関数 abs() で絶対値を取ります。

  fc = abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(1, 0)));

これだけで横方向に微分した画像が得られます。GLSL の組み込み関数 textureOffset() は、テクスチャ座標から画素単位にオフセットした位置の色をサンプリングします。

そらまめくんの縦のエッジ

これに、上の画素との差を加えてみます。

  fc = abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(1, 0)))
     + abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(0, 1)));

一応、エッジが検出できます。

そらまめくんの縦横のエッジ

平滑フィルタ

ということで、いろいろフィルタを書いてみます。たとえば平滑フィルタなら、textureOffset() を並べて足すだけです。

#version 330

// 3x3 移動平均フィルタ

uniform sampler2DRect image;

layout (location = 0) out vec4 fc;

// 平均を求める
void main(void)
{
  fc = texture(image, gl_FragCoord.xy);

  fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, -1));
  fc += textureOffset(image, gl_FragCoord.xy, ivec2( 0, -1));
  fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, -1));

  fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, 0));
  fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, 0));

  fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, 1));
  fc += textureOffset(image, gl_FragCoord.xy, ivec2( 0, 1));
  fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, 1));

  fc *= 0.11111111;
}

でも、なんかすっきりしません。こういうのは、やっぱり、

  fc = vec4(0.0);

  for (int i = -1; i <= 1; ++i)
    for (int j = -1; j <= 1; ++j)
      fc += textureOffset(image, gl_FragCoord.xy, ivec2(i, j));

って書きたいところです。でも、textureOffset()マニュアルには「offset は定数でないといけない」って書いてあって、実際コンパイル時にエラーになります。であればと、

  const ivec2 offset[] = ivec2[] (
    ivec2(-1, -1),
    ivec2( 0, -1),
    ivec2( 1, -1),
    ivec2(-1,  0),
    ivec2( 1,  0),
    ivec2(-1,  1),
    ivec2( 0,  1),
    ivec2( 1,  1)
  );

  fc = texture(image, gl_FragCoord.xy);

  for (int i = 0; i < offset.length(); ++i)
    fc += textureOffset(image, gl_FragCoord.xy, offset[i]);

ということをやってみました。しかし、ここでまた頭を抱えてしまいます。今日 (2014 年 7 月 26 日) の時点で Windows 7 の Quadro K4000 (ドライバのバージョン 335.23) だとこれで一応動いた気がします。でも、Windows 8.1 の GeForce 840M (337.88) だとなんだかサンプル点がずれてるように見えて、まともに動いているようには思えません。また OS X Mavericks の MacBook Air だと、やっぱり「offset は定数でないといけない」っていうエラーになります。

たぶん、NVIDIA の GLSL コンパイラの最適化能力が非常に高くて1、これくらいのループはアンローリングされてインラインに展開されてしまうのだと思います。だから「定数の配列」でも OK なのかもしれません。でも、仕様上それが正しいのかどうかわからなかったので、結局上のように「手でアンローリング」することにしました。もっとエレガントな書き方ないですか。

いろいろ書いてみた

今まで作ったフィルタをサンプルプログラム (1)に追加しました。このフラグメントシェーダを以下のもので置き換えれば、たぶん動くと思います。

ちなみに、中間値フィルタは RGBA それぞれ独立に中間値を求めているので、個々の画素のチャンネルはシャッフルされています。あと、ソーティングで使われるような繰り返し回数が定まらないループをシェーダで使うと「ありえんくらい遅くなる」ので、これも力技でインラインで書いています2。これ元ネタあるんですけど、ソースがわかりません。また探します。

  1. 使ってない uniform をコンパイル時に消されて、変数定義してるのに glGetUniformLocation() に -1 を返されてうろたえたってことがありました。 

  2. GLSL の関数はインライン展開されるのでしょうか。