実際に作ってみる


なんかドキュメントみたいなものばかりでつまらないので、とりあえずプラグインを1個作ってみる。

普段僕が作ってる手順を簡単に説明していく(たまに僕自身も忘れてて苦労することがあるので)

作成手順 その1 コーディング前


コーディング前に考えておく事(仕様決定)実はここが一番時間がかかる。

Step.01 どんなプラグインを作るか?

まず、どんなプラグインを作るか決める。
今回はAfterEffectsにバンドルされてる「CC Toner.aex」と同じものを作ってみよう。

CC Toner.aexは僕がよく使うエフェクトプラグインで、まぁ簡単に言ってセピア化フィルタ。 つい最近16Bitに対応していない事に気がついて絶望したので、16Bit対応したものを作ってみることにする。

Step.02 アルゴリズムを考える。

想像だけどCC Toner.aexの、アルゴリズムはこんな感じ。
  1. 指定された3個の色からカラーテーブルを作成。
  2. ターゲット座標のピクセルを得る。
  3. 得たピクセルのRGBから輝度を計算(モノクロ化)
  4. 輝度を元にカラーテーブルから新しいピクセル値を得る
  5. 新しいピクセルを書き込む。その時アルファー値は元のピクセルのものを使う
  6. すべてのピクセルで同じ事を行う。

基本的なフィルタですな。最初作るカラーテーブルとは、1ピクセルごとに色の計算を行うのはかなり無駄なので 最初に輝度ごとに色の計算を終わらせて配列に収納しておいて、ピクセル毎には輝度の計算のみ行って事です。
テーブル参照とか、ジャンプテーブルとか呼ばれるテクニック。

でも、8Bitの時は輝度は0から255の256個ですむけど、16Bitの時は32768個にもなるのでかなり不経済。 多分CC Toner.aexが16Bitに対応していない原因だと思う。

8Bitの時はテーブルにするにして、16Bitの時はどうしようか?
考え方は以下の三つ。

  1. 1ピクセルごとに色の計算をする。
  2. 輝度をはしょって経済的な数まで減らす。
  3. 3万個ぐらいの配列は、HDフルサイズでたったの17Line(2048kByte)気にしないでテーブルにする

今回はテスト的にこの三つのやり方全部作ってみて、レンダリング時間・クオリティの結果を比較して正式版にすることにする。
まぁ、輝度をはしょる時は最大値255(つまり8Bit)と最大値4095(つまり12Bit)の2種類確認しよう。

追記
結局5種類になった。

Step.03 エフェクトコントロールのパラメータを決める。

まず最初にエフェクトコントロールに表示されるパラメータを決める。

僕が注意している点は、パラメータを不必要に多くしない事。 細かい調整をしたくなるのでプログラム組む時はパラメータは多くなる傾向があるけど、作業するときは少ない方が楽。とりあえず 内部的にはパラメータを変数で扱うが、ユーザーに操作させるパラメータはなるべく減らすべき。

今回はCC Toner.aexとまったく同じにするつもりなので以下のようになる。
Highlights
輝度が最大値の時の色
Midtones
輝度が中間値の時の色
Shadows
輝度が最小値の時の色
Blend w. Original
オリジナルの画像を合成する時の濃度(%)
0%でまったく合成しない。100%でオリジナルと同じ。
あと内部的に16Bit時の処理の切り替えも考えておく。

作成手順 その2 コーディング


実際のプログラム開始。
同じようなエフェクトプラグインとして、SDKのGamma_TableShifterがあるので参考にする。

一応テスト版のソース
FsTonerTest01.zip

Step.04 スケルトンからプロジェクトを作成

AE_Create.exeを使い、Tonerプロジェクトを作成。



AE_Create.exeは、Skeletonからプロジェクトの雛形を作るソフト。
きっと調べればVSの機能で雛形を簡単に作れると思うが、もう癖で自作のソフトで処理してる。

プロジェクト名を変えたい時は、AE_ProjectRename.exe(これも自作ソフト)で変更する。

どちらもスケルトンプログラムのアーカイブ内に入ってる。Delphiソースつき

Step.05 Headerファイル

まずHeaderに変数・構造体・マクロ等の定義を追加
この段階で、プラグインの仕様の詳細が決定される。

以下は、デバック用の定義。
正式リリース時には定義をコメントにする。
//動作確認用の時間経過表示スイッチ
#define TEST_MODE
	
エフェクトコントロールのID番号定義
//ユーザーインターフェースのID
//ParamsSetup関数とRender関数のparamsパラメータのIDになる
enum {
  MY_INPUT = 0,         // default input layer 
  MY_Highlights_COLOR,  //PF_Pixel
  MY_Midtones_COLOR,    //PF_Pixel
  MY_Shadows_COLOR,     //PF_Pixel
  MY_Blend_w_Original_FIXED,

