見出し画像

After Effects エフェクトプラグインをゼロから作って見よう 3回目 "スケルトンを元に新しい物を作る"

今回は簡単なEffectsプラグインを作って見ましょう。
その作業をしながら、元にしたスケルトンの改良した方がいい項目も探していきます。


何を作るか仕様を考える

作り始める前にどんなエフェクトを作るか考えます。
今回はShiftParと言うエフェクトを作ってみます。
SDKにある"Shifter"や標準プラグインの"オフセット"のように画面を上下左右にずらす奴を想定しています。違いはずらす量を横幅・縦幅のパーセント指定できるようにします。

映写機での上映時にフィルムがガタガタと上下に揺らしたり、モニタの画面切り替え時のノイズと一緒に使う事を考えています。

とりあえず8bitモードのみです。移動はピクセル単位で小数には対応しない。

スケルトンから新しいプロジェクトを作る。

エクスプローラーでSkeltonフォルダを複製します。

前回は面倒な手順で処理しましたが面倒なのでアプリを使います。
CPP_ProjectRename.exeです。

サクッとリネームしてNF_Pluginsのソリューションに追加します。

PiPL関係の修正・outflagsの設定

リネームがうまくいっていれば"ShiftPerPiPL.r"の修正はほぼないです。ただ先にバージョンとoutflagsを決めないといけません。
該当箇所は"ShiftPer.c"のGlobalSetupにありますが、その他にも使い回すので、別のファイルにまとめてインクルードさせるようにします。
NF-Target.hを作成します。内容は以下の通りです。outflagとかは毎回決まりきった値なのでここに書き込んでおきます。本来はマクロでコンパイル時に計算させていますが、所々の問題で数値を直接書き込んでいます。
計算は自作アプリで対処しています。

// NF-Target.h
#pragma once
#ifndef NF_TARGET_H
#define NF_TARGET_H
//-----------------------------------------------------------------------------------
//プラグインの識別に使われる名前
#define NF_NAME			"NF-ShiftPer"
#define NF_MATCHPNAME	"NF-ShiftPer"

//-----------------------------------------------------------------------------------
//プラグインの説明に使われる文字
#define NF_DESCRIPTION	"Shift the screen by a specified percentage of the width."

//-----------------------------------------------------------------------------------
//プラグインが表示されるメニュー名
#define NF_CATEGORY "NF-Plugins{Beta}"

//-----------------------------------------------------------------------------------
// バージョン
/*
	PF_VERSION(
	MAJOR_VERSION,
	MINOR_VERSION,
	BUG_VERSION,
	STAGE_VERSION,
	BUILD_VERSION);
*/
#define NF_VERSION 524288

//-----------------------------------------------------------------------------------
//out_flags
/*
  out_data->out_flags
	PF_OutFlag_PIX_INDEPENDENT		1024
	PF_OutFlag_NON_PARAM_VARY		4
	PF_OutFlag_DEEP_COLOR_AWARE		33554432
	PF_OutFlag_USE_OUTPUT_EXTENT	64
	PF_OutFlag_I_EXPAND_BUFFER		512
	PF_OutFlag_I_DO_DIALOG			32
*/

//#define NF_OUT_FLAGS	33556032	//通常はこちら
//#define NF_OUT_FLAGS	33556036	//こっちを有効にすると毎フレームごとに描画する。NON_PARAM_VARYを動作中に切り替えるときもこちらに
#define NF_OUT_FLAGS	1600		//8bitのみ

//-----------------------------------------------------------------------------------
//out_flags2
/*
	out_data->out_flags2
	PF_OutFlag2_FLOAT_COLOR_AWARE
	PF_OutFlag2_PARAM_GROUP_START_COLLAPSED_FLAG
	PF_OutFlag2_SUPPORTS_SMART_RENDER
	PF_OutFlag2_SUPPORTS_QUERY_DYNAMIC_FLAGS
	PF_OutFlag2_DOESNT_NEED_EMPTY_PIXELS;
*/
#define NF_OUT_FLAGS2 134222921

//-----------------------------------------------------------------------------------
#endif

ShiftPerPiPL.rを以下のように"NF_Target.h"をインクルードして、該当項目を修正します。

