After Effects エフェクトプラグインをゼロから作って見よう 1回目 "準備とスケルトンの作成"
Xで投稿してたけど限界を感じたのでこっちに書いてみます。
この記事ではエフェクトプラグインの元となるスケルトンの作成までの解説です。画像処理とかの解説とかエフェクトプラグインの作り方はまた別に書きます。
とりあえず、プラグインを作るための下準備についての解説です。
まず最初に準備
After Effects SDKをDLしましょう。方法は他で調べてね。
googleで"after effects sdk download"で検索すれば見つかると思います。AdobeにサインインしないとDLできませんので注意です。
後はVisual studio 2022をダウンロードです。Community2022なら無料です。
Visual studio Community 2022 とAfter Effects SDKがあればプラグインの開発が出来ます。
SDK_Noiseがスタート
自分用のエフェクトプラグインを作るために最初はSDKの中にあるSDK_Noiseからの改造が一番の早道です。SDKを適当なところにDLしたら、中にあるExamplesフォルダの中に自分専用のフォルダを作成します。
ここでは"NF_Plugins"としてます。
その中にEffectsフォルダにある"SDK_Noise"フォルダをコピーします。
次にNF_Plugins\SDK_Noise\Win\SDK_Noise.slnをクリックしてVisual studioを立ち上げます。
上記のダイアログが表示されますが、気にしないでOKです。
こんな感じに読み込まれたら、ソリューションエクスローラーでソリューションをクリック(上記の絵ではSDK_Noiseがクリックされてる状態なので注意)ファイルメニューでソリューションを別名保存してください。
場所は"NF_Plugins"フォルダの直下です。
ビルドしてみる
まずビルドする前に出力先の設定をします。
SDK_Noiseを右クリックでプロパティを表示
"構成プロパティ"タブの"全般"に出力ディレクトリの設定がありますので、そこを書き換えます。DebugとReleaseに2個ありますがここではDebugだけ
..\..\output_debug\
本来は環境変数AE_PLUGIN_BUILD_DIRを設定すればそこに出力されるって設定ですが、僕は相対パスで2つ上の階層に別のフォルダ作って書き出すようにしてます。これで準備はOK。
しかし、SDK_Noiseを右クリックしてビルドを実行すると以下のエラーが出るはずです。
これは警告レベルが高いので、警告をエラーとしてビルドを止める設定になっているだけなので、設定し直します。
また右クリックでプロパティ"C/C++"タブの警告レベルが"/W3"になっているので1段階下げて"/W2"にします。適応ボタンを押すのを忘れずに。ビルドすると今度は無事に成功するはずです。
この段階でビルド失敗する事はほとんどありません。ちゃんと適切な場所にファイル・ディレクトリが保存されていないだけなのでそこら辺を確認してください。
プロパティの設定
以上でビルドが旨くいったら、プロパティの見直しをします。
DebugだけでなくReleaseも同様に設定します。
“全般” : 出力先ディレクトリ 必要に応じてDebugとReleaseで変えておくと便利。
"全般" : C++言語標準 C++20に変えておくと便利。たまにgoogle検索で見つけたコードをコピペするとこれで引っかかってエラーになってはまることがあったので。
"C++"タブの"コード生成" で”構造体メンバーのアライメント"を16バイトに。OpenCVとか使うときに備えてです。
上記の設定をDebugとRelease両方にしておいてください。左上のプルダウンで変えられます。必ず適応ボタンを押すこと。
最近のSDKだとDebugとReleaseが両方用意されてるので楽です。後は必要に応じて設定しますが、とりあえずはこれで十分です。
あ、リソースファイルの一部をこの段階で書き換えておいた方がいいかな?
”SDK_NoisePiPL.r"をダブルクリックして表示させてCategoryの所にある"Sample Plug-ins"を"NF-Plugins"に書き換えておきましょう。間違えてEffectsフォルダにあるSDK_Noiseをビルドしてしまって混乱したことあるので念のためです。
以上の状態でビルドを行い、出来たバイナリーをAfterEffectsのプラグインフォルダへコピーして再起動させれば無事に認識されるはずです。
新規プロジェクトのやり方
今までSDK_Noiseのままだったのでその名称の変更法です。
今後新しいエフェクト(プロジェクト)を作る場合も同様になります。
面倒な手順なので細かく説明しますが、僕は専用のツール使ってやってます。それは後で。
フォルダの複製
SDK_Noiseフォルダをエクスプローラーで単純に複製します。名前はここでは"Skelton"にします。
ファイルのリネーム
"SDK_Noise"の文字を"Skelton"に手作業でリネームします。
その他の文字(PiPL等)はそのままです。
Winフォルダ内の物も忘れずにリネームです。
この段階で".vs"や"x64"等のフォルダがあったら消去しておいてください。
ファイルの中の文字を置換
今度はファイルの中の"SDK_Noise"を"Skelton"で置換します。このとき大文字小文字の区別なしでしておいた方が楽です。
ただのテキストファイルなのでメモ帳等テキストエディタで行った方が楽です。僕はTeraPadを使いました。
ソリューションへの追加
メニューからファイル:追加:既存のプロジェクトで上記のリネームを行った"Skelton.vcxproj"ファイルを選んで追加します。
ビルドを行い、無事に追加出来たことを確認します。
ビルド失敗の場合は置換のミスですのでそこを修正します。
大抵のエラーは「ファイルが見つからない」ので、Skelton.vcxprojとSkelton.vcxproj.filtersを見直してください。
専用アプリで
以上の手順でプロジェクトを複製して新規のエフェクト(プロジェクト)を作ります。
VSのテンプレートの機能を使えばもっと簡単に出来ますがディレクトリの構造が壊れてしまうので、僕はもっぱら上記の方法で行っています。
ただ、流石に大変なので専用のアプリを作ってそれで行っています。
https://github.com/bryful/F-s-PluginsProjects/tree/master/_tools
ここにある、CPP_ProjectRename.exeです。
コードの修正
上記でプラグインの元となるプロジェクトは出来ましたが、中身は相変わらず"SDK_Noise"のままなので自分専用に修正していきます。
リソースファイル(SkeltonPiPL.r)
PiPLリソースを記述するファイルで、AfterEffectsホストにプラグイン情報を認識させるためのものです。下記のコードは最低限の修正を終えたものです。Name/MatchName/Category等を変更しています。
AE_Effect_Global_OutFlags/AE_Effect_Global_OutFlags_2はプラグインの制御を行うフラグで、"AE_Effect.h"に詳細があります。
AE_Effect_Versionは、Skelton.hで定義してあるバージョンの値を計算して記述しないといけません。ちょっと面倒です。今回バージョンを1.0.0 としたのでこれを計算して数値を出さないといけません。
// ヘッダーファイル
#define MAJOR_VERSION 1
#define MINOR_VERSION 0
#define BUG_VERSION 0
#define STAGE_VERSION PF_Stage_DEVELOP
#define BUILD_VERSION 0
//AE_Effect.hにあるマクロ
#define PF_VERSION(vers, subvers, bugvers, stage, build) \
(A_u_long)( \
((((A_u_long)PF_Vers_VERS_HIGH(vers)) & PF_Vers_VERS_HIGH_BITS) << PF_Vers_VERS_HIGH_SHIFT) | \
((((A_u_long)(vers)) & PF_Vers_VERS_BITS) << PF_Vers_VERS_SHIFT) | \
((((A_u_long)(subvers)) & PF_Vers_SUBVERS_BITS)<<PF_Vers_SUBVERS_SHIFT) |\
((((A_u_long)(bugvers)) & PF_Vers_BUGFIX_BITS) << PF_Vers_BUGFIX_SHIFT) |\
((((A_u_long)(stage)) & PF_Vers_STAGE_BITS) << PF_Vers_STAGE_SHIFT) | \
((((A_u_long)(build)) & PF_Vers_BUILD_BITS) << PF_Vers_BUILD_SHIFT) \
)
リソースファイルを変更したときは必ず「リビルド」を行ってください。「ビルド」だけではリソースファイルは更新されません。
#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-Skelton"
},
/* [3] */
Category {
"NF-Plugins"
},
#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 {
524288
},
/* [9] */
AE_Effect_Info_Flags {
0
},
/* [10] */
AE_Effect_Global_OutFlags {
0x2000404
},
AE_Effect_Global_OutFlags_2 {
0x8001400
},
/* [11] */
AE_Effect_Match_Name {
"NF-Skelton"
},
/* [12] */
AE_Reserved_Info {
0
},
/* [13] */
AE_Effect_Support_URL {
"https://github.com/bryful"
}
}
};
ヘッダーアイル(Skelton.h)
プラグイン個別の情報を記述します。元のSDK_Noiseはプレミアへの対応するコードが入っていましたが全部削除してあります。基本的にデータの構造体等の指定が主ですが、バージョンや表示文字等もここに記述されています。
#pragma once
#ifndef Skelton_H
#define Skelton_H
#include "AEConfig.h"
#include "entry.h"
#include "AEFX_SuiteHelper.h"
#include "PrSDKAESupport.h"
#include "AE_Effect.h"
#include "AE_EffectCB.h"
#include "AE_EffectCBSuites.h"
#include "AE_Macros.h"
#include "AEGP_SuiteHandler.h"
#include "String_Utils.h"
#include "Param_Utils.h"
#include "Smart_Utils.h"
#ifdef AE_OS_WIN
#include <Windows.h>
#endif
#define DESCRIPTION "NF-Skelton by bry-ful"
#define NAME "NF-Skelton"
#define MAJOR_VERSION 1
#define MINOR_VERSION 0
#define BUG_VERSION 0
#define STAGE_VERSION PF_Stage_DEVELOP
#define BUILD_VERSION 0
enum {
NOISE_INPUT = 0,
NOISE_SLIDER, // default input layer
NOISE_NUM_PARAMS
};
enum {
SLIDER_DISK_ID = 1
};
#define FILTER_NOISE_MIN 0
#define FILTER_NOISE_MAX 1000
#define FILTER_NOISE_DFLT 10
#define SLIDER_MIN 0
#define SLIDER_MAX 100
#define RESTRICT_BOUNDS 0
#define SLIDER_PRECISION 1
#define DISPLAY_FLAGS PF_ValueDisplayFlag_PERCENT
extern "C" {
DllExport
PF_Err
EffectMain (
PF_Cmd cmd,
PF_InData *in_data,
PF_OutData *out_data,
PF_ParamDef *params[],
PF_LayerDef *output,
void *extra);
}
typedef struct NoiseInfo{
PF_FpLong valF;
} NoiseInfo, *NoiseInfoP, **NoiseInfoH;
typedef struct {
A_u_char blue, green, red, alpha;
} PF_Pixel_BGRA_8u;
typedef struct {
PF_FpShort blue, green, red, alpha;
} PF_Pixel_BGRA_32f;
#endif // Skelton_H
skelton.c プラグインの本体コード
Skelton.cがエフェクトプラグインの本体になります。
修正点はプレミア対応のコードをすべて削除しただけです。
#include "Skelton.h"
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",
NAME,
MAJOR_VERSION,
MINOR_VERSION,
DESCRIPTION);
return PF_Err_NONE;
}
static PF_Err
GlobalSetup (
PF_InData *in_dataP,
PF_OutData *out_data,
PF_ParamDef *params[],
PF_LayerDef *output )
{
PF_Err err = PF_Err_NONE;
out_data->my_version = PF_VERSION( MAJOR_VERSION,
MINOR_VERSION,
BUG_VERSION,
STAGE_VERSION,
BUILD_VERSION);
out_data->out_flags = PF_OutFlag_PIX_INDEPENDENT |
PF_OutFlag_DEEP_COLOR_AWARE |
PF_OutFlag_NON_PARAM_VARY;
out_data->out_flags2 = PF_OutFlag2_FLOAT_COLOR_AWARE |
PF_OutFlag2_SUPPORTS_SMART_RENDER |
PF_OutFlag2_SUPPORTS_THREADED_RENDERING;
return err;
}
static PF_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("Noise variation",
FILTER_NOISE_MIN,
FILTER_NOISE_MAX,
SLIDER_MIN,
SLIDER_MAX,
FILTER_NOISE_DFLT,
SLIDER_PRECISION,
DISPLAY_FLAGS,
0,
SLIDER_DISK_ID);
out_data->num_params = NOISE_NUM_PARAMS;
return err;
}
static PF_Err
FilterImage8 (
void *refcon,
A_long xL,
A_long yL,
PF_Pixel8 *inP,
PF_Pixel8 *outP)
{
PF_Err err = PF_Err_NONE;
NoiseInfo * niP = reinterpret_cast<NoiseInfo*>(refcon);
PF_FpLong tempF = 0;
if (niP){
tempF = rand() % PF_MAX_CHAN8;
tempF *= (niP->valF / SLIDER_MAX);
outP->alpha = inP->alpha;
outP->red = MIN(PF_MAX_CHAN8, inP->red + (A_u_char) tempF);
outP->green = MIN(PF_MAX_CHAN8, inP->green + (A_u_char) tempF);
outP->blue = MIN(PF_MAX_CHAN8, inP->blue + (A_u_char) tempF);
}
return err;
}
static PF_Err
FilterImage16 (
void *refcon,
A_long xL,
A_long yL,
PF_Pixel16 *inP,
PF_Pixel16 *outP)
{
PF_Err err = PF_Err_NONE;
NoiseInfo * niP = reinterpret_cast<NoiseInfo*>(refcon);
PF_FpLong tempF = 0;
if (niP){
tempF = rand() % PF_MAX_CHAN16;
tempF *= (niP->valF / SLIDER_MAX);
outP->alpha = inP->alpha;
outP->red = MIN(PF_MAX_CHAN16, inP->red + (A_u_short) tempF);
outP->green = MIN(PF_MAX_CHAN16, inP->green + (A_u_short) tempF);
outP->blue = MIN(PF_MAX_CHAN16, inP->blue + (A_u_short) tempF);
}
return err;
}
static PF_Err
FilterImage32 (
void *refcon,
A_long xL,
A_long yL,
PF_PixelFloat *inP,
PF_PixelFloat *outP)
{
PF_Err err = PF_Err_NONE;
NoiseInfo * niP = reinterpret_cast<NoiseInfo*>(refcon);
PF_FpShort tempF = 0;
if (niP){
tempF = (PF_FpShort)(rand() % PF_MAX_CHAN16);
tempF *= (PF_FpShort)(niP->valF / (SLIDER_MAX * PF_MAX_CHAN16));
outP->alpha = inP->alpha;
outP->red = (inP->red + tempF);
outP->green = (inP->green + tempF);
outP->blue = (inP->blue + tempF);
}
return err;
}
static PF_Err
Render (
PF_InData *in_dataP,
PF_OutData *out_data,
PF_ParamDef *params[],
PF_LayerDef *output )
{
PF_Err err = PF_Err_NONE;
NoiseInfo niP;
A_long linesL = 0;
AEFX_CLR_STRUCT(niP);
linesL = output->extent_hint.bottom - output->extent_hint.top;
niP.valF = params[NOISE_SLIDER]->u.fs_d.value;
if(params[NOISE_SLIDER]->u.fs_d.value != 0.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
¶ms[NOISE_INPUT]->u.ld, // src
NULL, // area - null for all pixels
(void*)&niP, // refcon - your custom data pointer
FilterImage8, // 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)
¶ms[NOISE_INPUT]->u.ld, // Source
output, // Dest
NULL, // Source rect - null for all pixels
NULL); // Dest rect - null for all pixels
}
return err;
}
static PF_Err
PreRender(
PF_InData *in_dataP,
PF_OutData *out_dataP,
PF_PreRenderExtra *extraP)
{
PF_Err err = PF_Err_NONE;
PF_ParamDef noise_param;
PF_RenderRequest req = extraP->input->output_request;
PF_CheckoutResult in_result;
AEFX_CLR_STRUCT(noise_param);
AEFX_SuiteScoper<PF_HandleSuite1> handleSuite = AEFX_SuiteScoper<PF_HandleSuite1>( in_dataP,
kPFHandleSuite,
kPFHandleSuiteVersion1,
out_dataP);
PF_Handle infoH = handleSuite->host_new_handle(sizeof(NoiseInfo));
if (infoH){
NoiseInfo *infoP = reinterpret_cast<NoiseInfo*>(handleSuite->host_lock_handle(infoH));
if (infoP){
extraP->output->pre_render_data = infoH;
ERR(PF_CHECKOUT_PARAM( in_dataP,
NOISE_SLIDER,
in_dataP->current_time,
in_dataP->time_step,
in_dataP->time_scale,
&noise_param));
if (!err){
infoP->valF = noise_param.u.fs_d.value;
}
ERR(extraP->cb->checkout_layer( in_dataP->effect_ref,
NOISE_INPUT,
NOISE_INPUT,
&req,
in_dataP->current_time,
in_dataP->time_step,
in_dataP->time_scale,
&in_result));
UnionLRect(&in_result.result_rect, &extraP->output->result_rect);
UnionLRect(&in_result.max_result_rect, &extraP->output->max_result_rect);
handleSuite->host_unlock_handle(infoH);
}
} else {
err = PF_Err_OUT_OF_MEMORY;
}
return err;
}
static PF_Err
SmartRender(
PF_InData *in_data,
PF_OutData *out_data,
PF_SmartRenderExtra *extraP)
{
PF_Err err = PF_Err_NONE,
err2 = PF_Err_NONE;
PF_EffectWorld *input_worldP = NULL,
*output_worldP = NULL;
AEFX_SuiteScoper<PF_HandleSuite1> handleSuite = AEFX_SuiteScoper<PF_HandleSuite1>( in_data,
kPFHandleSuite,
kPFHandleSuiteVersion1,
out_data);
NoiseInfo *infoP = reinterpret_cast<NoiseInfo*>(handleSuite->host_lock_handle(reinterpret_cast<PF_Handle>(extraP->input->pre_render_data)));
if (infoP){
ERR((extraP->cb->checkout_layer_pixels( in_data->effect_ref, NOISE_INPUT, &input_worldP)));
ERR(extraP->cb->checkout_output(in_data->effect_ref, &output_worldP));
PF_PixelFormat format = PF_PixelFormat_INVALID;
AEFX_SuiteScoper<PF_WorldSuite2> wsP = AEFX_SuiteScoper<PF_WorldSuite2>(in_data,
kPFWorldSuite,
kPFWorldSuiteVersion2,
out_data);
if (infoP->valF == 0.0) {
err = PF_COPY(input_worldP, output_worldP, NULL, NULL);
} else {
ERR(wsP->PF_GetPixelFormat(input_worldP, &format));
if (!err){
AEFX_SuiteScoper<PF_iterateFloatSuite2> iterateFloatSuite =
AEFX_SuiteScoper<PF_iterateFloatSuite2>(in_data,
kPFIterateFloatSuite,
kPFIterateFloatSuiteVersion2,
out_data);
AEFX_SuiteScoper<PF_iterate16Suite2> iterate16Suite =
AEFX_SuiteScoper<PF_iterate16Suite2>( in_data,
kPFIterate16Suite,
kPFIterate16SuiteVersion2,
out_data);
AEFX_SuiteScoper<PF_Iterate8Suite2> iterate8Suite =
AEFX_SuiteScoper<PF_Iterate8Suite2>( in_data,
kPFIterate8Suite,
kPFIterate8SuiteVersion2,
out_data);
switch (format) {
case PF_PixelFormat_ARGB128:
iterateFloatSuite->iterate( in_data,
0,
output_worldP->height,
input_worldP,
NULL,
(void*)infoP,
FilterImage32,
output_worldP);
break;
case PF_PixelFormat_ARGB64:
iterate16Suite->iterate(in_data,
0,
output_worldP->height,
input_worldP,
NULL,
(void*)infoP,
FilterImage16,
output_worldP);
break;
case PF_PixelFormat_ARGB32:
iterate8Suite->iterate( in_data,
0,
output_worldP->height,
input_worldP,
NULL,
(void*)infoP,
FilterImage8,
output_worldP);
break;
default:
err = PF_Err_BAD_CALLBACK_PARAM;
break;
}
}
}
ERR2(extraP->cb->checkin_layer_pixels(in_data->effect_ref, NOISE_INPUT));
}
return err;
}
extern "C" DllExport
PF_Err PluginDataEntryFunction2(
PF_PluginDataPtr inPtr,
PF_PluginDataCB2 inPluginDataCallBackPtr,
SPBasicSuite* inSPBasicSuitePtr,
const char* inHostName,
const char* inHostVersion)
{
PF_Err result = PF_Err_INVALID_CALLBACK;
result = PF_REGISTER_EFFECT_EXT2(
inPtr,
inPluginDataCallBackPtr,
"NF-Skelton", // Name
"NF-Skelton", // Match Name
"NF-Plugins", // Category
AE_RESERVED_INFO, // Reserved Info
"EffectMain", // Entry point
"https://github.com/bryful"); // support URL
return result;
}
PF_Err
EffectMain(
PF_Cmd cmd,
PF_InData *in_dataP,
PF_OutData *out_data,
PF_ParamDef *params[],
PF_LayerDef *output,
void *extra)
{
PF_Err err = PF_Err_NONE;
try {
switch (cmd)
{
case PF_Cmd_ABOUT:
err = About(in_dataP,out_data,params,output);
break;
case PF_Cmd_GLOBAL_SETUP:
err = GlobalSetup(in_dataP,out_data,params,output);
break;
case PF_Cmd_PARAMS_SETUP:
err = ParamsSetup(in_dataP,out_data,params,output);
break;
case PF_Cmd_RENDER:
err = Render(in_dataP,out_data,params,output);
break;
case PF_Cmd_SMART_PRE_RENDER:
err = PreRender(in_dataP, out_data, (PF_PreRenderExtra*)extra);
break;
case PF_Cmd_SMART_RENDER:
err = SmartRender(in_dataP, out_data, (PF_SmartRenderExtra*)extra);
break;
}
} catch(PF_Err &thrown_err) {
// Never EVER throw exceptions into AE.
err = thrown_err;
}
return err;
}
Skeltonの改造
上記の修正で最低限の機能だけを持つSkeltonが出来ました。
もうSDK_Noiceは必要ないので、ソリューションから削除してしまいましょう。
SDKにはTemplateフォルダ内に同様な"Skeleton"(綴りが違うのがポイント)がありますが、ちょっと使いにくいので、SDK_Noise改造の方が僕は推奨です。
この後自分の作りたいエフェクトをこのスケルトンを元にして作って行きます。まだこのスケルトンではスケルトンとしては不十分ですが、この記事ではここまでです。
最後に
今回作成した物はgithbに登録してあります。
ここにあるSkeltonは逐次更新してしまうのでこの記事の物は別に"Skelton_step1"としてプロジェクト内に入れてあります。
この記事が気に入ったらサポートをしてみませんか?