  #ifdef TEST_MODE
    MY_TEST_MODE_POP, //モード切替
    MY_TEST_TIME_CB,  //時間表示フラグ
  #endif
  
  MY_NUM_PARAMS
  };
	
エフェクトコントロールをまとめた構造体。
typedef struct{
  PF_Pixel      Highlights;
  PF_Pixel      Midtones;
  PF_Pixel      Shadows;
  PF_Fixed      Blend_w_Original;
  #ifdef TEST_MODE
    long        test_mode;
    PF_Boolean  test_time;
  #endif
} MY_Params;
	
ITERATEで呼び出す関数用のパラメータ構造体。
typedef struct {
	PF_PixelPtr		colorTbl;
	MY_Params			*myprm;
} TonerInfo;
	

必要に応じていろいろ定義する。
定数値は、直接コード中には書かないでここにまとめておくとあとで楽。

文字列関係は別のコードにまとめてある。
Toner_Strings.cppToner_Strings.hを参照のこと。

Step.06 GlobalSetup

プラグインの初期化。
my_versionとout_data->out_flagsを設定。これはリソースの値と一致していないとAfterEffectsに怒られる。
CS3では、out_flags2って項目が増えていた。
static PF_Err GlobalSetup (
  PF_InData   *in_data,
  PF_OutData    *out_data,
  PF_ParamDef   *params[],
  PF_LayerDef   *output )
{
  PF_Err  err = PF_Err_NONE;

  //AE_Effect.hで定義されるPF_VERSIONマクロで求められる値を
  //リソースファイルのAE_Effect_Versionと一致させる事
  out_data->my_version = PF_VERSION(  MAJOR_VERSION, 
                    MINOR_VERSION,
                    BUG_VERSION, 
                    STAGE_VERSION, 
                    BUILD_VERSION);

  out_data->out_flags |=
    PF_OutFlag_USE_OUTPUT_EXTENT  |
    PF_OutFlag_PIX_INDEPENDENT  |
    PF_OutFlag_DEEP_COLOR_AWARE;

  return err;
}
out_flagsはプラグインの動作をAfterEffectsに伝えるためのフラグで、詳細はAE_Effect.hを参照。
僕は決まりきったプラグインしか作っていないので、ほとんど変更していない。
まぁ、フレームごとに描画させる時とか16Bit対応している時には変更の必要がある。
ちなみにPF_OutFlag_DEEP_COLOR_AWAREが16Bit対応のフラグ。

Step.07 SequenceSetup

カラーテーブル用のメモリを確保
普通はここでグローバルなメモリを確保するのだが、 今回確保するテーブルのサイズがパラメータによって変化させたかったので、ここではしていない。

後、大きなメモリを確保しようとするとAfterEffectsが変な動作起こす時がある。
1画面分丸々確保しようとすると変な事になる(僕だけかなぁ)

サンプルのGamma_Tableを見れば使い方はわかる。
ハンドルでの確保なのでキャストに注意。

Step.08 SequenceSetdown

カラーテーブル用のメモリを破棄

今回は未使用。

Step.09 SequenceResetup

同上

Step.10 ParamsSetup

エフェクトコントロール用のインターフェースの作成。
ここはAE_Macros.hで定義された便利なマクロを使う。詳細はスケルトンのソースを参照。

Step.11 Render

ここで実際の描画を行う。
エフェクトコントロールに入力されたパラメータを確保したり、必要な動作を行う。

16Bit/8Bitの切り替えもここで行う。
僕は完全に16bit/8bit用の処理を分けてコーディングしている。C++の機能を使えばまとめることも可能だが、 実行時の速度が変に遅くなったりとかいろいろあったのでそうしている(勉強しなくちゃなぁ)

今回は、
  1. AfterEffectsの状態を調べて構造体に収納
  2. 入力されたパラメータを調べて構造体に収納
  3. テストモードによってカラーテーブルのメモリを確保
  4. カラーテーブルを作成
  5. ITERATEで画像にアクセスして1ピクセルごとに処理(モードによって分岐あり)
  6. 処理が終わったら、メモリを破棄
って感じになっている。

Step.12 リソースファイル