#include "NF_Target.h"

#include "AEConfig.h"
#include "AE_EffectVers.h"

#ifndef AE_OS_WIN
	#include "AE_General.r"
#endif


resource 'PiPL' (16000) {
	{	/* array properties: 12 elements */
		/* [1] */
		Kind {
			AEEffect
		},
		/* [2] */
		Name {
			NF_NAME
		},
		/* [3] */
		Category {
			NF_CATEGORY
		},
#ifdef AE_OS_WIN
	#ifdef AE_PROC_INTELx64
		CodeWin64X86 {"EffectMain"},
	#endif	
#else
	#ifdef AE_OS_MAC
		CodeMacIntel64 {"EffectMain"},
		CodeMacARM64 {"EffectMain"},
	#endif
#endif
		/* [6] */
		AE_PiPL_Version {
			2,
			0
		},
		/* [7] */
		AE_Effect_Spec_Version {
			PF_PLUG_IN_VERSION,
			PF_PLUG_IN_SUBVERS
		},
		/* [8] */
		AE_Effect_Version {	
			NF_VERSION
		},
		/* [9] */
		AE_Effect_Info_Flags {
			0
		},
		/* [10] */
		AE_Effect_Global_OutFlags {
			NF_OUT_FLAGS
		},
		AE_Effect_Global_OutFlags_2 {
			NF_OUT_FLAGS2
		},
		/* [11] */
		AE_Effect_Match_Name {
			NF_MATCHNAME
		},
		/* [12] */
		AE_Reserved_Info {
			0
		},
		/* [13] */
		AE_Effect_Support_URL {
			"https://github.com/bryful"
		}
	}
};

"ShiftPer.h"にも"NF_Target.h"をして、"ShiftPer.c"の該当箇所も修正します。

static PF_Err 
About (	
	PF_InData		*in_data,
	PF_OutData		*out_data,
	PF_ParamDef		*params[],
	PF_LayerDef		*output )
{
	PF_SPRINTF(	out_data->return_msg, 
				"%s, v%d.%d\r%s",
				NF_NAME, 
				MAJOR_VERSION, 
				MINOR_VERSION, 
				NF_DESCRIPTION);

	return PF_Err_NONE;
}

この段階でビルドを行い、AfterEffectsに正常に登録できるか確認します。

パラメータの登録

追加するパラメータは二つ。
余りにも変更点が多いのでこの段階でのファイルをリンク添付します

修正点は

  1. 描画部分をすべて削除

  2. enumでパラメータのインデックス番号を定義(ShiftPer.h)

  3. typedefでパラメータを保存する構造体を定義(ShiftPer.h)

  4. ParamsSetup関数でパラメータを登録するコードを記述

  5. Render関数でパラメータ獲得のコードを記述。描画はしない(コピー)

  6. PreRender関数で、パラメータ獲得のコードとSmartFXの準備

  7. Render関数で描画(コピーだけ)とSmartFXの後始末

パラメータのインデックス

sdkではわざわざenumを2個用意してインデックスを2個用意していますが、1個にまとめてあります。

パラメータの登録

PF_ADD_FLOAT_SLIDERXマクロでfload型数値のパラメータを追加しています。重要なのはPF_ParamDef型の変数"def"で、マクロ内部で使われています。AEFX_CLR_STRUCTマクロは構造体を初期化するものです。"err"変数もERRマクロで使用されています。

