🍌

TouchDesignerのC++オペレーターとPythonの連携

2021/12/04に公開

TouchDesignerのC++オペレーター

TouchDesignerでは、C++を使ってカスタムのライブラリを作り、独自の機能をもったオペレーターを作ることができます。今回の記事ではC++のオペレーターを作る基礎の部分の解説はしきれないのですが、↓ の記事などを参考にしてください。

https://docs.derivative.ca/Custom_Operators
https://note.com/toyoshimorioka/n/nce76daf98907
http://satoruhiga.com/post/extending-touchdesigner/

この記事中で紹介したコードのサンプルプロジェクトはこちらです
https://drive.google.com/file/d/1x4garLy7CiFHvM8AckT0KYAmmjxmUmAE/view?usp=sharing

C++オペレーターの問題点

C++でオペレーターを作ると、TouchDesignerのパラメーターインターフェイスやノードのインプット、アウトプットを使った入出力が行えるようになります。
大体のケースにおいてはパラメーターやノードの接続さえできれば問題ないことが多いのですが、

  • C++オペレーターからのイベント呼び出しをしたい時
  • TouchDesigner側のPythonからオペレーターの機能を呼び出したりしたい時
  • 1つの処理でCHOP的なデータ、DAT的なデータなど、オペレータータイプをまたぐような処理をしたい時

などにパラメーターやインアウト接続だけだとスマートに処理をさばくのが難しい時があります。

この記事では、C++オペレーターにPythonから扱える機能を追加する方法を紹介したいと思います。

キーポイント

オペレーターをPythonで拡張するキーになるのが、CPlusPlus_Common.h で定義されている OP_ParameterManager::appendPython()OP_Inputs::getParPython() です。


これは、C++オペレーターのパラメーターとしてPythonオブジェクトをうけとれるようにするというもので、これを使ってC++オペレーターとTouchDesignerのPythonをブリッジしていきます。

たとえば setupParameters をオーバーライドした関数の中で appendPython をすると、

void setupParameters(OP_ParameterManager* manager, void* reserved1) override
{
	{
		OP_StringParameter sp;
		sp.label = "Python Parameter";
		sp.name = "Pythonparam";
		sp.page = "Test PyObject";
		manager->appendPython(sp);
	}
}

TouchDesignerのパラメーターウィンドウにパラメーターが追加されます。

追加したパラメーターは、C++オペレーター側では getOutputInfoexecute など、OP_Inputs 型の引数が渡されているメソッドから inputs->getParPython() を使うことでアクセスできます。

bool getOutputInfo(CHOP_OutputInfo* info, const OP_Inputs *inputs, void *reserved1) override
{
	info->numSamples = 600;
	info->numChannels = 1;

	PyObject* pyobj = inputs->getParPython("Pythonparam");
	if (pyobj)
	{
		// PyObjectを使ったコードをここに記述
		// ...
	}

	return true;
}

ただし、PyObjectをいじるにはPyObjectがどういったものであるかをC++コンパイラに伝えないといけないため、PythonのSDKへパスを通す必要があります。今回のサンプルプロジェクトではcmakeを使っているので、include_directorieslink_directories にそれぞれWindowsにインストールしたPython3.7のパスを追記します。

include_directories(
    "src"
    "derivative"
    "derivative/GL"
    ${CMAKE_CURRENT_SOURCE_DIR}/libs/include
    "C:/Python37/include"
)

link_directories(
    ${CMAKE_CURRENT_SOURCE_DIR}/libs/lib
    "C:/Python37/libs"
)

pythonVersionの指定

オペレーターのビルドができた後でそれをTouchDesignerに読み込んで実行すると以下のエラーが表示されることがあります。

これは、 pythonVersion を指定しなかった時に出るエラーで、FillCHOPPluginInfo など、プラグイン初期化時に使用するPythonのバージョンを指定するとエラーが出なくなります (ここでTouchDesignerのPythonのバージョン以外のPythonを使えるかはよくわかりません…)

DLLEXPORT void FillCHOPPluginInfo(CHOP_PluginInfo *info)
{
	// ...
	
	info->customOPInfo.pythonVersion->setString("3.7");
}

pybind11

Pythonのヘッダとライブラリとを導入してビルドできるようになるとあとはPythonモジュールの開発と同じような感じで進めていけるのですが、Python APIはC言語で書かれており、一見して何をしているのかよくわからないコードが多数出てくるので、こういったことをする時はPythonとC++をうまいこと繋いでくれるpybind11というライブラリをよく使っています。

https://github.com/pybind/pybind11

pybind11はヘッダオンリーなライブラリなので、適当な場所に置いてインクルードパスを通せばOKです。今回は以下のようにして設定しました

include_directories(
    "src"
    "derivative"
    "derivative/GL"
    ${CMAKE_CURRENT_SOURCE_DIR}/libs/include
    "C:/Python37/include"
    "libs/pybind11/include" # pybind11
)

ソースコードの上のほうで

#include <pybind11/pybind11.h>

namespace py = pybind11;