以下にリソース定義ファイルを全部。
#ifndef MSWindows
#include "AE_General.r"
#endif
resource 'PiPL' (16000) {
	{	/* array properties: 12 elements */
		/* [1] */
		Kind {
			AEEffect
		},
		/* [2] */
		Name {
			/*AEのメニューに使われる */
			"F's Toner(TEST01)"
		},
		/* [3] */
		Category {
			/*AEのメニューに使われる */
			"F's Plugins(16Bit)"
		},
		
#ifdef MSWindows
		CodeWin32X86 {"EntryPointFunc"},
#else
		CodeMachOPowerPC {"EntryPointFunc"},
		/* CodeMacIntel32 {"EntryPointFunc"}, */
#endif

		/* [6] */
		AE_PiPL_Version {
			2,
			0
		},
		/* [7] */
		AE_Effect_Spec_Version {
			12,
			10
		},
		/* [8] */
		AE_Effect_Version {
			/* v1.00 */
			525824 
		},
		/* [9] */
		AE_Effect_Info_Flags {
			0
		},
		/* [10] */
		AE_Effect_Global_OutFlags {
			33555520
		},
		/* [11] */
		AE_Effect_Match_Name {
			/*プラグインの識別に使われる */
			"F's Toner"
		},
		/* [12] */
		AE_Reserved_Info {
			0
		}
	}
};
	
メニューのカテゴリとか表示名とプラグイン識別の名称はここで指定。

AE_Effect_Versionは、out_data->my_versionの数値でプラグインのバージョン。 AE_Effect_Global_OutFlagsもGlobalSetupで入力する数値。
計算は、AE_Utils.exe(これも自作アプリ)で行う。

VSのコマンドラインは以下の設定になっていて、PiPLTool.exeでコンパイルされる。
cl /D "MSWindows" /EP "..\$(InputName).r" > "$(IntDir)\$(InputName).rr""$(ProjectDir)..\..\..\Resources\PiPLTool.exe" "$(IntDir)\$(InputName).rr" "$(IntDir)\$(InputName).rrc"
cl /D "MSWindows" /EP "$(IntDir)\$(InputName).rrc" > "$(ProjectDir)$(InputName).rc"
	
PiPLTool.exeがうまく動かない場合は、msvcr71.dllを同じフォルダに入れれば動くかも?
SDK7のPiPLTool.exeは未確認だが、SDK6.5とXPsp2では入れないと動かなかった。
msvcr71.dll自体はProgram FilesのAdobeの中を探せばいくらでも見つかる。

メニューに表示されるプラグイン名とカテゴリ名は、テスト時はわかりやすいようにテスト版と明記してある。
過去にテスト時のバイナリが正式版のアーカイブの中に紛れ込んでいた失敗があったので。
正式版リリース時には修正しておく。

Step.13 カラーテーブルの作成

カラーテーブルの作成
これはコードを参照。

Step.14 ITERATE

画像のピクセルへのアクセスは、自前でアドレス計算を行ってするほうが好きだが、今回はITERATEコールバック関数を使って行っている。
定義は以下のとおり
iterate(
  PF_InData *in_data,   //読み込み画像情報の構造体
  long progress_base,   //プログレスバーの初期値。通常0
  long progress_final,  //プログレスバーの最終値。処理する横列の数
  PF_EffectWorld *src,  //読みにいく画像
  const PF_Rect *area,  //処理する範囲
  long refcon,          //下のポインタ関数に渡されるポインタ値。自由に使える。
  PF_Err (*pix_fn)(     //ピクセル毎に呼び出す関数ポインタ。
    long refcon,            //上のrefconの値。情報構造体のポインタとかに使う。
    long x,                 //対象ピクセルのX位置(PF_Fixedでは無く。整数値)
    long y,                 //対象ピクセルのY位置。
    PF_Pixel *in,           //inDataのピクセルのポインタ
    PF_Pixel *out),         //outDataのピクセルのポインタ
  PF_EffectWorld *dst   //書き込み画像
  );
実際は以下のマクロを使って呼び出している
#define PF_ITERATE(PROG_BASE, PROG_FINAL, SRC, RCT_P, REFCON, PIX_FUNC, DST)	\
	(*in_data->utils->iterate)(	\
		in_data, (PROG_BASE), (PROG_FINAL), (SRC), (RCT_P), (REFCON), (PIX_FUNC), (DST))
関数ポインタとrefconを使って処理を行うので、わからないとちんぷんかんぷん。
僕自身は、複数のアドレス計算が必要な場合は自前でやったほうが楽。 しかし、ドキュメントでは絶対に使えと書いてある。
確かに今回のようなプラグインを作る時は楽だし、プログレスバーの処理やマルチプロセッサの処理をしっかりするので これからはなるべく使うようにしたいと思っている。

なぜか16Bit用のiterate16がマクロ登録されていないので、自前で定義するかAE_EffectCB.hに書き加える必要がある。

作成手順 その3 デバッグ


Step.15 AfterEffectsでの動作確認

この時点でのソース
FsTonerTest01.zip

