映像オペレーション用システムのUI構築

この記事は[TouchDesigner Advent Calendar 2022](https://qiita.com/advent-calendar/2022/touchdesigner)の12日目の記事です。

[株式会社レイ](https://ev.ray.co.jp/)という映像機材を扱う会社でエンジニアをしています。
TouchDesignerで映像オペレーションをする上でのUIを構築することになりました。
これまでちゃんとUIを組んだことが無かったので、今回は学習の共有です。

# UI構築の経緯
現在弊社ではスイッチャーやメディアサーバの他にV出しにはRolandのPR-800HD、VJ的にレイヤーで出す時はRosolumeのArenaなどを使ったりします。

PRはシンプルで使いやすいのですが、4KのV出しができません。Arenaはアウトプットの切り出しが簡単でLED出しの時に便利なのですが、TDと併用になると最初からTDで完結できるのでは？と思いました。

また今後TDを使った汎用的な演出や仕組みを開発した時に、それを組み込むベースとなり自分以外のオペレータ（非TDエンジニア）も違和感無く使えるシステムが必要と考え、今回のUI構築となりました。

# 完成のイメージ
Figmaで何となく完成時のイメージを作りました。
![20221130_MokuMoku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/ec5252f3-655f-9a6b-0aad-1a2999846e87.png)

# UIを作る上でのポイント
個人的には以下の3つがポイントと考えました。

* **Panels系COMPの挙動・機能理解**
* **CloneとExternalを利用した静的複製**
* **ReplicatorCOMPによる動的複製**

## 1. Panels系COMPの挙動・機能理解
### 1-1. 親子関係を作る上での使い分け

UIを作る時はCOMPのPanels系を階層化するかオペレータの上下のワイヤーを繋げて親子関係を作ります。
![スクリーンショット (77).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/9d0cc75f-f6a7-19d3-6c9c-8de2ba8dc79b.png)


シンプルなUIな場合は、Base COMPでオペレータをまとめる時のように階層構造で問題無いです。しかし複雑なUIを作ろうとすると、階層がどんどん深くなってメンテナンス性が最悪になります。

極力ワイヤーで親子にしつつ、**細かすぎるパーツ**や後述の**複製が発生する場合**は階層化します。
![スクリーンショット (79).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/3f681eff-6cfe-3db1-f132-9c2a9cc01059.png)
（現在構築途中のNetwork Editor画面）

### 1-2. 各UIパーツをキレイにレイアウトする
基本的には子のLayoutのModeを`Fill`にして、親のChildrenの`Align`を指定すればキレイになります。
![スクリーンショット (75).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/8a5ca141-fa93-29e6-aa4f-26b951b0a743.png)
子の位置を整頓したい場合
⇒ `Justify Horizontal/Vertical`を指定

パーツとパーツの間に隙間を空けたい場合
⇒ `Spacing`と`Margin`

パーツを正確な比率（7:3など）に整頓したい場合
⇒ 子のModeを`Anchors`に設定、対応させたいCOMP同士をReferenceで紐づける
![スクリーンショット (80).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/933d990f-1312-1406-44c4-6def0584e5ae.png)

### 1-3. パーツごとに特定の機能を付与する
アプリケーション「らしい」振る舞いをするには、これらの実装が必要です。

* **ドラッグ＆ドロップ**
Drag/Dropの`Drag/Drop Script`を設定することで、動画のセットなどができます。
![スクリーンショット (90).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/bcf98dda-7f7d-d2d8-cf0b-f3435cbb2454.png)
詳しくはJoe君がアドベントカレンダー20日目の記事で解説してくれます（丸投げ）

* **右クリックでメニュー表示**
`Info CHOP`や`Panel CHOP`を使って操作を検出することで、メニュー表示などができます。

* **MIDIやOSCと紐づける際のTIPS**
MIDIコンとTD上の数値を紐付けた際に、数値だけ何らかの理由で変えた場合コントローラと数値の間に差が生まれてしまい、操作した際にガクッとなってしまいます。これを避けるため、`Bind CHOP`の`Channel Pickup`をOnにすることで、コントローラが同じ値になるまで数値が変化しないようにすることができます。
![スクリーンショット (89).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/87af6779-ae69-4b30-eb9c-1fd598b8f1ef.png)

### 1-4. 結局「Widget COMP」って何？

半自動的かつ一括でCOMPのParameterをUI化する時に使います。
Palleteから`basicWidgets`と`autoUI`、さらにUI化したいCustom Componentが設定されたCOMPとUIを表示するためにContainer COMPを用意します。`autoUI`のUIとCustom COMPのところに上記の2つを紐付けてGenerate UIをPulseすると、UIが生成されます。
![スクリーンショット (81).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/6a0bb8d2-cbc9-4062-ab8e-ff0a60aaf840.png)
便利と言えば便利ですが、現状Widget COMP単体ではContainer COMPと挙動が基本変わらないことと、自分でパーツごとの見た目や機能は制御したいので使いません。

## 2. CloneとExternalを利用した複製
### 2-1. Clone機能の基本とOperatorのImmutable化
UIを作る際に、共通して同じ見た目・機能を持ったパーツを複数作ることになるのが通常だと思います。
しかしパーツごとに独立したCOMPにしてしまうと、後から変更を加える時個別に1つずつ変えなければいけなくなってしまいます。この問題を解決するのが`Clone`という機能です。

Clone先のCOMPのCommonタブにある`Enable Cloning`をOnにし、Clone元となるCOMPを`Clone Master`に設定することで、Clone元の変更が一括でClone先に反映されます。またClone元のCOMP内にあるオペレータの`Clone Immune`フラグを立てることで、そのオペレータのParameterをユニークにすることができます。
![スクリーンショット (82).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/b40548df-7a77-4eb2-0b3d-5cc9766e5686.png)

### 2-2. 複製したパーツに異なる見た目・機能を設定する
上記のようにImmuneにすることでCOMP内のParameterをユニークにしても良いのですが、実際の運用上は別の方法を採用しています。Cloneには**Clone元のCOMP自体のParametersは変更されない**という仕様があります。この仕様を生かして、ユニークにしたいParameterをCustom Componentに設定してあげる方が参照先が一元化されて、後から調整しやすいです。
![スクリーンショット (83).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/1263772d-b6da-59d2-181a-e4a55a62f773.png)
個別化してしまうと結局まとめて変更したい時に面倒ではと思うかもしれませんが、Custom Component自体は共通化されているので、変更したいCOMPを複数選択して1か所変更すれば一括で変えられます。これも先述の親子関係を極力階層で作らない／Custom Componentでユニーク化するという2点のメリットです。

### 2-3. Operatorのコンポーネント化とExternalによる共通化
作成したUIやパーツを別プロジェクトで使いたい場合があると思います。
その時は使用したいオペレータを右クリックし、`Save Component .tox`でtoxファイルとして保存します。このtoxを参照させたいオペレータのCommonタブにある`External .tox`に設定することで、Cloneと同じように元のtoxと共通化されます。
![スクリーンショット (84).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/1ce6dbd9-629b-f04f-6cb1-3386db15edfc.png)
ただtox化した時、toxの変更を参照元に反映させようと思うと一々Saveし直すのが面倒です。その問題を解決するのが[Satoru Higa](https://twitter.com/satoruhiga)さんの「[ExternalToxSaver](https://satoruhiga.gumroad.com/l/ExternalToxSaver)」です。Gumroadにて3ドルで購入できます。

## 3. Replicator COMPによる動的複製
### 3-1. Replicator COMP機能の基本
Replicator COMPの`Operator Prefix`で複製したいオペレータを指定、`Replication Method`を`By Number`にして`Number of Replicants`で数値を設定することで、その数値分だけ指定したオペレータが複製されます。
![スクリーンショット (85).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/6f0e736b-fac3-8e2b-5685-3f6ccf5257f5.png)
またReplicator COMPのcallbacksの中で`c.outputConnectors[{OUT}].connect({IN})`と記述することで複製時に元のオペレータのOUTを特定のオペレータ（例えばSwitch TOPなど）にINにすることが出来ます。
~~~Python:replicator2_callbacks
def onReplicate(comp, allOps, newOps, template, master):

	sw = op('switch1')

	for c in newOps:
		c.outputConnectors[1].connect(sw)
		
		#c.display = True
		#c.render = True
		#c.par.display = 1
		#c.par.clone = comp.par.master
		pass

	return
~~~

CHOPの場合は`Select CHOP`の`CHOPs`に`{Operator Prefix}*/out{num}`という形で記述することで、callbacksに書かなくともOUTの数値をまとめることが出来ます。
![スクリーンショット (86).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/e60aae41-ac13-6d41-0548-b7d359ac3b46.png)
### 3-2. 特定のOperatorのParameterUIを動的に複製する
Parameter COMPを使えば簡単にオペレータのParameterをUI化することができます。この時、CommonタブのParameterは不要な場合がほとんどだと思います。TIPSとしては`Page Scope`に`^Common`と記述する事で、CommonタブだけをUI化から外すことができます。
![スクリーンショット (88).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969514/be42a778-4ec7-269d-897e-90d194ae0b55.png)
ただこの方法だと、UIの見た目がParameter COMPに依存してしまいますし、実際にUI化したいParameterは決まっていることが想定されます（すべての読み込んだ動画にLevel TOPのOpacityのparameterを付けたいなど）。

なので、Cloneを使った複製の時と同様、UI化したいParameterをCustom Componentとして設定し、その値を`Parameter DAT`などで取得、Replicator COMPの`Template DAT table`の設定とcallbacksの組み合わせによって、UIを生成します。こうすることでCloneを使うにしろReplicatorを使うにしろ、ワークフローとフォーマットが共通化できます。

※**この辺りは今度追記します。。**

### 3-3. CloneとReplicatorCOMPの使い分け
結論だけ言うと、複製するUIを**静的**（決まった数・形式）に変更する場合はClone、**動的**（柔軟に増減する）な場合はReplicatorという使い分けになると思います。

# 終わりに
UIに関する記事は数多くあり、焼き増しの内容も多かったかと思います。
本来はアルファ版まで出来たものをこの記事のタイミングで公開する予定だったのですが、間に合わず初歩的な内容に終始してしまいました。

また開発途中のシステムとUIが出来た際には、そこから得た知見を共有したいと思います。

# 参考サイト
http://satoruhiga.com/TDWS2019/day16/

https://interactiveimmersive.io/blog/controlling-td/simple-right-click-menus-in-touchdesigner/

https://note.com/omakazu/n/n6b2596e9085c

https://www.youtube.com/playlist?list=PLSqkC3f_BStxA4TpF6uWj3HOHPCglRpN9

https://www.youtube.com/watch?v=B0lEnVglMkk

https://qiita.com/joe0hara/items/a1be5e9e06b8feac6cc1

https://qiita.com/joe0hara/items/42614c7deab5f0cd7f33

https://qiita.com/kodai100/items/05b360a89a91516f21e6