Unityなどでよく見られるBloomエフェクトをopenFrameworksでも使ってみたかったのですが、あまりリソースが出てこなかったので、GLSLを使って簡単なものですが実装してみました。まだ荒い感じですが、エフェクトをかける参考になればと思います。

実行環境
- macOS high Sierra 10.13.4
- openFrameworks 0.10.0
addon
- ofxGui(エフェクトのかかり具合を決めるだけなので、無くてもいい)
概要
Bloomエフェクトでは、元の描画結果にガウシアンぼかしをかけ、ぼかしがかかった描画結果と元の描画結果を加算合成することによって、物体から光が溢れているような表現を作ることができます。今回はこれをFBO(Frame Buffer Objects)とシェーダ(GLSL)を用いて実装していきます。
ガウシアンぼかし
画像処理において、ガウシアンぼかし (ガウスぼかし、ガウシアンブラー、ガウシアンフィルター、ガウスフィルター、Gaussian Blur)とは、ガウス関数をもちいて画像をぼかす処理である。デジタルカメラの撮像画像などからノイズを除去したり、アンシャープマスク処理、エッジ抽出の前処理などに応用できる。
実装
1. 描画結果をFBOに保存
まずは、描画内容をFBOに保存します。FBOに保存すると1フレームごとの描画結果を画像のように扱うことができ、あとでシェーダ側に描画結果をテクスチャとして送ることができます。ここは今回の肝ではないので、描画結果を画像として保存していることがわかれば大丈夫です。
void ofApp::update(){
// fbo.begin()とfbo.end()で挟むことによって描画結果をfboに保存し
// 画像のように使うことができる
fbo.begin();
ofClear(0, 0, 0, 0); // おまじないのようなもの(やらないとダメ)
cam.begin();
ofRotateZDeg(30.0);
ofSetColor(200, 200, 200, 255);
ofDrawBox(100, 0, 300, 100);
ofSetColor(0, 0, 200, 255);
ofDrawSphere(-100, 0, 300, 100);
cam.end();
fbo.end();
}
2. 元の描画結果にガウシアンぼかしをかける
ガウシアンぼかしをかけるのは画像のピクセルごとの処理になり、CPUで処理をしていると非常に重くなってしまうので、シェーダ(GPU側)で処理をします。
まずは、C++側の処理からです。
// X方向のガウシアンぼかし
onePassFbo.begin();
blur.begin();
// シェーダ側にuniform変数で値を受け渡す。
blur.setUniformTexture("tex", fbo.getTexture(), 0); // 描画結果
blur.setUniform1i("horizontal", true); // X方向にぼかしをかけるかどうか
blur.setUniform1f("strength", strength); // ぼかしの強度
fbo.draw(0, 0);
blur.end();
onePassFbo.end();
// Y方向のガウシアンぼかし
twoPassFbo.begin();
blur.begin();
// 上に同じ
blur.setUniformTexture("tex", onePassFbo.getTexture(), 0);
blur.setUniform1i("horizontal", false);
blur.setUniform1f("strength", strength);
onePassFbo.draw(0, 0);
blur.end();
twoPassFbo.end();
onePassFbo
とtwoPassFbo
は、ぼかしをかけた結果を保存するFBOです。ガウシアンぼかしでは、X方向にぼかした画像を、さらにY方向にぼかすという処理を行うため、2段階のぼかしをかけることになります。よってFBOもそれ用に2つ用意しています。
次にシェーダ側の処理です。シェーダ側では実際にガウシアンぼかしをかける処理を行います。計算はフラグメントシェーダで行い、頂点シェーダは頂点情報などを受け流すだけなので、頂点シェーダ部分については割愛します。githubのリンクを貼っておくので、そちらを参照してください。
# version 150
precision mediump float;
uniform sampler2DRect tex;
uniform bool horizontal;
uniform float strength;
float weight[5] = float[](0.382928, 0.241732, 0.060598, 0.005977, 0.000229);
in vec2 vTexCoord;
out vec4 vFragColor;
void main() {
vec3 color = vec3(0.0);
// texture(tex, vTexCoord)に入ってきたピクセルの値が格納されるので、
// それをstrengthによってずらし、weight[]をかけることでぼかしをかけている
color += texture(tex, vTexCoord + vec2(0.0, 0)).rgb * weight[0];
if(horizontal) {
for(int i = 1; i < 5; i++) {
color += texture(tex, vTexCoord + vec2(strength * i, 0.0)).rgb * weight[i];
color += texture(tex, vTexCoord - vec2(strength * i, 0.0)).rgb * weight[i];
}
} else {
for(int i = 1; i < 5; i++) {
color += texture(tex, vTexCoord + vec2(0.0, strength * i)).rgb * weight[i];
color += texture(tex, vTexCoord - vec2(0.0, strength * i)).rgb * weight[i];
}
}
vFragColor = vec4(color.rgb, 1.0);
}
簡単に説明すると、texture(tex, vTexCoord)
には入ってきたテクスチャのUV座標内のピクセルの値が入っているので、vTexCoord
の値を少しずらしてあげると少しだけずれたピクセルにアクセスすることができます。これを利用して少しずらしたピクセルの値を足していってあげることで、ぼかした画像が出来上がります。
テクスチャのUV座標の話については、以下のサイトが非常にわかりやすく載っているので、そちらを参照してみてください。
テクスチャマッピング · けんごのお屋敷
さて、ガウシアンぼかしがかけられました。結果は以下のようになります。
- X方向のぼかし

