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


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

#Starling2.0フィルタの特徴

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

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

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

![DemoCapture_2016_5_29_15_30_16.png](https://qiita-image-store.s3.amazonaws.com/0/8302/2d91cb13-9c73-329e-f2cc-a868417d88f5.png "DemoCapture_2016_5_29_15_30_16.png")
デモは[ここ](http://harayoki.github.io/qiita_demo/posterizationTest/)にあります。各スライダーを操作して、RGBAチャンネルそれぞれでポスタリゼーションのかかり具合を調整できます。写真の女性は画像処理でおなじみのLenaさん。:woman: 

![a.png](https://qiita-image-store.s3.amazonaws.com/0/8302/e1cfb52d-3d17-cc82-2982-3f9d366c93c5.png "a.png")
画像は左上から、フィルタ無し、ポスタリゼーションフィルタのみ、ポスタリゼーションフィルタを適用してからぼかしフィルタ適用、ぼかしフィルタを適用してからポスタリゼーションフィルタ適用、と、なっています。多重フィルタも正しく動いているようです。(そのあたり考慮せずコードを書いても、かってにそのように動きます。)

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

![b.png](https://qiita-image-store.s3.amazonaws.com/0/8302/a1e622ff-0397-4f0c-1a13-25d88ce4617f.png "b.png")

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

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

各チャンネルの分解能を指定して使います。意味的に最低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](https://gist.github.com/PrimaryFeather/31f1dd7f04cd6ce886f1)

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

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

動作サンプルのコードは[ここ](https://github.com/harayoki/-qiita1/blob/posterizationTest/src/demos/MyFirstFilterDemo.as)にあります。


#ポスタリゼーションフィルターの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](http://jacksondunstan.com/articles/1664)に詳しく書いてあります。英語なので、そのうち訳します。

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

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

```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コーディング最中はどこでおかしな結果になっているかわからず、苦労しました。

#フィルターのバグ？
動作サンプルを作っているに当たって、１つのフィルターを複数の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