wgld.orgの管理人であるdoxasさん主催のWebGLスクールの第十回目です。
今回はポストエフェクトについてです。
前回までのまとめ
第一回 WebGLスクール 「WebGLの概念」
第二回 WebGLスクール 「WebGLの手続きと手順」
第三回 WebGLスクール 「シェーダの基礎」
第四回 WebGLスクール 「行列とクォータニオンについて知る」
第五回 WebGLスクール 「ライティングの基本」
第六回 WebGLスクール 「テクスチャで画像データを使用する」
第七回 WebGLスクール 「ブレンドファクターとアルファブレンディング」
第八回 WebGLスクール 「シェーダエフェクトテクニック」
ポストエフェクトとは
一度オフスクリーンにレンダリングしたシーンに対して、見た目を変更すること。
出来ることはとても多く、難易度もバラつきがある。
ポストエフェクトの例としては、「モザイク」「ブラー」「エッジ検出」「カラー調整」「フィルタ処理」などがある。
ネガポジ反転シェーダ
色を反転させる。シェーダを以下のように修正。
void main(){
vec4 texColor = texture2D(texture, vec2(vTexCoord.x, 1.0 - vTexCoord.t));
// 色の反転
vec3 negaColor = 1.0 - texColor.rgb;
gl_FragColor = vec4(negaColor, texColor.a);
}
ブラウン管シェーダ
ブラウン管のようなノイズを表現する。ノイズ表現はサイン波を使うことで実装できる。
void main(){
vec2 centerOrigin = vTexCoord * 2.0 - 1.0;
float cornerShade = 1.0 - pow(length(centerOrigin * 0.75), 10.0);
// sinを使ってノイズを表現
float noiseLine = (3.0 + sin(vTexCoord.y * 200.0)) / 4.0;
vec4 texColor = texture2D(texture, vec2(vTexCoord.x, 1.0 - vTexCoord.t));
gl_FragColor = vec4(texColor.rgb * noiseLine, texColor.a);
}
モノクロフィルタ
レンダリング結果をモノクロにするフィルタ。変換する方法は NTSC 系加重平均方 を用いる。
人間の視覚が感じやすい色の強弱を加味し、係数を掛けて輝度を算出する。
人間の目は緑をより認識しやすい。
色の度合いを係数で宣言する。
const float redScale = 0.298912;
const float greenScale = 0.586611;
const float blueScale = 0.114478;
上記の係数はvec3
で管理でき、テクスチャから読みだしたRGB値を乗算して平均化する。
内積を求めることで同様のことができる。
内積の計算方法
内積(ドット積)で v1 ・ v2 を求める場合
v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
シェーダを修正する。
void main(){
vec4 texColor = texture2D(texture, vec2(vTexCoord.x, 1.0 - vTexCoord.t));
// 内積を求める
float mono = dot(texColor.rgb, vec3(redScale, greenScale, blueScale));
gl_FragColor = vec4(vec3(mono), texColor.a);
}
ソーベルフィルタ
ソーベルフィルタはエッジ検出に用いられる手法。周囲のテクセルの色情報と比較して 色の差分の強さ を求める。
テクセルとはテクスチャピクセルのこと。
ソーベルフィルタはカーネルを用いて表現する。
カーネル
カーネルは色の差分を判断するのに使う。テクセルからどのくらいの色を参照するかの係数を格納したマップみたいなもの。
カーネルの向き
カーネルには縦と横の方向があり、JS側で配列として格納しシェーダに送る。
// 横方向カーネル
var hKernel = [
1.0, 0.0, -1.0,
2.0, 0.0, -2.0,
1.0, 0.0, -1.0
];
// 縦方向カーネル
var vKernel = [
1.0, 2.0, 1.0,
0.0, 0.0, 0.0,
-1.0, -2.0, -1.0
];
カーネルuniformデータで送信
送信にはuniform1fv
を使う。
1fvのfはfloat、vは配列を意味している。
gl.uniform1fv(orthoUniLocation[2], hKernel);
gl.uniform1fv(orthoUniLocation[3], vKernel);
シェーダでuniform配列を受け取る
uniform float hKernel[9];
uniform float vKernel[9];
シェーダで配列を受ける場合は、 配列数に変数を使うことができない。
ガウシアンフィルタ
ガウシアンブラーと呼ばれるぼかしフィルター。ぼかしフィルターは汎用的に使えるので使えると表現が広がるガウシアンブラーはブラー処理の中では、軽量で高品質。
ただし、実装は手間でフラグメントシェーダがややこしいことになる。
実装概念
今までフレームバッファが1つだったが、ガウシアンブラーでは 2つのフレームバッファを使用する。
縦と横のぼかしが別々で処理され、1つ目のフレームバッファで横にぼかし、それを再度参照し縦方向にぼかしをかける。
JSで重み係数の算出
JS側でガウシアンブラーを掛けるために重み係数を算出しておく。
重み係数は、周囲のテクセルからどの程度の影響度で色を拾ってくるかというもの。
重み係数の算出には ガウス関数 という概念を使う。
コード的には以下のようにして求める。
var weight = new Array(10);
var t = 0.0;
// ぼかしの強度
var d = 100.0;
for(var i = 0; i < weight.length; i++){
var r = 1.0 + 2.0 * i;
var w = Math.exp(-0.5 * (r * r) / d);
weight[i] = w;
if(i > 0){w *= 2.0;}
t += w;
}
for(i = 0; i < weight.length; i++){
weight[i] /= t;
}
シェーダ側の実装
シェーダ側では、求めた重み係数を受け取り、重み係数分処理を記述する。
void main(){
float tFrag = 1.0 / 512.0;
vec2 fc;
vec3 destColor = vec3(0.0);
if(horizon){
fc = vec2(gl_FragCoord.s, 512.0 - gl_FragCoord.t);
destColor += texture2D(texture, (fc + vec2(-9.0, 0.0)) * tFrag).rgb * weight[9];
destColor += texture2D(texture, (fc + vec2(-8.0, 0.0)) * tFrag).rgb * weight[8];
destColor += texture2D(texture, (fc + vec2(-7.0, 0.0)) * tFrag).rgb * weight[7];
destColor += texture2D(texture, (fc + vec2(-6.0, 0.0)) * tFrag).rgb * weight[6];
destColor += texture2D(texture, (fc + vec2(-5.0, 0.0)) * tFrag).rgb * weight[5];
destColor += texture2D(texture, (fc + vec2(-4.0, 0.0)) * tFrag).rgb * weight[4];
destColor += texture2D(texture, (fc + vec2(-3.0, 0.0)) * tFrag).rgb * weight[3];
destColor += texture2D(texture, (fc + vec2(-2.0, 0.0)) * tFrag).rgb * weight[2];
destColor += texture2D(texture, (fc + vec2(-1.0, 0.0)) * tFrag).rgb * weight[1];
destColor += texture2D(texture, (fc + vec2( 0.0, 0.0)) * tFrag).rgb * weight[0];
destColor += texture2D(texture, (fc + vec2( 1.0, 0.0)) * tFrag).rgb * weight[1];
destColor += texture2D(texture, (fc + vec2( 2.0, 0.0)) * tFrag).rgb * weight[2];
destColor += texture2D(texture, (fc + vec2( 3.0, 0.0)) * tFrag).rgb * weight[3];
destColor += texture2D(texture, (fc + vec2( 4.0, 0.0)) * tFrag).rgb * weight[4];
destColor += texture2D(texture, (fc + vec2( 5.0, 0.0)) * tFrag).rgb * weight[5];
destColor += texture2D(texture, (fc + vec2( 6.0, 0.0)) * tFrag).rgb * weight[6];
destColor += texture2D(texture, (fc + vec2( 7.0, 0.0)) * tFrag).rgb * weight[7];
destColor += texture2D(texture, (fc + vec2( 8.0, 0.0)) * tFrag).rgb * weight[8];
destColor += texture2D(texture, (fc + vec2( 9.0, 0.0)) * tFrag).rgb * weight[9];
}else{
fc = gl_FragCoord.st;
destColor += texture2D(texture, (fc + vec2(0.0, -9.0)) * tFrag).rgb * weight[9];
destColor += texture2D(texture, (fc + vec2(0.0, -8.0)) * tFrag).rgb * weight[8];
destColor += texture2D(texture, (fc + vec2(0.0, -7.0)) * tFrag).rgb * weight[7];
destColor += texture2D(texture, (fc + vec2(0.0, -6.0)) * tFrag).rgb * weight[6];
destColor += texture2D(texture, (fc + vec2(0.0, -5.0)) * tFrag).rgb * weight[5];
destColor += texture2D(texture, (fc + vec2(0.0, -4.0)) * tFrag).rgb * weight[4];
destColor += texture2D(texture, (fc + vec2(0.0, -3.0)) * tFrag).rgb * weight[3];
destColor += texture2D(texture, (fc + vec2(0.0, -2.0)) * tFrag).rgb * weight[2];
destColor += texture2D(texture, (fc + vec2(0.0, -1.0)) * tFrag).rgb * weight[1];
destColor += texture2D(texture, (fc + vec2(0.0, 0.0)) * tFrag).rgb * weight[0];
destColor += texture2D(texture, (fc + vec2(0.0, 1.0)) * tFrag).rgb * weight[1];
destColor += texture2D(texture, (fc + vec2(0.0, 2.0)) * tFrag).rgb * weight[2];
destColor += texture2D(texture, (fc + vec2(0.0, 3.0)) * tFrag).rgb * weight[3];
destColor += texture2D(texture, (fc + vec2(0.0, 4.0)) * tFrag).rgb * weight[4];
destColor += texture2D(texture, (fc + vec2(0.0, 5.0)) * tFrag).rgb * weight[5];
destColor += texture2D(texture, (fc + vec2(0.0, 6.0)) * tFrag).rgb * weight[6];
destColor += texture2D(texture, (fc + vec2(0.0, 7.0)) * tFrag).rgb * weight[7];
destColor += texture2D(texture, (fc + vec2(0.0, 8.0)) * tFrag).rgb * weight[8];
destColor += texture2D(texture, (fc + vec2(0.0, 9.0)) * tFrag).rgb * weight[9];
}
gl_FragColor = vec4(destColor, 1.0);
}
ポストエフェクトとの向き合い方
ポストエフェクトを用いると、同じシーンであってもまったく違う表現が可能。
シェーダが複数になったり、フレームバッファが増えたりと実装は大変だけど得られる効果は大きい。
ポストエフェクトは便利な代わりに、なんどもレンダリングしたりシェーダを実行したりと 負荷の増加に注意する。
感想
今回はポストエフェクトについて学びました。ポストエフェクトを使えば本当にいろいろな表現を実現できるので、面白い。
ただ、シェーダ側でループ処理をするのがけっこう面倒でちょっとびっくりしました。
とにかくシェーダのコードは 慣れ とのことなので、たくさん書いてなれるしかない。
次回は環境マッピングについて学びます。