ParamsSetup (
	PF_InData		*in_data,
	PF_OutData		*out_data,
	PF_ParamDef		*params[],
	PF_LayerDef		*output)
{
	PF_Err			err = PF_Err_NONE;
	PF_ParamDef		def;
	
	AEFX_CLR_STRUCT(def);
	
	PF_ADD_FLOAT_SLIDERX("ShiftX(%)", 
						-30000,
						30000,
						-200,
						200,
						0,
						1,
						PF_ValueDisplayFlag_PERCENT,
						0,
						SFTP_X);

PF_ADD_FLOAT_SLIDERXマクロについては、VSで選択して右クリックして「定義をここに表示」で詳しく見ることが出来ます。
マクロをさかのぼるとPF_InDataPF_ADD_PARAMを呼び出しています。引数がPF_ParamDefだけなのでわかりやすくマクロ定義してあります(嘘です。分かりません!)下のリンクに一応解説はされています。

いちおうPF_ADD_FLOAT_SLIDERXの引数を説明すると

  1. 表示される文字列

  2. このパラメータが扱える最小値

  3. このパラメータが扱える最大値

  4. UIのスライダーの最小値

  5. UIのスライダーの最大値

  6. デフォルトの値

  7. 小数の桁数

  8. ディスプレイタイプ

  9. 各種フラグ

  10. パラメータのインデックス番号

となってますが、ヘルプから解読するのがもはや不可能なのでSDKのコードからそのままコピペが一番です。

ParamsSetup関数の最後でパラメータの数をPF_OutDataのnum_paramsへ入れます。これを忘れたり間違った数を入れるとかなり重度のエラーが出ます。

out_data->num_params = SFTP_NUM_PARAMS;

ここまで終わったら、ビルドを行いAfterEffectsに無事に登録されるか確認します。エラーやおかしな挙動があったらいろいろ確認します。

エフェクトのコード

今回はインテレータという機能を使いエフェクトを実装します。

AEFX_SuiteScoper<PF_Iterate8Suite2> iterate8Suite = 
	AEFX_SuiteScoper<PF_Iterate8Suite2>(in_dataP,
										kPFIterate8Suite,
										kPFIterate8SuiteVersion2,
										out_data);

iterate8Suite->iterate(	in_dataP,
						0,								// progress base
						linesL,							// progress final
						&params[NOISE_INPUT]->u.ld,		// src 
						NULL,							// area - null for all pixels
						(void*)&niP,					// refcon - your custom data pointer
						FilterImage8,					// pixel function pointer
						output);	
ilterImage8 (
	void		*refcon, 
	A_long		xL, 
	A_long		yL, 
	PF_Pixel8	*inP, 
	PF_Pixel8	*outP)
{
	PF_Err			err = PF_Err_NONE;
	
	SFTPInfo *	niP		= reinterpret_cast<SFTPInfo*>(refcon);
	PF_FpLong	tempF		= 0;
					
以下略

interate関数にポインタで関数を渡すと、面倒なアドレス計算なしでピクセルの処理が出来ます。まぁ今回はエフェクトの都合上アドレスの計算はしないと駄目ですが。

というわけで完成したrender関数。
Suiteって言うコールバック関数があってそれを呼び出して制御しますが、まぁお決まりって事でほとんどコピペで大丈夫。

static PF_Err 
Render ( 
	PF_InData		*in_dataP,
	PF_OutData		*out_data,
	PF_ParamDef		*params[],
	PF_LayerDef		*output )
{
	PF_Err				err		= PF_Err_NONE;

	SFTPInfo			info;
	A_long				linesL	= 0;
	
	AEFX_CLR_STRUCT(info);
	
	linesL 		= output->extent_hint.bottom - output->extent_hint.top;
	info.shiftX = params[SFTP_X]->u.fs_d.value/100;
	info.shiftY = params[SFTP_Y]->u.fs_d.value/100;


	PF_EffectWorld *input = &params[SFTP_INPUT]->u.ld;
	//サイズを獲得
	info.widthIn = input->width;
	info.heightIn = input->height;
	info.widthTrueIn = input->rowbytes/sizeof(PF_Pixel);
	info.widthOut = output->width;
	info.heightOut = output->height;
	info.widthTrueOut = output->rowbytes / sizeof(PF_Pixel);

	info.shiftXPixel = (A_long)((PF_FpLong)info.widthIn * info.shiftX + 0.5);
	info.shiftYPixel = (A_long)((PF_FpLong)info.heightIn * info.shiftY + 0.5);
	info.data = input->data;

	if ((info.shiftXPixel != 0)|| (info.shiftYPixel != 0)) {

		AEFX_SuiteScoper<PF_Iterate8Suite2> iterate8Suite =
			AEFX_SuiteScoper<PF_Iterate8Suite2>(in_dataP,
				kPFIterate8Suite,
				kPFIterate8SuiteVersion2,
				out_data);

		iterate8Suite->iterate(
			in_dataP,
			0,								// progress base
			linesL,							// progress final
			input,	// area - null for all pixels
			NULL,
			(void*)&info,					// refcon - your custom data pointer
			ShiftPixel8,					// pixel function pointer
			output);						// dest
	}
	else {

		AEFX_SuiteScoper<PF_WorldTransformSuite1> worldTransformSuite =
			AEFX_SuiteScoper<PF_WorldTransformSuite1>(in_dataP,
				kPFWorldTransformSuite,
				kPFWorldTransformSuiteVersion1,
				out_data);

		worldTransformSuite->copy(in_dataP->effect_ref,			// This effect ref (unique id)
			&params[SFTP_INPUT]->u.ld,		// Source
			output,							// Dest
			NULL,							// Source rect - null for all pixels
			NULL);							// Dest rect - null for all pixels
	}
	return err;
}

PF_ParamDef型配列からパラメータを獲得、100%なので100で割ってます

info.shiftX = params[SFTP_X]->u.fs_d.value/100;

入力画像はインデックス0番から獲得。PF_EffectWorldが画像構造体、
出力画像は引数のoutputとなる。PF_LayerDefとPF_EffectWorldは同じ型です。

PF_EffectWorld *input = &params[SFTP_INPUT]->u.ld;

PF_EffectWorldのメンバーから必要な情報を集めているが、ここで注意点

  • 画像の横幅とデータ上での横幅が違う!

  • 入力・出力画像は同じサイズとは限らない。

横幅が例えば300pxだったとしてもデータ上では304pxの時がある。と言うかほとんどの場合がそう。昔は8の倍数に切り上げってパターンが多かったが最近はまちまち。実際のサイズはrowbytesメンバーになってるので1ピクセル分のサイズで割ってやれば、実サイズが分かる。

info.widthTrueIn = input->rowbytes/sizeof(PF_Pixel);

なのでこんなに多くの情報を集める必要がある。
パラメータは幅のパーセントなのでこの段階で実際にずらすピクセルも計算して出しておく。

info.shiftXPixel = (A_long)((PF_FpLong)info.widthIn * info.shiftX + 0.5);
info.shiftYPixel = (A_long)((PF_FpLong)info.heightIn * info.shiftY + 0.5);

最後に入力画面のバイト配列のポインターを確保しておく

info.data = input->data;

この後はinterate関数に渡せば描画が始まる。

static PF_Err
ShiftPixel8(
	void* refcon,
	A_long		xL,
	A_long		yL,
	PF_Pixel8* inP,
	PF_Pixel8* outP)
{
	PF_Err			err = PF_Err_NONE;

	SFTPInfo* infoP = reinterpret_cast<SFTPInfo*>(refcon);

	A_long nx = (xL - infoP->shiftXPixel) % infoP->widthIn;
	if (nx < 0) nx += infoP->widthIn;
	A_long ny = (yL - infoP->shiftYPixel) % infoP->heightIn;
	if (ny < 0) ny += infoP->heightIn;
	PF_Pixel* data = (PF_Pixel*)infoP->data;
	*outP = data[infoP->widthTrueOut * ny + nx];


	return err;
}

実際の描画コードはこれだけ。
引数は


  • refcon interateを呼び出すときに入れたパラメータ構造体。void*なのでちょっと複雑なキャストになる。

  • xL,yL 呼び出された時のXY座標、A_long型

  • 入力画像のポインター。ポインターなので *inP と米印つけると値が取り出せる。"inP->red"って感じにメンバの直接アクセスしてもOK

  • 出力画像のポインター


efconをキャストしてパラメータ構造体にして、
現在の位置XYにシフトした位置を計算している。幅の余剰を求めてマイナスの値だったら幅を足して画面ないの位置nx,nyを求める。
簡単なアドレス計算してそのピクセルの数値を獲得、現在の位置に書き込んでます。凄い簡単。

最後に

簡単なエフェクトを作ったが、まだこれで完成ではない。16bit/32bitの部分、SmartFXへの対応が残ってる。まぁ8bit部分が出来たらその対応はそんなに苦ではない。

何よりもパラメータを登録、そしてその確保が異様に面倒なのだ。

次回はスケルトンを見直してそこら辺を楽にする方法を実装して見ます。


この記事が気に入ったらサポートをしてみませんか?