コンパイルはDebugではなくRelease用の設定で行う。Debug用は実行時にデバッガを立ち上げる時にしか使わない。
しかも、プラグイン作成時にはほとんどデバッガは使えない。同じルーチンの実行が何千回もあるから追いきれないからだ。 まぁ最後の手段って奴か。
あとDebug用のバイナリはAfterEffectsでエラーを起こすことがあるので注意。特にEntry Pointが狂って認識すらされないことがアル。

実行して見ると、エフェクトコントロールには以下のようにテスト用のパラメータがある。


よく見るとパラメータに変な文字「MY_」が、いきなりバグがあるなあ^^;
Blend with Originalが正しい気がするが、元にしたCC Tober.aexがBlend w. Originalなのであわせる事に。

エフェクトとしては簡単なものなので、特に不都合らしきものはないようだ。
最初、SequenceSetupでカラーテーブル用のメモリを確保していたが、結局Render内で描画ごとに行うことにした。 パラメータが同じでもカラーテーブルを作り直すのが無駄に感じられたが、処理時間には大差がなかったので良しとした。

実は、最初のテストではタイプミスによるアドレスエラーとか、カラーテーブル作成時のオーバーフローとか致命的なバグがあったが、その場で修正している。

Step.16 動作結果

一応簡単に動作確認
検証用のaepを作成して調べてみた。 処理時間はaep内の02-Test01コンポを10回再描画させて秒数の平均を出した。
処理時間はRender関数内のみの時間でプラグイン全体では違う。あくまで参考。
画質は以下のようなコンポを作成して目で見た判断。

●toner_test_aep.zip




結果は以下のとおり。
モード処理時間画質情報パレットでの最大値(16-bpc)
8Bit時0.025秒比較なし0 / 32768
16Bit-8BitTable0.32秒×115 / 32768
16Bit-12BitTable0.32秒13 / 32768
16Bit-14BitTable0.32秒5 / 32768
16Bit-16BitTable0.33秒0 / 32768
16Bit-Tableなし0.66秒比較元0 / 32768

処理時間はおおむね予想通り。テーブルを使わないと使ったのでは2倍の差がでてる。画質も予想通り。
速度はテーブルを小さくしても結局桁あわせの除算が入るので、それによって高速になることはないようだ。

情報パレットは、レベル補正をOFFの状態でマウスカーソルを適当に動かして一番大きなRGB値の探したもの。

16Bit-8BitTableの時は、レベル補正すると模様が見える。それ以外は情報パレットで確認すると所々に色が変わっていることがわかる。
16Bit-16BitTableと16Bit-Tableなしは、変化なし。

まぁ、レベルでの補正をかけなければ画面上では変化は見えない。 情報パレット数字が128以下の場合は、理論上ディスプレイでの変化はないからだ。

結果だけ考えれば、16Bit-16BitTableが一番高速で高画質なのだが、32768*64Byteで2048KByteつまり2MByteものメモリを 消費するのが怖かったからだ。

結果的には16BitTableでの方法にする事にした。 オプションでユーザーに選ばせる方法もあるが、説明が面倒な上勘違いすることもあるので、今回はしないことに。

あと、Blend w. Originalのために、AEのコールバック関数のblendを最初使っていたが、あまりにも遅いので自前の関数を作成した。
これは、他のプラグインでも使いそうなので別ファイルにしてある。

基本的にはAEのコールバック関数の方が早くていろいろ便利なものが多いのだが、blend関数はちょっと違ったみたい。

Step.17 完成

動作確認も終わり、バグもなくなったと判断できたら正式リリース版を作成する。
一応僕の場合、この状態で1週間位使って見てから判断する。

Step.18 完成・追記

いきなり仕事で使ったら、バグ発見^^;
16Bit時の輝度を求める計算式でRGBチャンネルを間違えていた。

あと、メモリがやっぱり勿体無いのでテーブル作成のオプションをつけた。

●FsToner100.zip

一応これで完成。

作成手順 その5 反省

終わった後

終わった後は、ソースをきれいにしたりとかする。
具体的には、デバッグ用に入れたTEST_MODEのマクロを削除するとか、余計なコメントを削除、または補足のコメントの追加など。
一番なのがマニュアル。プラグインは自分自身が使うために作ってるのだが、僕はよく忘れる^^;
今後バグが出たりとか、必要に応じたバージョンアップ時に楽にする為に結構必要。

まぁ、今書いてるこのHP自体がその為にあるから:-)

同じ効果のプラグインがなぜか複数あったり、バージョンアップしたくてもできないとか…
プラグインのアーカイブにマニュアルにのってないプラグインがあるのは発作的に作ったプラグインがほとんど。作ったことも忘れてるからなぁ。


-index-