JavaScript Advent Calendar/WebGLコース10日目・画像フィルターを作る

JavaScript Advent Calendar 2011 WebGL駅伝10日目、独走3日目になります。今からでも参加したい方がいましたらぜひ教えてください。

今日はこのオルセー美術館をTiltShiftさせてみます。実はこれがやりたくて一昨日から書いてきたのでした。

サンプルです。

シェーダー部分以外は9割ぐらい昨日と同じです。

シェーダー

昨日は頂点シェーダーでテクスチャーの座標をvaryingに入れてましたが、今日は別の方法でやるので、頂点シェーダーはたったこれだけです。

  attribute vec2 aPosition;

  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
  }

フラグメントシェーダーでは、ガウスフィルター(Gaussian Blur)のための関数を定義しています。フィルターのパラメーターは適当にググッて拾ってきました。そこはまあ重要ではありません。

  precision mediump float;

  uniform sampler2D uSampler;
  uniform vec2 uTextureSize;

  vec4 GaussianBlur(in sampler2D tex, in vec2 c, in vec2 size) {
    // http://homepages.inf.ed.ac.uk/rbf/HIPR2/gsmooth.htm
    return (
      41.0 * texture2D(tex, vec2(c / size)) +
      26.0 * (
        texture2D(tex, (c + vec2( 1.0, 0.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 0.0)) / size) +
        texture2D(tex, (c + vec2( 0.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 0.0,-1.0)) / size)
      ) + 
      16.0 * (
        texture2D(tex, (c + vec2( 1.0, 1.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 1.0,-1.0)) / size) +
        texture2D(tex, (c + vec2(-1.0,-1.0)) / size)
      ) +
      7.0 * (
        texture2D(tex, (c + vec2( 2.0, 0.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 0.0)) / size) +
        texture2D(tex, (c + vec2( 0.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 0.0,-2.0)) / size)
      ) +
      4.0 * (
        texture2D(tex, (c + vec2( 1.0, 2.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 1.0,-2.0)) / size) +
        texture2D(tex, (c + vec2(-1.0,-2.0)) / size) +
        texture2D(tex, (c + vec2( 2.0, 1.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 2.0,-1.0)) / size) +
        texture2D(tex, (c + vec2(-2.0,-1.0)) / size)
      ) +
      1.0 * (
        texture2D(tex, (c + vec2( 2.0, 2.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 2.0,-2.0)) / size) +
        texture2D(tex, (c + vec2(-2.0,-2.0)) / size)
      )) / 273.0;
  }

  void main() {
    float y = gl_FragCoord.y / uTextureSize.y;
    if (y < 0.3 || y > 0.4) {
      gl_FragColor = GaussianBlur(uSampler, gl_FragCoord.xy, uTextureSize);
    } else {
      gl_FragColor = texture2D(uSampler, gl_FragCoord.xy / uTextureSize);
    }
  }

一番のポイントは、gl_FragCoordという組み込み変数を使っていることです。この変数は、ビューポートの左下からの座標がピクセル単位で入っています。つまり、ビューポートをテクスチャー画像のサイズと同じにした場合は、xは0から画像の幅まで、yは0から画像の高さまでの値をとります。

uTextureSizeにその画像のサイズを入れてあります。(この場合300x240)

  var uTextureSizeLocation = gl.getUniformLocation(program, "uTextureSize");

  …

  gl.uniform2f(uTextureSizeLocation, img.naturalWidth, img.naturalHeight);

このように一枚画像を全面に表示する場合は、gl_FragCoord.xy / uTextureSizeが昨日やった(0,0)から(1,1)のテクスチャー座標と同じものになります。(gl_FragCoord.xyってこれ以外の用途はあるんだろうか?)

まとめ

実は、WebGLによる画像フィルターばかりを集めたデモが公開されていて、TiltShiftもちゃっかりあります。


WebGLで画像処理はCanvas2DContextより簡単です。魚眼レンズエフェクトとかも簡単にできるはずです。

↓のように端をclamp(GLSLのtexture2D関数に0より小さい座標を与えた場合は0、1より大きな座標を与えた場合は1と考える)にしておけばエッジのことも考え無くていいですし。

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

しかし、FirefoxWebGLがバグってるため、今のところはやはりテクスチャーのサイズは2の累乗にしたほうがいいです。今日のサンプルもFirefoxChromeでは縮小のされ方が違います。