devlog

http://twitter.com/yusukei

AfterEffectsにPythonを組み込む

初めに

たまには外に情報を発信しないとね!ということで、AfterEffectsにPythonを組み込むという企画を行ってみようかと思います。

用意するもの

今回はAfterEffectsのスクリプトからPython 2.6を呼び出しその結果を返すという実装を行ってみます。 はじめにAfterEffectsのスクリプトからC++のコードを呼び出す部分です。 これに関してはJavaScript Tools GuideのIntegrating External Librariesの章に書いてあります。

Pythonの初期化

Pythonを初期化する際にPythonのある位置をPythonに教えます。 初期化後、そのままではbuiltinモジュールが使えないため、mainモジュールをインポートし、それをglobalsとします。

        // Pythonの初期化
        Py_SetPythonHome("C:/Python26");
        Py_Initialize();
        printf("Py_Initialize\n");

        // globals空間の生成
        PyObject* main = PyImport_AddModule("__main__");
        gGlobals = PyModule_GetDict(main);

ExtendScriptにクラスを登録する

ExtendScript側からC++を呼び出せるようにクラスを登録します。

// ExtendScript側のPyAEクラス
SoObjectInterface objectInterface =
{
    PySample::constructor, // initialize
    NULL,                  // put
    NULL,                  // get
    PySample::call,        // call
    NULL,                  // valueOf
    NULL,                  // toString
    PySample::destructor   // finalize
};

これは登録するクラスのインターフェイスです。 コンストラクタ、アトリビュートの取得設定、メソッドの呼び出し、デストラクタなどの際に呼ばれる関数を定義します。

        // ExtendScriptにPyAEクラスを登録する
        ret = gpServer->addClass(hServer, "PyAE", &objectInterface) ;
        if (ret!=kESErrOK)
            printf("Failed register pyAE Class.\n");

これをExtendScript側に渡すことで、ExtendScript側から

var pyae = New PyAE();

とすることでコンストラクタが呼ばれるようになります。

次にコンストラクタですが、

// PyAEクラスのコンストラクタ
ESerror_t PySample::constructor(SoHObject hObject,int argc, TaggedData* argv)
{
    gpServer->addMethod(hObject, "eval", 0, NULL);
    return kESErrOK;
}

と先ほどインターフェイスで定義した関数が呼び出され、その中でメソッドを登録します。

こうすることで、

pyae.eval("print 'aaa'");

のようにメソッドが呼び出しできるようになります。

Pythonでevalする

PyAEクラスのevalで呼び出されると、PyAE::callが呼び出されます。 eval以外のメソッドが呼び出された場合も一度すべてここが呼び出されます。 そのため、必要であれば引数nameに入っている値で処理を分けます。

// PyAEクラスのメソッドをcallすると呼ばれる
ESerror_t PyAE::call(SoHObject hObject, SoCClientName* name,
                     int argc, TaggedData* argv, TaggedData* pResult)
{
    // name->name_sigにcallされたメソッド名が入っている
    if (strcmp(name->name_sig, "eval")==0)
        return PyAE::pyEval(hObject, name, argc, argv, pResult);
    
    return kESErrNotImplemented;
}

ExtendScriptで呼び出された際の引数とその数はargv, argcに入っているので、そこから実行するPythonスクリプトの文字列を取得します。 retがNULLの際は何かしらエラーが発生しているので、トレースバック文字列を表示して終了します。

// Pythonのevalを行う
ESerror_t PyAE::pyEval(SoHObject hObject, SoCClientName* name,
                       int argc, TaggedData* argv, TaggedData* pResult)
{
    if (argc!=1)
        return kESErrBadArgumentList;
    
    int start_symbol = Py_eval_input;
    
    // 引数に渡された文字列をPythonでevalする
    PyObject* ret = PyRun_String(argv[0].data.string, start_symbol, gGlobals, gGlobals);
    if (ret==NULL) {
        PyErr_Print();
        PyErr_Clear();
        return kESErrException;
    }
    
    py2as(ret, pResult);

    return kESErrOK;
}

Pythonで評価した値とAfterEffectのScriptの値を変換する

Pythonで評価した結果の値をAfterEffectのScriptへ返すため、変換を行います。 今回は文字列、整数、小数の3種類のみに対応しています。

// PyObject -> TaggedDataへ変換する
void py2as(PyObject* pyObj, TaggedData* aeObj)
{
    if (PyInt_Check(pyObj)||PyLong_Check(pyObj))
    {
        aeObj->data.intval = PyInt_AsLong(pyObj);
        aeObj->type = kTypeInteger;
    }
    else if (PyFloat_Check(pyObj))
    {
        aeObj->data.fltval = PyFloat_AsDouble(pyObj);
        aeObj->type = kTypeDouble;
    }
    else if (PyString_Check(pyObj))
    {
        aeObj->data.string = strdup(PyString_AsString(pyObj));
        aeObj->type = kTypeString;
    }    
}

おわりに

さくっとAfterEffectsにPythonを組み込んでみました。 実際に使うには色々と問題が残っているのですが、基本的な組み込み方は分かっていただけたのではないかと思います。

次回があるとすれば、このままでは標準出力がどこにも出ないので、そのあたりの話を書こうかと思います。

pyae.cpp

gist