WebGLでGaussianブラーを実装してみました。
縦方向と横方向に分けて2パスでブラーを掛けることでテクスチャのサンプリング数を減らしています。また、川瀬式MGF(Multiple Gaussian Filter)を参考にして、縮小バッファも使用してみます。
今回はブラーの効果がわかりやすいように以下のようなシンプルなシーンにブラーを掛けてみます。
サンプルはGithubに置いておきました。
aadebdeb/Sample_WebGL_GaussianBlur: Sample of Gaussian Blur in WebGL
縮小バッファの作成
Gaussianブラーの対象にする縮小バッファを作成します。縮小バッファへの書き込み及び読み出し時にバイリニアフィルターを利用できるようにgl.TEXTURE_MAG_FILTER
とgl.TEXTURE_MIN_FILTER
をgl.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
を設定しているので、拡大時にバイリニアフィルタがかかり滑らかなブラーがかかります。
結果
結果です。
縮小バッファのサイズが小さいほど、またブラーを掛ける回数が多いほどぼかし具合が大きくなります。サンプルステップも大きくするとぼかし具合が多くなりますが、アーティファクトが結構つらい感じでした。
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ブラーを実装してみました。ブラーはブルームや被写界深度といったポストエフェクトにも必要なので、効率的なきれいな方法を実装できるようになりたいですね。