TouchDesignerのWeb系OP、こんな感じで使ってみた

## 序

あるところに「Twitter APIを叩いて絵文字👍もきれいに描画したい」案件があったとさ。

## 検証環境

[TouchDesigner](https://derivative.ca) 2021.15240 (Commercial LICENSE) on Windows 10 w/ GeForce RTX3090
（本記事のスクリーンショットでは一部MacOSを使っています）


## Web Render TOP

絵文字を描画するのが[Text TOP](https://docs.derivative.ca/Text_TOP)では難しかった（絵文字対応フォントを使っても絵文字のカラー表示ができない）ので、
[Web Render TOP](https://docs.derivative.ca/Web_Render_TOP)を使って[HTML Canvas](https://developer.mozilla.org/ja/docs/Web/HTML/Element/canvas)に描画する作戦を展開。

![Screen Shot 2021-11-07 at 15.50.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/37810b86-ccf6-ed6d-14b8-fcd1585d1f10.png)

### Tips 1: File Inを使う

ソースに[File In DAT](https://docs.derivative.ca/File_In_DAT)を使ってHTML制作と分業する。
Canvasに `fillText()` する例はこんな感じ。

```html
<!DOCTYPE html>
<html class="no-js" lang="en">
  <head>
    <meta charset="utf-8" />
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      * {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.querySelector('canvas');
      const ctx = canvas.getContext('2d');
      const text = `MESSAGE`;
      const resized = () => {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        ctx.font = '64px sans-serif';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#999999';
        ctx.fillText(text, 0, 0);
        document.title = ctx.measureText(text).width;
      };
      window.addEventListener('resize', resized(), { once: true });
    </script>
  </body>
</html>
```

### Tips 2: Substitute DATで置換

![Screen Shot 2021-11-13 at 3.31.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/2165e80b-7420-c0e2-5577-8f84241d1837.png)

Operator Snippetの"display HTML saved in DAT"にもあるように、[Substitute DAT](https://docs.derivative.ca/Substitute_DAT)との組み合わせでTouchDesigner側の値をWeb Render TOPに渡すことができます。
冒頭の例では、 `MESSAGE` を任意のメッセージに置き換えています。
Substitute DATの2ndインプットにつなぐTable DATを書き換えると勝手にリロードされるのでお手軽です。

### Tips 3: executeJavaScript(script)

![Screen Shot 2021-11-07 at 13.36.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/0c10167f-f0c0-8b90-d855-62aacb694756.png)

Operator Snippetの"use javascript to control content"では[executeJavaScript(script)](https://docs.derivative.ca/WebrenderTOP_Class#Methods)を使った例を見ることができます。
ただ、実行したいjsを文字列として用意するのも、それをデバッグするのもしんどいので、最終手段に取っておきます。

### Tips 4: Canvasの最大化

Web Render TOPから得たい解像度を `Common > Resolution` で弄ったら、それに合わせてCanvasの大きさを変えるようにします。
`Window.resize()` イベントはリロード時にも発火するので、これだけ用意すればOKです。
ちなみに、Chromiumは `9999px` 以上の値に対して期待される振る舞いにならないことが多いらしく、
もっと大きな領域を使いたい場合は何かしらの工夫が必要そうです。

### Tips 5: TitleとCrop TOPの組み合わせ

![Screen Shot 2021-11-07 at 15.51.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/07393b5a-9922-5c3c-bd1b-f596b82aedb2.png)

Web Render TOPにInfo DATをつなぐと `title` が取れることがわかります。
なので、jsで `document.title` を設定してTouchDesigner側に値を返すことができます。
先述の例ではCanvasに描画された文字の幅(= `ctx.measureText(text).width` )を返しています。

```js
    ctx.textAlign = 'left';
    ctx.textBaseline = 'top';
```

として描画位置を左上合わせにしているので、[Crop TOP](https://docs.derivative.ca/Crop_TOP)で簡単に切り出せます。
Canvasに表示されるスクロールバーを消すのが意外と面倒だったりするので、この組み合わせは便利です。

### Tips 6: Transparent Background

Web Render TOPの"Transparent Background"をONにしておくと背景が透過された状態で出力されます。
TouchDesigner側でさらに手を加えることがほとんどだと思うので、透過して出力しておきます。
ON/OFF切り替え時に反映されない場合は、ActiveもON/OFFし直しましょう。

### Tips 7: Fetch APIとCORS対応

![Screen Shot 2021-11-07 at 17.02.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/1e551dc8-204d-bfc8-f2a7-b6561248c5bd.png)

[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)などを用いてWeb Render TOP内でAPIを叩いてしまうことも可能です。

```html
<!DOCTYPE html>
<html class="no-js" lang="en">
  <head>
    <meta charset="utf-8" />
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      * {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.querySelector('canvas');
      const ctx = canvas.getContext('2d');
      let text = `MESSAGE`;
      const resized = () => {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        ctx.font = '32px sans-serif';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#999999';
        fetch('https://qiita.com/api/v2/items')
          .then((response) => response.json())
          .then((data) => {
            text += ' from ' + data[0].user.name;
            ctx.fillText(text, 0, 0);
            document.title = ctx.measureText(text).width;
          });
      };
      window.addEventListener('resize', resized(), { once: true });
    </script>
  </body>
</html>
```

ただし、API先によっては[CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)起因で `fetch()` の失敗がままあります。
「適切なヘッダをつけてくれ」と言える相手なら対応依頼すれば良いですが、
たいていは別チーム&短納期&大人の事情だったりで、こちら側で対処しなければなりません。
相手方の安全性を確信できる場合、Web Render TOPの `Options` に `--disable-web-security --user-data-dir` を入れるとCORSを無視できます。
（ただし、この対処法もいずれ使えなくなりそうです…）

### Tips 8: Engine COMPと組み合わせる

結局のところ、Web Render TOPはTouchDesignerの中でChromiumが動いているに過ぎないので、負荷はそれなりにあり、
単一の.toe内で複数のWeb Render TOPを使うと、Reload時にdropped framesします。
他のケース同様、「刻みtoe」などの秘伝の対応策がありますが、
[Engine COMP Pro Tricks - The Interactive & Immersive HQ Inc.](https://interactiveimmersive.io/blog/deployment/engine-comp-pro-tricks/)の記事が素晴らしかったので、[Engine COMP](https://docs.derivative.ca/Crop_TOP)を使ってみました。
Engine COMP内外の通信にTouch In/OutなOPを使うようtox化するだけで良いのは素晴らしいですね。

## 余談1: OPへの色付け

![Screen Shot 2021-11-13 at 3.22.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/be357bb4-d1a2-c9b1-9d3c-251262ce31dc.png)

[「C」キーで使えるOPへの色付け](https://docs.derivative.ca/Application_Shortcuts)、余裕があるときにしかできないですが、
色をつけるときはRGBにならって、下記をマイルールにしています。

- Red: 入力系
- Orange(= Red + Green): 入力から処理系までのデバッグ
- Green: 処理系、の中でもScript系
- Cyan(= Green + Blue): Scriptから出力系までのデバッグ
- Blue: 出力系
- Yellow: 工事中、Work in Progress

三原色的にはRed+GreenはYellowですが、
YellowでCookを切った状態がトラ模様に見えるので、これを「工事中」としています。
Engine COMPが他のCOMPと見分けにくいときはPurpleとかつけることもあります。
ただ、6色の時点ですでに使いすぎてる感があり、もっとシンプルにした方が良い気もしています。

「色がつけられるよ」の先、「色の対応」については他の方がどうやっているかあまり聞いたことがないので、
もっと直感的な対応表があったらぜひ教えてください。

## Web Client DAT

### 基本

![Screen Shot 2021-11-07 at 18.19.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/dd803936-dd76-f122-8582-9b733f3ded42.png)

Operator SnippetにGETの例しかなかったので、POSTする例を。
1stインプットがheaderの[Table DAT](https://docs.derivative.ca/Table_DAT)、2ndインプットがdataの[Text DAT](https://docs.derivative.ca/Text_DAT)。

POST内容もScriptで作って[request()](https://docs.derivative.ca/WebclientDAT_Class)でも一撃ですが、
OPのつながりで実現できることはなるべくScriptに書かない方が良い気がしているので、
DATを2つ用意して、 `webclient.par.request.pulse()` だけ呼ぶのが好みです。

### 応用: Google Spreadsheetへログ

![Screen Shot 2021-11-07 at 18.37.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/e7781185-8159-91e4-5011-688805700490.png)

ログをSlackへ送る運用も試したことがあるのですが、通知がとんでもないことになったり、検索性が悪かったり、Slack無料枠では記録しきれなかったりするので、
Google Spreadsheetへの書き出しを推したいです。
Google Spreadsheetに吐いておけば長期的な値もグラフで確認できるし、複数人とのオンライン共有も容易いです。

Google Apps Scriptは `doPost()` でPOSTを受け取ることができます。
Google SpreadsheetでGoogle Apps Scriptを使う方法、および `doPost()` をWebアプリとして公開する方法については他の良記事がたくさんあるので割愛します。
小技として、日別や時間別にシートを作れるようにして、膨大な行数になるのを防いでいます。

```main.gs
function prepareSheet(spreadSheet, sheetName, header) {
  let sheet = spreadSheet.getSheetByName(sheetName);
  // if Sheet名を探して見つからない → 作る
  if (sheet == null) {
    sheet = spreadSheet.insertSheet(sheetName);
    sheet.appendRow(header);
  }
  return sheet;
}

function doPost(e) {
  // 当該シートのID
  const spreadsheet = SpreadsheetApp.openById(
    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
  );
  const receivedData = JSON.parse(e.postData.contents);
  const sheetName = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
  const header = ['Timestamp', 'Data'];
  // Sheetを用意する
  let sheet = prepareSheet(spreadsheet, sheetName, header);
  // Timestampを用意
  let arrayToAdd = [
    Utilities.formatDate(new Date(), 'Asia/Tokyo', 'HH:mm:ss.SSS'),
  ];
  // dataを用意 (追記前に整形するならこのあたり)
  Array.prototype.push.apply(data);
  // 書き込み
  sheet.appendRow(arrayToAdd);
}
```

何回か試した感じ、5 POST/秒までは問題なく、10 POST/秒ぐらいから書き込み順序が保証されず、それ以上は少しずつ取りこぼす様子。
まぁ、10行/秒以上はログとは呼ばない気がするので十分実用かと思います。

## 余談2: TDな現場であると嬉しい機材

![0B47190-500.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/d2351cd5-e701-509a-fc21-23318863e9e0.png)

[ThinkPad トラックポイント・キーボード](https://www.lenovo.com/jp/ja/accessories-and-monitors/keyboards-and-mice/keyboards/KEYBOARD-US-English/p/0B47190)は小さくて軽い上に中ボタンがあるので、
「3ボタンマウス忘れた」あるあるを回避できます。
左手と右手、どちらもカーソルを操る二刀流状態だと中ボタンドラッグでの細かいパラメータ調整がほんのちょっとやりやすい。
欠点はFnキーを同時押ししないとF1-F12キーが使えないこと。
Bluetoothモデルより有線モデルの方がより安心かつ安価。
海外の人に貸すこともありえるので配列はUS一択です。

![automate_detection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/8843d000-a644-7148-e48d-e637e45b1582.png)

[Stream Deck](https://www.elgato.com/ja/stream-deck)を使うと、「特定ファイル(= `.toe`)の起動」を物理化できるので、運用を他者に任せるときに便利。
そのほか、「CPU使用率の監視」、「外部IPアドレスの確認(= ネットワーク接続状態の監視)」を画面外で行えるほか、
「OSCの送信」などもできるのでデバッグにも便利。
いつか暇になったら、TouchDesigner → Stream Deckな仕組みも作ってみたいところ。
1年ほど使ってみて、ノートラブル。
一番小さい6キー版で十分だけど、便利すぎるのですぐに15キー版を買い増ししました。

![4962420473252_03_ATEM156.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/3a756c90-8303-5dd4-ccc5-5b92615ed7c0.png)

[SEETEC ATEM156](https://feelworld.ltd/products/seetec-atem156-15-6-inch-live-streaming-broadcast-monitor-with-4-hdmi-input-output-for-atem-mini)は、HDMIが4入力、4スルーできる15.6インチのモニタで、
Amazon JPで5.5万円ぐらいでした。
4K信号も一応入力できて、4分割表示もできる、とは言えAmazonレビューが微妙で、「まぁ、モニタ4枚で5.5万円なら失敗しても許せるか」ぐらいで購入したのですが、
多画面出力あるあるの「HDMIのEDIDが飛んだ、OSのディスプレイ設定が変わった」を物理的に回避できます。
ただ、分割切り替え時に一瞬黒が出力される問題があって、本番中に切り替えるのはご法度。
別画面でUIを出しておきたい、キャプチャ前のソースを確認したい、などの確認用に使うと良い感じです。
15.6インチなのでちょっと大きめのノートPCを1枚余計に、ぐらいの感覚で持ち出せます。

## 結

美しいグラフィックや斬新なインタラクションについては偉大な先人が数多いらっしゃいますし、
一時期、[NTT ICC](https://www.ntticc.or.jp/ja/)でテクニカルスタッフをしていたこともあって、
私からは運用に近いところの知見を共有させていただきました。

Web系OPの利点は、TDよりWebでやった方が早いところを切り出して、Web系エンジニアに任せられる、
つまり工期圧縮につながるところだと思います。
[Three.js](https://threejs.org)でWebGLとかも面白そうですが、それは[GLSL TOP](https://docs.derivative.ca/GLSL_TOP)やSOPが豊富なTDの方が得意な領域なので、
今回のように文字主体のレンダリングとか、データベースに接続してあれこれみたいなケースに適していそうです。

クライアントワークを実例にしてしまったので、本記事から案件特定できてしまった人は内緒にしておいてください:P
誰の役に立つかわかりませんが、サンプルのtoeも置いておきます。
説明が下手なところや、間違いがあったらお気軽にご指摘ください。

![Screen Shot 2021-11-13 at 3.56.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234167/6d0f99bc-f6cb-c2d1-5255-92e3aa1c8c90.png)

https://drive.google.com/file/d/1l0GRzXWP_3WusFZ-rouzTcTPjZLBt8k-/view?usp=sharing