という風にすると使えるようになります。簡単。
getParPython() で取得した PyObject* をpybind11管理にするには ↓ のようにします。pybind11のオブジェクト型は pybind11::print() でPythonと同じようにprintできます。

bool getOutputInfo(CHOP_OutputInfo* info, const OP_Inputs *inputs, void *reserved1) override
{
	info->numSamples = 600;
	info->numChannels = 1;

	PyObject* pyobj = inputs->getParPython("Pythonparam");
	if (pyobj)
	{
		py::object o = py::object(pyobj, false);
		py::print(o);
	}

	return true;
}

C++からText DATの関数を呼び出す

pybind11の動作確認ができた所で、C++からText DAT内で定義したPythonの関数を呼び出してみます。

TouchDesigner側では ↓ のようにして、C++オペレーターで定義した Python Parameter に対して mod('text1') のようにして、呼び出したい Text DAT をモジュールとして渡します。

モジュールとして渡されたText DATは普通のPythonオブジェクトなので、hasattrgetattr で内容にアクセスできます。
getattr でオブジェクトを取得した後は .call() で関数を呼び出すことができます。

bool getOutputInfo(CHOP_OutputInfo* info, const OP_Inputs *inputs, void *reserved1) override
{
	info->numSamples = 600;
	info->numChannels = 1;

	PyObject* pyobj = inputs->getParPython("Pythonparam");
	if (pyobj)
	{
		py::object text1 = py::object(pyobj, false);

		if (!text1.is_none()
			&& py::hasattr(text1, "td_func")) // td_func があるか確認
		{
			py::object td_func = py::getattr(text1, "td_func"); // td_func を取得
			td_func.call(); // 関数呼び出し
		}
	}

	return true;
}

TouchDesignerのTextportにText DATに書いたprint文の内容が表示されていればC++ → Python呼び出しは成功です

関数呼び出しの引数

関数呼び出しの際の .call() にはある程度任意の引数を入れることができます。その際の型変換やめんどくさい部分は pybind11 が自動で対応してくれます。

たとえば、std::vectorのようなstlの型も渡すことができます。#include <pybind11/stl.h> をインンクルードに追加して、関数呼び出し部分を ↓ のように変更します

std::vector<float> arr{1, 2, 3, 4, 5};
td_func.call(arr, "string");

その際、呼び出される側の Text DAT のPythonコードも変更します。実行してみると意図した通りに引数が渡されているのがわかります。

PythonにC++の関数をエクスポートする

次に、PythonにC++の関数をエクスポートする方法です。

大まかな流れとしてはさきほどと同じく Text DAT のモジュールに対して行うのですが今度は py::setattr() を使います。
ただし、Text DAT内であらかじめ定義されている内容に対してしかsetattrできないので、Text DATを編集してあらかじめ呼び出しの関数を定義する必要があります。

別のText DATを作って関数を呼び出すと、text1 で定義されたように dummy と表示されました。次にC++プラグイン側からこの関数を上書きする部分を実装していきます。

PyObject* pyobj = inputs->getParPython("Pythonparam");
if (pyobj)
{
	py::object text1 = py::object(pyobj, false);
	
	// ...

	if (!text1.is_none()
		&& py::hasattr(text1, "cpp_func"))
	{
		// 複数回初期化されないためのガード
		static bool inited = false;
		if (!inited)
		{
			inited = true;
			
			// Pythonにエクスポートする関数を作る
			auto fn = py::cpp_function([]() -> std::string {
				std::cout << "C++" << std::endl;
				return "c++ result";
			});

			py::setattr(text1, "cpp_func", fn); // cpp_func を上書き
		}
	}
}

関数のエクスポートを複数回やるのはあまり意味がないので static bool のフラグを作って複数回実行されないようにしました。Pythonの関数は pybind11::cpp_function() に関数を渡すことで作れます。今回はラムダ式の関数を渡しましたが、おそらく std::function に変換できるものであれば何でも渡せると思います。

別のText DATからさきほどと同様に cpp_func を呼び出すと、dummy のプリントが消え、かわりに返り値として str が戻されるようになりました。
環境変数として TOUCH_TEXT_CONSOLE=1 を指定している場合、TouchDesigner起動時にstdout出力されるウィンドウが表示されます。そこには実装通り C++ と表示されているはずです。

まとめ

これはC++のライブラリしか提供されていないデバイスをTouchDesignerで使うために試行錯誤しているうちに見つけた方法なのですが、最近はこの方法よりもpybind11やCythonなどでC++ライブラリをラップしたPythonモジュールを作り、それをTouchDesingerでインポートして使ったほうが楽なのでは… という風に思ってきております。

ただ、この手法でしか解決できないケースもありそうではあるので、どこかの誰かの為に役に立つかもしれないと思い直して記事としてまとめました。

TouchDesignerの情報かと思いきやほとんどC++とcmakeとPython APIの話になってしまい、TouchDesignerのアドベントカレンダーを楽しみにしていた方々には申し訳ないです… 完全に ↓ になってしまった

Discussion