🦿

【TouchDesigner】PythonとAnnotateでEditor内を瞬間移動する

2023/12/19に公開

はじめに

こちらはTouchDesigner Advent Calendar 2023 19日目の記事です。
https://qiita.com/advent-calendar/2023/touchdesigner

みなさんは2022年に実装されたAnnotateCOMP使っていますでしょうか?
この機能によって実装ごとにまとめたりコメントを入れられたりとても便利になりました。

今回はこのAnnotateCOMPを用いてNetworkEditor内を飛び回るシステムを作りました。
https://twitter.com/usbhatyu/status/1737088888571826459
経緯や実装から書くと長くなったので結論からまとめます…!
Githubはこちら(READMEは年末年始書きます。。)
https://github.com/usbmasa/JumpOperator

結論

簡単にまとめるとAnnotateCOMPができたことによって、Pythonで機能ごとにまとまってるオペレーターに擬似的にジャンプできるようになりました。
TouchDesignerのオペレーターをPythonからいじることができるのはみなさん周知の通りかと思います。
chimanacoさんが何年も前に記事にしてくださっています。
https://qiita.com/chimanaco/items/6db8b1698325242332a9
derivativeドキュメント
https://derivative.ca/UserGuide/Working_with_OPs_in_Python

以下の実装でPython側からオペレーターの位置にジャンプすることができます。
一旦全てのオペレーターの選択状態を解除しパスからオペレーターを検索し、そのオペレーターを選択状態にしてジャンプします。

# 指定のオペレーターを取得
focusOpName = 'audiodevout'
target_op = op(focusOpName)

for op_all in ui.panes.current.owner.children:
	op_all.selected = False  # すべてのオペレーターの選択を解除
# 飛び先のオペレーターを選択状態にする
target_op.current = True
target_op.selected = True
ui.panes[0].owner = target_op.parent()  # UIのペインを更新
ui.panes[0].homeSelected()  # 選択したオペレーターを中心にUIを再配置

今までは機能としてまとまっている一塊の複数オペレーターにフォーカスするには対象のオペレーターのパスを全て知る必要がありました。
しかし同一階層のオペレーターを一つの大きなオペレーターが内包する機能ができたことで、フォーカスした時に結果的に機能としてまとまっているオペレーター全部が画面に収まった状態でジャンプできるようになりました。
AnnotateCOMPが別機能ではなくオペレーターとして実装してくれたのが大きいです。

実装

toeファイルの中で肝となるのは以下の部分
pathsTableDATです。

中を見てみると以下のようになっています。

大事なのは二つのCHOPExecuteDATです。
buttonGenerateExecがButtonを生成する処理、jumpButtonExecがボタンが押された時の処理です。
以下コードです。
コメント細かく書いてもらいました…ChatGPTに…助かりますね…

buttonGenerateExec
def onOffToOn(channel, sampleIndex, val, prev):
	# 一番上のオペレーター
	top_operator_name = parent().parent().name
	# 親のカスタムコンポーネントにセットされているTableDAT
	parent_table = parent().par.Paths.eval()
	# 各ボタンの値をまとめているMargeCHOP
	merge_chop = op('merge1')
	# ButtonをまとめるContainer
	generate_buttons_comp = op('GenerateButtons')
	# 現在フォーカスしているTableナンバー
	current_num = op('currentNum')
	# 現在の階層にある特定のオペレーターのリストを取得
	button_ops = parent().findChildren(type=buttonCOMP)
	math_ops = parent().findChildren(type=mathCHOP)

	# 各オペレーターをチェック
	for button_op in button_ops:
		# オペレーターの名前が 'NextButton' または 'PrevButton' でなければ削除
		if button_op.name not in ['NextButton', 'PrevButton']:
			button_op.destroy()

	for math_op in math_ops:
		# オペレーターの名前が 'NextMath' または 'PrevMath' でなければ削除
		if math_op.name not in ['NextMath', 'PrevMath']:
			math_op.destroy()

	# 新しいButton COMPとMath CHOPを作成する
	for i in range(parent_table.par.rows):
		# Button COMPを生成
		button_comp = parent().create(buttonCOMP, 'Button' + str(i))
		# ButtonTypeをMomentaryに設定
		button_comp.par.buttontype = 0  
		# 飛び先オペレーター名をラベルに設定
		button_comp.par.label = op('/'+top_operator_name+'/'+parent_table.cell(int(i), 0)).name
		if(i == current_num.par.value0):
			button_comp.par.colorr = 1.0
			button_comp.par.colorg = 1.0
			button_comp.par.colorb = 0.5

		# Math CHOPを生成
		math_chop = parent().create(mathCHOP, 'Math' + str(i))
		math_chop.par.gain.val = i+1  # Multiplyパラメータを連番の数字に設定

		# Button COMPの出力をMath CHOPの入力に接続
		button_comp.outputConnectors[0].connect(math_chop.inputConnectors[0])

		# Container COMPの出力をButton COMPの入力に接続
		generate_buttons_comp.outputCOMPConnectors[0].connect(button_comp)
		
		# Math CHOPの出力をMerge CHOPに接続
		math_chop.outputConnectors[0].connect(merge_chop)
	return
