WebGLでGaussianブラー

WebGLでGaussianブラーを実装してみました。

縦方向と横方向に分けて2パスでブラーを掛けることでテクスチャのサンプリング数を減らしています。また、川瀬式MGF(Multiple Gaussian Filter)を参考にして、縮小バッファも使用してみます。

今回はブラーの効果がわかりやすいように以下のようなシンプルなシーンにブラーを掛けてみます。
![no-blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/74bdee88-1eb1-d8d3-e770-2aef05bb92aa.png)

サンプルはGithubに置いておきました。
[aadebdeb/Sample\_WebGL\_GaussianBlur: Sample of Gaussian Blur in WebGL](https://github.com/aadebdeb/Sample_WebGL_GaussianBlur)

## 縮小バッファの作成

Gaussianブラーの対象にする縮小バッファを作成します。縮小バッファへの書き込み及び読み出し時にバイリニアフィルターを利用できるように`gl.TEXTURE_MAG_FILTER`と`gl.TEXTURE_MIN_FILTER`を`gl.LINEAR`にしておくことがポイントです。

```js
// 縮小バッファを作成する関数
const createFramebuffer = function(gl, width, height) {
  const framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.bindTexture(gl.TEXTURE_2D, null);
  return {
    framebuffer: framebuffer,
    texture: texture,
    width: width,
    height: height
  };
};

// 1/4サイズの縮小バッファを作成する（サンプルでは縮小率をGUIで指定できるようにしています）
const blurWidth = Math.ceil(canvas.width / 4);
const blurHeight = Math.ceil(canvas.height / 4);
// 複数回ブラーを掛けるために、2つの縮小バッファを作成しておく
let blurFbObjR = createFramebuffer(gl, blurWidth, blurHeight);
let blurFbObjW = createFramebuffer(gl, blurWidth, blurHeight);
const swapBlurFbObj = function() {
  const tmp = blurFbObjR;
  blurFbObjR = blurFbObjW;
  blurFbObjW = tmp;
};
```

## 縮小バッファーへのコピー

作成した縮小バッファーに以下のシェーダーでブラー対象をコピーします。

```glsl:フラグメントシェーダー
#version 300 es

precision highp float;

in vec2 v_uv;

out vec4 o_color;

uniform sampler2D u_texture;

void main(void) {
  o_color = texture(u_texture, v_uv);
}
```

## ブラーの適用

縮小バッファに対してブラーを適用します。縦方向・横方向にそれぞれブラーを掛ける操作を複数回実行することで深くブラーが掛かるようにします。

```js
const applyBlur = function() {
  gl.viewport(0.0, 0.0, blurFbObjW.width, blurFbObjW.height);
  gl.useProgram(blurProgram);

  const blurNum = parameters['blur num'];
  gl.uniform1i(blurUniforms['u_sampleStep'], parameters['sample step']);
  for (let i = 0; i < blurNum; i++) {
    // 横方向のブラー
    gl.bindFramebuffer(gl.FRAMEBUFFER, blurFbObjW.framebuffer);
    setUniformTexture(gl, 0, blurFbObjR.texture, blurUniforms['u_texture']);
    gl.uniform1f(blurUniforms['u_horizontal'], true);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    swapBlurFbObj();

    // 縦方向のブラー
    gl.bindFramebuffer(gl.FRAMEBUFFER, blurFbObjW.framebuffer);
    setUniformTexture(gl, 0, blurFbObjR.texture, blurUniforms['u_texture']);
    gl.uniform1f(blurUniforms['u_horizontal'], false);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    swapBlurFbObj();
  }
  
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
};
```

使用するフラグメントシェーダーは以下のようになります。サンプリング時に適当な間隔をあけることでブラーが大きくかかるようにしています。Gaussianブラーの重みは、[Unity Graphics Programming vol\.2](https://indievisuallab.stores.jp/items/5ae077b850bbc30f3a000a6d)の「第10章 Image Effect応用」を参考にしました。重みはGaussian分布の式からJavaScript側で計算してuniformで渡してもいいと思います。

```glsl:フラグメントシェーダー
#version 300 es

precision highp float;

out vec4 o_color;

uniform sampler2D u_texture; // ブラーを掛ける対象
uniform bool u_horizontal; // ブラーを掛ける方向
uniform int u_sampleStep; // サンプルステップ

// Gaussianブラーの重み
const float[5] weights = float[](0.2270270, 0.1945945, 0.1216216, 0.0540540, 0.0162162);

// texelFetchはWrapの設定が効かないので、シェーダーでgl.CLAMP_TO_EDGEと同じになるようにする
ivec2 clampCoord(ivec2 coord, ivec2 size) {
  return max(min(coord, size - 1), 0);
}

void main(void) {
  ivec2 coord = ivec2(gl_FragCoord.xy);
  ivec2 size = textureSize(u_texture, 0);
  vec3 sum = weights[0] * texelFetch(u_texture, coord, 0).rgb;
  for (int i = 1; i < 5; i++) {
    ivec2 offset = (u_horizontal ? ivec2(i, 0) : ivec2(0, i)) * u_sampleStep;
    sum += weights[i] * texelFetch(u_texture, clampCoord(coord + offset, size), 0).rgb;
    sum += weights[i] * texelFetch(u_texture, clampCoord(coord - offset, size), 0).rgb;
  }
  o_color = vec4(sum, 1.0);
}
```

## 縮小バッファーからの拡大

縮小バッファから元のサイズに戻すときにも、縮小バッファーにコピーしたときと同じシェーダーを使います。
縮小バッファを作成したときにテクスチャに`gl.LINEAR`を設定しているので、拡大時にバイリニアフィルタがかかり滑らかなブラーがかかります。

## 結果

結果です。
![blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/98ba4afc-8d3c-f845-407d-b36580f1eb01.png)
縮小バッファのサイズが小さいほど、またブラーを掛ける回数が多いほどぼかし具合が大きくなります。サンプルステップも大きくするとぼかし具合が多くなりますが、アーティファクトが結構つらい感じでした。

## Floatテクスチャの利用

WebGLでHDRをやるために、フレームバッファにFloatテクスチャを利用してみます。Floatテクスチャを利用するためにフレームバッファ作成時に以下のようにします(`'EXT_color_buffer_float`拡張を有効化する必要があります)。

```js
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
```

基本的には先ほど紹介した方法と同じようにブラーを掛けるのですが、Floatテクスチャには`gl.NEAREST`しか使えないためシェーダー側でバイリニアフィルタを行う必要があります。具体的には縮小バッファーへのコピー及び縮小バッファーからの拡大に利用するシェーダーを以下のようにします。

```glsl:フラグメントシェーダー
#version 300 es

precision highp float;

in vec2 v_uv;

out vec4 o_color;

uniform sampler2D u_texture;

vec4 textureBilinear(sampler2D tex, vec2 uv) {
  vec2 textureSize = vec2(textureSize(tex, 0));
  vec2 invTextureSize = 1.0 / textureSize;

  vec2 texel = uv * textureSize;
  vec2 i = floor(texel);
  vec2 f = fract(texel);

  if (f.x > 0.5) {
    i.x += 1.0;
    f.x -= 0.5;
  } else {
    f.x += 0.5;
  }
  if (f.y > 0.5) {
    i.y += 1.0;
    f.y -= 0.5;
  } else {
    f.y += 0.5;
  }

  vec4 v00 = texture(tex, (i + vec2(-0.5, -0.5)) * invTextureSize);
  vec4 v10 = texture(tex, (i + vec2(0.5, -0.5)) * invTextureSize);
  vec4 v01 = texture(tex, (i + vec2(-0.5, 0.5)) * invTextureSize);
  vec4 v11 = texture(tex, (i + vec2(0.5, 0.5)) * invTextureSize);
  return mix(mix(v00, v10, f.x), mix(v01, v11, f.x), f.y);
```

何をしているかわかりづらいですが、バイリニアフィルタに使用する4つの点がテクセルの中心になるようにしています。例えばソース中の`texel`の値が`vec2(0.8, 1.3)`の場合、サンプルする4つの点は`vec2(0.5, 0.5)`, `vec2(1.5, 0.5)`, `vec2(0.5, 1.5)`, `vec2(1.5, 1.5)`にそれぞれ`invTextureSize`を掛けた値になり、補間に使用する`f`の値は`vec2(0.3, 0.8)`になります。

## 終わりに

WebGLでGaussianブラーを実装してみました。ブラーはブルームや被写界深度といったポストエフェクトにも必要なので、効率的なきれいな方法を実装できるようになりたいですね。

## 参考

* [☆PROJECT ASURA☆ \[Direct3D 11\] 『川瀬式グレアエフェクトを勉強する\(1\)』](http://project-asura.com/program/d3d11/d3d11_010.html)