LoginSignup
17
13

More than 3 years have passed since last update.

WebGLでGaussianブラー

Posted at

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

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

今回はブラーの効果がわかりやすいように以下のようなシンプルなシーンにブラーを掛けてみます。
no-blur.png

サンプルはGithubに置いておきました。
aadebdeb/Sample_WebGL_GaussianBlur: Sample of Gaussian Blur in WebGL

縮小バッファの作成

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

// 縮小バッファを作成する関数
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;
};

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

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

フラグメントシェーダー
#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);
}

ブラーの適用

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

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の「第10章 Image Effect応用」を参考にしました。重みはGaussian分布の式からJavaScript側で計算してuniformで渡してもいいと思います。

フラグメントシェーダー
#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
縮小バッファのサイズが小さいほど、またブラーを掛ける回数が多いほどぼかし具合が大きくなります。サンプルステップも大きくするとぼかし具合が多くなりますが、アーティファクトが結構つらい感じでした。

Floatテクスチャの利用

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

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しか使えないためシェーダー側でバイリニアフィルタを行う必要があります。具体的には縮小バッファーへのコピー及び縮小バッファーからの拡大に利用するシェーダーを以下のようにします。

フラグメントシェーダー
#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ブラーを実装してみました。ブラーはブルームや被写界深度といったポストエフェクトにも必要なので、効率的なきれいな方法を実装できるようになりたいですね。

参考

17
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
13