JavaScript
WebGL
GLSL

WebGLでGaussianブラー

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


参考