- XとY方向のぼかし

3. 元の描画結果とぼかし結果を加算合成
最後です。元の描画結果とぼかし結果を加算合成します。c++側ではこれまでと同じように描画結果のテクスチャをシェーダ側に送っているだけなので、以下ではシェーダのコードを載せたいと思います。
# version 150
precision mediump float;
uniform sampler2DRect tex;
uniform sampler2DRect blur;
in vec2 vTexCoord;
out vec4 vFragColor;
void main() {
const float gamma = 2.2;
vec3 p = texture(tex, vTexCoord).rgb;
vec3 b = texture(blur, vTexCoord).rgb;
p += b;
vec3 result = vec3(1.0) - exp(-p * 1.0);
result = pow(result, vec3(1.0 / gamma));
vFragColor = vec4(result, 1.0);
}
tex
が元の描画結果、blur
がぼかし結果のテクスチャです。ここでは、まずp
とb
にピクセルの色を格納し、ダイレクトに加算合成しています。そのあとresult
にp
の値を調整した値を入れて出力して終了です。result
ではガンマ補正をかけていますが、そこまで気にしなくてもいい部分だと思います。見栄えが良くなる程度に思っておいていいと思います。(自分もそこまで理解していない...)
最終的な出力結果は最初と同じですが、以下のようになります。
まとめ
今回のオブジェクトの例だとあまりわかりづらいかもしれませんが、要するにオブジェクトの元のエッジを残しつつ、周りにぼんやりとした部分を作ることで、光が溢れているようなエフェクトを作っています。また、加算合成をしているので、ピクセルの色の値がそこまで大きくない場合などは色が非常に明るい表現へと変化します。
以下に、自分のスケッチに適用してみたらとても変化した例があるので載せます。
- エフェクトをかける前

- エフェクトをかけた後

非常に明るくなっているのがわかると思います。このように光が溢れているような表現を使いたいときはとても使えるエフェクトなのでいいと思います。こんなことしなくても、こんな感じの表現ができるaddonはもう存在しているようですが。。。(笑)
このエフェクトも対象オブジェクトの大きさなどによって、強度などを変えたほうが良さそうなので、その辺は各自で試してみてください。あと、自分自身理解していない部分や拙い部分があるので、この辺違うよ!とか、こっちの方がいいよ!的なことを言って頂けるとありがたいです!!編集リクエストバンバン送ってください!!!
oFがTDやHoudiniなどに代替されていく中で、まだまだoFを書き続けていきたいと思うものです(笑)