jumpButtonExec
def onOffToOn(channel, sampleIndex, val, prev):
	# 現在フォーカスしているTableナンバーを取得
	current_num = op('currentNum')
	# 親のカスタムコンポーネントにセットされているTableDATを取得
	parent_table = parent().par.Paths.eval()
	# 一番上のオペレーターの名前を取得
	top_operator_name = parent().parent().name
	# 飛び先のTableナンバーを格納する変数を定義
	new_num = current_num.par.value0
	# 同階層内のボタンオペレーターを取得
	button_ops = parent().findChildren(type=buttonCOMP)

	# NextまたはPrevボタンが押された時の処理
	if(val >= 100):
		if(val == 100):
			new_num = current_num.par.value0 + 1  # 次の番号に更新
		elif(val == 101):
			new_num = current_num.par.value0 - 1  # 前の番号に更新
	else:
		new_num = val

	# 遷移先のページ数が0以下またはTableの要素数を超えている場合、何もしない
	if(new_num <= 0) or (new_num > parent_table.par.rows):
		return

	# ボタンの色を初期化
	for button_op in button_ops:
		button_op.par.colorr = 0.5
		button_op.par.colorg = 0.5
		button_op.par.colorb = 0.5

	# 押下されたボタンをハイライトする
	push_button = op('Button'+str(int(new_num - 1)))
	push_button.par.colorr = 1.0
	push_button.par.colorg = 1.0
	push_button.par.colorb = 0.5

	# 指定のオペレーターへフォーカスを移動
	focusOpName = parent_table.cell(int(new_num) - 1, 0)
	print(focusOpName)
	target_op = op('/' + top_operator_name + '/' + focusOpName)

	# オペレーターを現在のオペレーターとして設定し、選択状態にする
	target_op.current = True
	target_op.selected = True

	# 指定されたオペレーターが存在するかどうかをチェック
	if target_op is not None:
		for op_all in ui.panes.current.owner.children:
			op_all.selected = False  # すべてのオペレーターの選択を解除
		ui.panes[0].owner = target_op.parent()  # UIのペインを更新
		ui.panes[0].homeSelected()  # 選択したオペレーターを中心にUIを再配置
		current_num.par.value0 = new_num  # 現在の番号を更新
	else:
		print("指定されたオペレーターが見つかりません。")  # オペレーターが見つからない場合のメッセージ

	return

TableDATのパスの先のオペレーターを取得し、登録したパスの数ButtonCOMPやそれに付随するCHOPを自動で配置、接続しています。
Next,Prevボタンはデフォルトで存在しています。
また遷移先が10個以上になるとスクロールできるようになります。

TableDATの中身を変更したときはUpdateを押してください。

経緯と使い所

ここからは実装とは別の開発中に考えてたことや今回のシステムの使い所のまとめなのでお時間ある時に読んでもらえればと思います。

僕の普段の仕事ではイベント用にTouchDesignerでちょっとしたゲームを作ることがあります。
ちょっとしたゲームでもステート管理、制限時間、得点、センサーの制御、アニメーション、ポストエフェクト,etc...と細かい機能が裏側も含めて盛りだくさんです。

ノードベースの個人的に苦手な部分が、複雑になると処理の繋がりがわかりづらくメンテナンス性がよくないことです。(スパゲッティコードの視覚化)
なので機能や役割(各機能のデータ保持担当,データ処理担当等)ごとにContainerCOMPAnnotateCOMPでまとめて後から見返して素早く理解できるように心掛けています。
そのおかげで機能ごとに整理されていて理解しやすいのですが、同時にEditor内の移動がめんどくさいです。
ネストも深くしたくないので処理の内容に繋がりがあるものはAnnotateCOMPで同一階層にいくつかグループを作るので、例えばバグが起きた際処理の流れを追うのに何回もマウスをドラッグしたり、UとIキーを押すことになったりします。

またUnityで開発していたら本番用とデバッグ用のUIを実装しますが、Touchで本番用UIとデバッグ用UIを二つ同じContainer内で用意するのはわかりづらいですし、Layoutを考えたり表示、非表示の管理や納品時の削除が手間です。
今回の仕組みを使えば本番に必要なUIだけを表に出しつつ、検証や開発中に必要なパラメーターを各機能の中に留めることができます。
開発終えた機能への導線が不要になれば気軽にパスを削除するだけでOKです。

別の使い道

開発とは別にプレゼンのスライドとして使うこともできます。
以前お付き合いのある会社様と技術共有会をした際、文章量も少なくパワポとTouchを行き来するのは少し冗長だなと感じたので、今回の仕組みを使ってオペレーターをジャンプしてスライドと実装の説明をTouchDesignerだけで完結させたりしました。
ちょっとしたことですが興味を持っていただき嬉しかったです…!

まとめ

以上今年のアドカレの記事でした。
今年は前半0 // 2023 Public Visuals Tokyo x TDSWやDOMMUNEなどイベントに出させていただいたり、高頻度でビジュアルをアウトプットしていて割と表に出る活動をしていたのですが、反動で後半は引きこもって作品を黙々作ったり、システマチックなもののばかり開発していた1年でした。
来年はどんなものを作る一年になるのでしょうか??
それではみなさん良いお年を〜!

Discussion