概要
Grimoirejsをつかってポストエフェクトする話。
Grimoirejsについての基本的なことは他の記事を参照ください。
シェーダーに関する説明はあまりしません。
できたもの↓
ポストエフェクトとは
エフェクトと言う通り、簡単に言えば最終的に画面に表示される前に画像を加工する工程のことです。
トーンマッピング、モーションブラー、被写界深度、レンズフレア、ブルームエフェクト、トゥーンシェーディングなど様々あります。
レンダリングパス
Grimoirejsはタグ(Goml)でシーンを記述できましたが、ポストエフェクトするのもタグで行えます。
<import-material typeName="myPostEffect" src="./my-post-effect.sort"/>
<renderer camera=".camera" viewport="0,0,512,512">
<render-buffer name="rb"/>
<texture-buffer name="bb1"/>
<render-scene out="bb1" depthBuffer="rb"/>
<render-quad material="new(myPostEffect)" source="backbuffer(bb1)" out="default"/>
</renderer>
ポストエフェクトを行わないときは<render-scene>
タグのout
属性をdefault
にすることで、そのままシーンが画面に表示されます。
今回はout
にbb1
という名前のバックバッファ(<texture-buffer>
)にオフスクリーンレンダリングします。
そのあと<render-quad>
タグのsource
属性にバックバッファ(backbuffer(bb1)
)を指定することでポストエフェクトが行えるようになります。
my-post-effect.sort
はポストエフェクトで利用するシェーダーコードです。参考
レンダリングパスに関する詳細はこちらの記事を参照ください。
ポストエフェクトする
実際にポストエフェクトを掛けながら流れを追っていきましょう。
シェーダーを書く
ポストエフェクトに使うシェーダーを書きます。Grimoire.jsではsortファイルにシェーダーを記述します。
今回は簡単なビネッティングのエフェクトを作ります。ビネッティングとは写真の隅のほうが暗くなる物理現象です。
下は実際のsortファイルになります。<render-quad>
タグのsource
に指定したテクスチャバッファが、uniform変数のsource
からアクセスできるようになっています。
#ifdef FS
以下がフラグメントシェーダーになっており、今回のビネッティング効果の実装は主にその中です。中心からの距離によってだんだんと暗くなるようにしています。
@Pass
@NoBlend()
@NoDepth()
FS_PREC(mediump,float)
varying vec2 vTexCoord;
#ifdef VS
attribute vec3 position;
attribute vec2 texCoord;
void main(){
gl_Position = vec4(position, 1.);
vTexCoord = texCoord;
}
#endif
#ifdef FS
uniform sampler2D source;
uniform vec2 _viewportSize;
@{default:"false"} // この記法を用いるとタグの属性として変更が可能になります
uniform bool pass;
@{default:"0.9"}
uniform float spread;
@{default:"0.7"}
uniform float size;
vec4 alphaBlend(vec4 base, vec4 blend) {
return vec4(base.rgb * base.a * (1.0 - blend.a) + blend.rgb * blend.a, blend.a + base.a * (1.0 - blend.a));
}
void main() {
vec2 iTexCoord = vec2(1., -1.) * vTexCoord;
if (!pass) {
float p = atan(length((iTexCoord + vec2(-0.5, 0.5)) * 2. * _viewportSize * size / length(_viewportSize)) * 0.7071 / spread);
float cos1 = cos(p);
// なんとなくコサイン四乗則
gl_FragColor = alphaBlend(texture2D(source, iTexCoord), vec4(0., 0., 0., clamp(1. - (cos1 * cos1 * cos1 * cos1 / (spread * spread)), 0.0, 1.0)));
} else {
gl_FragColor = texture2D(source, iTexCoord);
}
}
#endif
シェーダーを適用する
<import-material>
タグで先程のシェーダー(sortファイル)をよみこみます。今回はindex.sort
という名前にしました。
<render-quad>
のmaterial
属性でnew(マテリアルのtypeName)
とすることでシェーダーを適用できます。
下のようにGomlを編集します。(シーンの記述は含まれていません)
render-scene
→ texture-buffer[name=bb1]
→ render-quad
→ 画面
という順でレンダリングされた画像が渡っています。
<import-material typeName="vignetting" src="./index.sort"/>
<renderer camera=".camera" viewport="0,0,512,512">
<render-buffer name="rb"/>
<texture-buffer name="bb1"/>
<render-scene out="bb1" depthBuffer="rb"/>
<render-quad material="new(vignetting)" source="backbuffer(bb1)" out="default"/>
</renderer>
こんな絵ができました。
ポストエフェクトをタグにする
できたポストエフェクトをタグにして配布できるようにしたいですね。
具体的には<import-material>
タグと<render-quad>
タグをまとめて<render-vignetting>
タグをつくります。<render-vignetting>
をいれるだけでビネッティング効果が得られるようになります。うれしいですね。
下のようにJavaScriptを書きました。MaterialFactory.addSORTMaterial("vignetting", shader)
でシェーダーをマテリアルの名前をつけて登録できます。
そして<render-vignetting>
が使えうようにregisterNode
を行います。参考
// --- browserify(commonjs)でやる方法
const {MaterialFactory} = require('grimoirejs-fundamental').default.Material;
const shader = require('index.sort'); // stringify
// --- scriptタグで読み込む方法
// const MaterialFactory = gr.lib.fundamental.Material.MaterialFactory;
// const shader = ``; // シェーダーのコードをテキストとしてそのまま埋め込む
MaterialFactory.addSORTMaterial("vignetting", shader);
gr.registerNode("render-vignetting", [], {
material: "new(vignetting)"
}, "render-quad");
実際に使ってみます。
<renderer camera=".camera" viewport="0,0,512,512">
<render-buffer name="rb"/>
<texture-buffer name="bb1"/>
<render-scene out="bb1" depthBuffer="rb"/>
<render-vignetting source="backbuffer(bb1)" out="default"/>
</renderer>
きれいになました。うれしいですね。
もっとポストエフェクトする
この調子でもっとポストエフェクトしていきます。今回はアンチエイリアス、収差ぽいもの、HUDをさらに追加します。
いままでやってきた、シェーダーを書く、タグにする、Gomlに適用するを繰り返して最終的にGomlは次のようになりました。
ポストエフェクトの適用順は、アンチエイリアス→HUD→収差→ビネッティング、です。
数が増えて複雑に見えますが、流れは先程と同様です。
render-scene
→ texture-buffer[name=bb1]
→ render-fxaa
→ texture-buffer[name=bb2]
→ render-hud
→ texture-buffer[name=bb3]
→ render-aberration
→ texture-buffer[name=bb4]
→ render-vignetting
→ 画面
という順でレンダリングされた画像が渡っています。テクスチャバッファへの書き出しと読み込みの仕組みがわかれば単純だと思えるはずです。
<renderer camera=".camera" viewport="0,0,512,512">
<render-buffer name="rb"/>
<texture-buffer name="bb1"/>
<texture-buffer name="bb2"/>
<texture-buffer name="bb3"/>
<texture-buffer name="bb4"/>
<render-scene out="bb1" depthBuffer="rb"/>
<render-fxaa id="fxaa" source="backbuffer(bb1)" out="bb2"/>
<render-hud id="hud" source="backbuffer(bb2)" out="bb3" texture="sushi.png"/>
<render-aberration id="aberration" source="backbuffer(bb3)" out="bb4" power="2.0" coef="3.4"/>
<render-vignetting id="vignetting" source="backbuffer(bb4)" out="default" spread="0.9" size="0.7"/>
</renderer>
最終的にこんな絵ができました。
よくわかりませんね。いちおう下のリンクで遊べます。
で
タグでシェーダーを当てる順番を記述することで、かんたんにポストエフェクトを重ねることができました。使うだけなら分かりやすいと感じられたと思います。
シェーダーを書く人にも、レンダリングパスをxmlで宣言的に記述できることによって、より明確になるのではないでしょうか。
🍣
みなさんもGrimoirejsで🍣しましょう。