この記事は、Starling2.0でカスタムフィルターを作ってみる:ポスタリゼーションフィルタの続きです。
カスタムレンダリングの仕組みとして新たにStarling2.0から登場したカスタムエフェクトを(フィルタのコードを改変して)作ったので、解説します。結論、多少敷居が上がりますが、フィルタのAGALコードはほぼ再利用できるので、フィルタが作れるのなら実装は難しくはありません。
Staring2.0メッシュエフェクトの概要および特徴
- フィルタより高速に動くエフェクトを作成できる
- 1つのDisplayObject(正確にはMesh)に1つのエフェクトしか設定できない。フィルタのように多重に適用できない。
- 作るのが(Filterに比べて)若干難しい プログラム的な制限も多い
- DisplayObjectContainerには適用できない
- Meshの領域外にはみでる効果は作成できない (例えばぼかしエフェクトなどは作れない)
ざっとこんな感じです。デメリットが多く見えてしまいますが、同じスタイルが適用されているMeshはドローコールがまとまるため、フィルタに比べてずっと高速に動作させることができます。これは最大の利点です。同じスタイルであれば、その属性値(例えば効果の強さなど)がそれぞれで違っていても、1つのドローコールにまとまりす。フィルタをDisplayObjectコンテナに適用した場合では個々のDisplayObjectで効果のかかり具合を別に設定することはできませんね。
ここに2Dイメージにライティングをかけて立体感を出している動作サンプルがありますが、これもエフェクトです。加工のために追加のテクスチャ情報を読み込ませており、ここまで派手なことができます。そしてこれをたくさん並べてもドローコールが1です。
ポスタリゼーションエフェクトの動作サンプル
ここでポスタリゼーションエフェクトが動作しているのを確認できます。左上が効果なし、それ以外が効果あり、右下だけぼかしフィルターをエフェクトに追加で適用しています。ドローコール3の内訳は、デフォルトスタイル+ポスタリゼーションスタイル+ぼかしフィルタ、です。
下のスライダーで効果のかかり具合を変更できます。チェックボックスで、スタイルやフィルタの適用を無効にできます。
ポスタリゼーションスタイルの使い方
フィルタと同じく、各チャンネルの分解能を指定して使います。意味的に最低2、最大256です。
var style:PosterizationStyle = new PosterizationStyle();
style.redDiv = 2; // redチャンネルの分解能
style.greenDiv = 4; // greenチャンネルの分解能
style.blueDiv = 8; // blueチャンネルの分解能
style.alphaDiv = 2; // alphaチャンネルの分解能
dobj.setStyle(style);
下記は上記と同じ
var style:PosterizationStyle = new PosterizationStyle(2, 4, 8, 2);
dobj.setStyle(style);
スタイルの大雑把な作り方
フィルタ同様に公式のガイドで配布されているコードをベースに改造していくと簡単です。
ColorOffsetStyle
エフェクト利用者とのインターフェースはMeshStyleを継承したクラスに、エフェクトの実装本体はMeshEffectを継承したクラスに書きます。ここのあたりはフィルター側と同じです。今回もMeshEffect側はインナークラスで書いてしまいます。
今回作成したポスタリゼーションエフェクトのコードはここにあります。上記ColorOffsetStyleを元に作りました。
harayoki.starling.styles.PosterizationStyle
MeshStyle側でエフェクトのパラメータをそのままMeshEffectに伝えるのは同じですが、copyFrom、updateVerticesという2つのメソッドの実装が必要になります。ここは意味がわかればほぼ機械的に書けます。MeshEffect側には実際の描画AGALコードがあり、一見難しそうにみえますが、おきまりのAGALコードにフィルタ側で作ったAGALコードを移植するだけです。他、get vertexFormat、beforeDraw、afterDrawなど、こちらも機械的に対応できる実装部分があります。
フィルタではActionScript側からAGALに設定値を受け渡すのにFragment Constant値(定数)を使いましたが、対してスタイルでは、Starling2.0のレンダリング処理の都合からConstant経由で値を受け渡してはいけないことになっているため、VertexDataの各頂点に設定値をくっつけて処理を行う感じになります。大量に値のコピーができそうですが、ドローコールをまとめるためにこのような仕様となったとの事です。
動作サンプルのコードはここにあります。
MeshStyleのカスタムポイント
公式ガイドのColorOffsetStyleから変更した点をかいつまんで書きます。ColorOffsetStyle では、RGBA分解能のパラメータをユーザから受け取ってfloat4タイプ2つの変数に保持します。
private var _divs1:Vector.<Number>;
private var _divs2:Vector.<Number>;
↑RGBAの分解能を保持するプライベート変数です。2セットあるのは今回のAGALコード処理の都合です。
public static const VERTEX_FORMAT:VertexDataFormat =
MeshStyle.VERTEX_FORMAT.extend("divs1:float4").extend("divs2:float4");
↑(フィルタでは定数で受け渡していた)分解能パラメータをdivs1
およびdivs2
で受け渡すよという宣言です。ColorOffsetStyleでは1回のextendでした。AGALコード内でこれらの値の保持に使えるVaryingレジスタはAGAL1とAGAL2では最大8個で、Starling 2.0の内部処理ですでに2個使っています。そこにさらに2個使うよと宣言したのが上記です。
override public function copyFrom(meshStyle:MeshStyle):void {
var posterizationStyle:PosterizationStyle = meshStyle as PosterizationStyle;
if (posterizationStyle) {
for (var i:int = 0; i < 4; ++i) {
_divs1[i] = posterizationStyle._divs1[i];
_divs2[i] = posterizationStyle._divs2[i];
}
}
super.copyFrom(meshStyle);
}
↑カスタムスタイル作成時に必ず実装しなくてはいけないcopyFrom
メソッドです。内部保持変数(_divs1と_divs2)を別のPosterizationStyleインスタンスにコピーする実装を書きます。レンダリング時にインスタンスのコピーが必要になることがあるとの事です。最後のsuper.copyFrom(meshStyle);
の呼び出しを忘れないようにしましょう。
private function updateVertices():void
{
if (target)z
{
var numVertices:int = vertexData.numVertices;
for (var i:int=0; i<numVertices; ++i) {
vertexData.setPoint4D(i, "divs1",
_divs1[0], _divs1[1], _divs1[2], _divs1[3]);
vertexData.setPoint4D(i, "divs2",
_divs2[0], _divs2[1], _divs2[2], _divs2[3]);
}
setRequiresRedraw();
}
}
↑こちらも必ず実装しなくてはいけないupdateVertices
メソッドです。vertexDataに_divs1と_divs2で保持していた値を設定します。1つのMeshStyleにて複数のMeshインスタンスをさばくので、このように値の受け渡しが必要になるとの事です。最後のsetRequiresRedraw();
呼び出しを忘れないようにしましょう。
他は_divs、_divs2への値のget/setのつなぎを書いているだけです。
ざっとPosterizationStyleを眺めるとややこしそうに見えますが、詳細を確認すると、あまり頭を使わず実装できる内容です。扱う変数の数が同じなら、コードはほぼ似た感じになるでしょう。
MeshEffectのカスタムポイント
こちらもAGALコード部分以外は機械的に実装できる内容となります。
public static const VERTEX_FORMAT:VertexDataFormat = PosterizationStyle.VERTEX_FORMAT;
↑VERTEX_FORMATにスタイル側で定義したVERTEX_FORMATの参照を与えます。
override protected function beforeDraw(context:Context3D):void
{
super.beforeDraw(context);
vertexFormat.setVertexBufferAt(3, vertexBuffer, "divs1"); //v3レジスタにdivs1を紐付け
vertexFormat.setVertexBufferAt(4, vertexBuffer, "divs2"); //v4レジスタにdivs2を紐付け
}
↑必ず実装しなくてはいけないbeforeDraw
メソッドです。Varyingレジスタの何番目にVERTEX_FORMATで設定したデータを格納するかを記述します。最初のsuper.beforeDraw(context);
呼び出しを忘れないようにしましょう。
override protected function afterDraw(context:Context3D):void
{
context.setVertexBufferAt(3, null);
context.setVertexBufferAt(4, null);
super.afterDraw(context);
}
↑必ず実装しなくてはいけないafterDraw
メソッドです。利用したVaryingレジスタ設定をクリアします。最後のsuper.afterDraw(context);
呼び出しを忘れないようにしましょう。
ここまで、ルールに則ったコードを書いているだけで、内容は難しくないですね。
MeshEffectのAGALコード
全体構造としては、テクスチャが指定されている場合と、そうでない場合では前処理が別の処理になるため、その分岐があります。そこに演出(今回はポスタリゼーション)固有のコードが1つくっつきます。
if (texture)
{
vertexShader = [
"m44 op, va0, vc0", // お決まり処理:座標変換して確定
"mov v0, va1 ", // お決まり処理:テクスチャの読み取り位置を取得してv0でfragmentShaderへパス受け渡す
"mul v1, va2, vc4", // お決まり処理:PMA処理をして(vc4がアルファ値、va2がカラー値)v1で受け渡す
"mov v2, va3 ", // カスタム処理 va3(分解能1)をv2で受け渡す
"mov v3, va4 " // カスタム処理 va4(分解能2)をv3で受け渡す
].join("\n");
↑テクスチャ指定がある場合のvertexShader部分です。最後の2行以外はそのまま編集せず使えば良いです。
fragmentShader = [
tex("ft0", "v0", 0, texture) + // お決まり処理:ft0にテクスチャカラーを取得
"mul ft0, ft0, v1", // お決まり処理:元カラーにテクスチャカラーを合成
posterization // ポスタリゼーションのAGALコード 後述
].join("\n");
}
↑テクスチャ指定がある場合のfragmentShader部分です。posterization
部分は後ろで説明しますが、実際のポスタリゼーホンを行うAGALコードが入ります。
ここまで、Starling2.0のルールに則って記述する部分なので真面目に理解すようとすると敷居が上がりますが、毎度流用できるので、詳細まで理解していなくてもカスタムスタイル作成はできます。
次にテクスチャ指定がない場合の処理を見てみます。
else
{
vertexShader = [
"m44 op, va0, vc0", // お決まり処理:座標変換して確定
"mul v0, va2, vc4", // お決まり処理:PMA処理をして(vc4がアルファ値、va2がカラー値)v1で受け渡す
"mov v2, va3 ", // カスタム処理 va3(分解能1)をv2で受け渡す
"mov v3, va4 " // カスタム処理 va4(分解能2)をv3で受け渡す
].join("\n");
↑テクスチャ指定がない場合のvertexShader部分です。テクスチャがある場合から1行減っているだけであとは全く同じとなります。
fragmentShader = [
"mov ft0, v0", // ポスタリゼーションの共通処理ではft0にV0が入っている前提なので移動
posterization // ポスタリゼーションのAGALコード 後述
].join("\n");
}
↑テクスチャ指定がない場合のfragmentShader部分です。ft0(フラグメントシェーダー用のtempレジスタ)にカラー値を入れています。
あとはテクスチャのありなしに関係なく同じ実装になるposterization
部分です。下記にコメント付きのコードを掲載しますが、、
var posterization:String = [
// _divs1の内容がv2に入っているのでft1に取り出す
"mov ft1, v2",
// _divs2の内容がv3に入っているのでft2に取り出す
"mov ft2, v3",
// PMA(premultiplied alpha)演算されているのを元の値に戻す rgb /= a
"div ft0.xyz, ft0.xyz, ft0.www",
// 各チャンネルにRGBA定数値(ft1)を掛け合わせる ft1はもういらないので次の命令で上書き
"mul ft0, ft0, ft1",
// ft0の小数点以下を破棄 ft1 = ft0 - float(ft0)、ft0 -= ft1
"frc ft1, ft0",
"sub ft0, ft0, ft1",
// ft0を掛けた際より1小さい値(ft2)で割る
"div ft0, ft0, ft2",
// 1.0を超える部分ができるので正規化
"sat ft0, ft0",
// PMAをやり直す rgb *= a
"mul ft0.xyz, ft0.xyz, ft0.www",
// ocに出力
"mov oc, ft0"
].join("\n");
フィルターのAGALコードではActionScriptから受け渡された値がfc(定数レジスタ)に入っていたのに対して、スタイルではVaryingレジスタ(v2、v3)に入っているので、そこの取り扱いの変更しか対応していません。よってほぼコピペで対応できてしまう実装となります。段階的に対応すれば難しくないですね!
とりあえず、解説は以上です。
おわりに
随分長い投稿内容となりました。エフェクト単体のコードで見ると、フィルタよりだいぶ複雑に見えますが、ほとんどがおきまりパターンを踏襲でき、かつAGAL部分もフィルタのコードとほぼ変わりません。一番難しいのは元のAGAL部分なので、時間に余裕があればスタイルとフィルタは同時に作ってしまうのが良さそうです。
なお、今回のエフェクト(フィルタも)ではVertexBufferの属性データを追加で2つ使っていますが、ここを1つでやりくりすることもできました。サンプルコードとしては2つのままが良さそうなのでこのままとしておきます。フィルタとエフェクトを正式に公開する際は1つで済まそうと思います。