Starling2.0でカスタムフィルターを作ってみる:ポスタリゼーションフィルタ


Starling2.0から、カスタムレンダリングの仕組みが少し変わりました。カスタムレンダリングの実装方法にはフィルターとメッシュスタイルの2つがあり、一長一短です。公式のガイドにのっとり、作るのが簡単なフィルタの方に手を出してみます。今回は、以前AGALを生で触って勉強していた時に作ったポスタリゼーション(階調飛ばし)のコードをフィルターとして移植してみました。

Starling2.0フィルタの特徴

  • 派手なエフェクトを作成できる
  • 多重にフィルタを重ねがけできる
  • Drawコールが増えるので、処理が重め
  • 作るのが(MeshStyleに比べて)簡単
  • DisplayObjectだけでなく、DisplayObjectContainerにも適用できる

ざっとこんな感じです。MeshStyleについては別途投稿します。

ポスタリゼーションフィルターの動作サンプル


デモはここにあります。各スライダーを操作して、RGBAチャンネルそれぞれでポスタリゼーションのかかり具合を調整できます。写真の女性は画像処理でおなじみのLenaさん。


画像は左上から、フィルタ無し、ポスタリゼーションフィルタのみ、ポスタリゼーションフィルタを適用してからぼかしフィルタ適用、ぼかしフィルタを適用してからポスタリゼーションフィルタ適用、と、なっています。多重フィルタも正しく動いているようです。(そのあたり考慮せずコードを書いても、かってにそのように動きます。)

ちなみにこの状態のポスタリゼーションは Red:8階調、Green:8階調、Blue:4階調で、MSXという昔のコンピュータのScreen8仕様です。うーん、それっぽい!

スライダーを操作すると、階調の飛ばし具合が変化します。この手のフィルタで透明度までポスタリゼーションをかけられるのは珍しいかもしれない。(しかしその対応が面倒だった。後述。)

ポスタリゼーションフィルタの使い方

各チャンネルの分解能を指定して使います。意味的に最低2、最大256です。

sample.as
var filter:PosterizationFilter = new PosterizationFilter();
filter.redDiv = 2; // redチャンネルの分解能 意味的にmax256まで
filter.greenDiv = 4; // greenチャンネルの分解能
filter.blueDiv = 8; // blueチャンネルの分解能
filter.alphaDiv = 2 // alphaチャンネルの分解能
dobj.filter = filter;

下記は上記と同じ

sample.as
var filter:PosterizationFilter = new PosterizationFilter(2,4,8,2);
dobj.filter = filter;

フィルタの大雑把なつくり方

公式のガイドで配布されているコードをひながたに改造していくのが簡単です。
ColorOffsetFilter.as

フィルター利用者とのインターフェースはFragmentFilterを継承したクラスに、フィルターの実装本体はFilterEffectを継承したクラスに書きます。ここが別々になってるのはFilterEffectを再利用しようとかそういう意図がありそうですが、通常は1対1の関係になるので、上記ColorOffsetFilterのように、FilterEffect側はインナークラスで書いてしまえば良いと思います。

今回のポスタリゼーションフィルターのコードはここにあります。上記ColorOffsetFilterを元に作りました。
harayoki.starling.filters.PosterizationFilter
FragmentFilter側ではフィルタのパラメータをそのままFilterEffectに伝えて再描画命令を出しているだけです。ほぼ何もしていません。FilterEffect側には実際の描画AGALコードがあり、フィルタのパラメータを2つの定数としてAGALに送っています。はい、それだけです。ここまでは簡単です。

動作サンプルのコードはここにあります。

ポスタリゼーションフィルターのAGALコード

AGAL部分はいわゆるマシン語なので難解ですが、コツをつかめば書けるようになってきます。途中でレジスタ(変数)の値がどうなっているかわからないので、うまく動かない場合は別途似たようなASコードを疑似的に書いて動作させつつ確認すると楽なようです。

Vertex ShaderのAGALコード

既存のAGALコードをそのまま使います。FilterEffect.STD_VERTEX_SHADERに定数定義されています。Starlingで頂点情報を変更するフィルターを作ることはあまりなさそうので、書かなくて良い、と認識すれば良いです。一応中身は下記のようになっています。

agal.as
//回転行列を座標に掛け合わせる
"m44 op, va0, vc0"
//カラーはそのままFragment Shaderに受けわたす
"mov v0, va1"

各レジスタが何を意味するのかなど今回は解説しませんが、ここのページIntroduction to AGAL: Part 2に詳しく書いてあります。英語なので、そのうち訳します。

Fragment ShaderのAGALコード

先に書いた通り、アルファチャンネルのポスタリゼーションに対応したので長くなりました。もっと効率の良い書き方はあると思いますが、まあそこはご容赦を。

ポスタリゼーションとしての処理は、一旦RGBA値(0.0~1.0)をN(階調段階数)倍して小数点以下を切り落とし、N-1で割って元に戻すと、うまい具合に階調が飛びます。NではなくN-1で割るのは、小数点以下を切り落とすだけだと、色の成分が下によってしまうからです。N-1で割って元に戻すことで、うまい具合の範囲で階調が同じになります。小数点以下切り落としの命令が見当たらなかったので、2つの命令を組み合わせてそこを実現しています。

agal.as
// テクスチャカラーをft0に取得するお決まりコード
tex("ft0", "v0", 0, texture) // == ft0, v0, fs0 <2d, linear>
// PMA(premultiplied alpha)演算されているのを元の値に戻す  rgb /= a
"div ft0.xyz, ft0.xyz, ft0.www"
// 各チャンネルにRGBA定数値(fc0)を掛け合わせる
"mul ft0, ft0, fc0"
// ft0の小数点以下を破棄 ft1 = ft0 - float(ft0)、ft0 -= ft1
"frc ft1, ft0"
"sub ft0, ft0, ft1"
// 定数ft0を掛けた際より1小さい値が定数1にはいっているので、それで割って戻す
"div ft0, ft0, fc1"
// 1.0を超える部分ができるので正規化 (sat : 0.0~1.0に収める命令)
"sat ft0, ft0"
// PMAをやり直す rgb *= a
"mul ft0.xyz, ft0.xyz, ft0.www"
// ocに出力
"mov oc, ft0"

アルファチャンネルが絡んでくると、公式ガイドにあるように、PMA(premultiplied alpha)を考慮しないといけないので、若干面倒です。できあがれば簡単なコードになりますが、AGALコーディング最中はどこでおかしな結果になっているかわからず、苦労しました。

フィルターのバグ?

動作サンプルを作っているに当たって、1つのフィルターを複数のDisplayObjectに適用すると、DisplayObjectが消えたり、ランタイムエラーが出ることに気づきました。バグなのか仕様なのかわかりませんが、flashネイティブではフィルタを複数のDisplayObjectに適用することが可能なので、直して欲しいですね。今回はフィルターのインスタンスを複数作って対応しました。こちら、問い合わせてみます。

まとめ

世の中にはAGALを簡単に記述するための高級言語やクラスも存在するようですが、今はまず手作業で書きつつ勉強することにしています。AGALについても何本がまとめを投稿する予定です。

なお、StarlingではAGALバージョン1.0を使っています。現在の最新はAGAL3です。AGAL2がデフォルトになる日はいつの日か。 AGAL2は2014/1のリリースなのでそろそろいいんではないかな。。
http://labsdownload.adobe.com/pub/labs/flashruntimes/shared/air16_flashplayer16_releasenotes.pdf