TouchDesignerでGLSL Shaderを使ってLaunchpadをマトリックスディスプレイにする

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

前日（6日目）の記事は@satoruhigaさんの「[感覚的に作るには](https://qiita.com/satoruhiga/items/77a183b6496334477080)」です。

<!--翌日（8日目）の記事は@joe0haraさんの「XXXXX」です。-->

----

TouchDesignerを休日にたしなんでいます @miyukki です。

## はじめに
Novation Launchpadとは、Novation社が出しているUSBインターフェースを備えたMIDIインターフェースで、ドラムラックの演奏やサンプラーなど音楽制作で使われることが多いデバイスです。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/04626ba1-6c6a-c53b-5596-06593acaaf96.png">
最近では楽曲制作以外にも、システムの制御やステータスの表示にNovation Launchpadが使われることも増えてきました。おそらく、TouchDesignerユーザーの方であれば1台持っていたりするのではないでしょうか。

ちなみに、私が知っている事例ではInterop Tokyoでネットワーク機器のステータス表示に使っていたりする事例もありました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ShowNet NOCで面白いのかったの、このMIDIコントローラーでおそらく何かのステータスを表示するやつ <a href="https://twitter.com/hashtag/interop2019?src=hash&amp;ref_src=twsrc%5Etfw">#interop2019</a> <a href="https://t.co/7QqctQ9bKu">pic.twitter.com/7QqctQ9bKu</a></p>&mdash; みゆっき(@toriimiyukki) <a href="https://twitter.com/toriimiyukki/status/1139356533010001921?ref_src=twsrc%5Etfw">June 14, 2019</a></blockquote>

余談はさておき、このLaunchpadを現場でまじまじと見ていると、マトリックスディスプレイとして使えないかと思い、空いている時間を見つけてTouchDesignerを使って実現しました。toxやコードを貼るだけでも良いのですが、せっかくなので思考プロセスや工程を含めて解説していこうと思います。

## Launchpadの基本動作

まず、Launchpadの基本動作についておさらいしておきます。ここでは、TouchDesignerにおけるMIDIデバイスの設定方法などは割愛します。
*Launchpadは、64個の何も書かれていないボタンと16個のコントロール用のボタン、計80個のボタンが備わっていますが、このうちコントロール用のボタンに関してはここでは取り扱わないものとします。*

`MIDI In CHOP` を設置してLaunchpadのボタンを押してみると、画像のようになります。
<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/7c708ef8-db1c-88e0-4484-7eb082075002.png">
Launchpadでは、ボタン下から1行目が `ch1n12`〜`ch1n19` 2行目が  `ch1n22`〜`ch1n29`... といったチャンネルになるようです。ボタンとチャンネルの対応を分かりやすくすると、以下の画像のようになります。
<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/2e362f90-d8e5-d6ea-cf0c-6539d4524789.jpeg">

また、試しに `ch1n12`〜`ch1n19` に対して 適当な値を入れた `Constant CHOP` を `MIDI Out CHOP` に接続してみます。MIDIの値は 0-127 のレンジで表現され、`MIDI Out CHOP`に送られた 0-1 の値は自動的に 0-127 変換されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/2844adbf-58b2-7ac3-f048-d1c51f20d2cd.png)
するとデバイスでは以下の画像のように表示されます。
<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/aea65b7e-fc71-2552-1234-35118f7a39f8.jpeg">
ボタンが押されたチャンネルに対して、値を入れることでボタンを表示させたり、またその色を変えたりできるといったことが分かりました。

## Launchpadのボタン色

ここで、勘の良い人であれば、どの値がどの色になっているのかの対応が気になるかと思います。
しかし、私が調べた情報では、0-127の値の中である程度の規則性がある部分とそうでない部分があるそうです。

0-127 の値と色の対応を一覧にまとめた画像がありましたが、確かにこれをコード上でRGBから変換するのは難しいような気がしてきました...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/b0938d15-e95d-8e31-0cf1-c24279d2eccc.png)

どうにか、RGB->MIDI値の変換ができないか...ということで私の中で出た方法が、表示したい色とLaunchpadのボタン色パターンを比較して一番近いパターンの色を採用するというものです。

楽に考えるのであればPythonを用いるのが良いのですが、8x8のピクセルに対して127通りのループを作るのはパフォーマンスの面であまり気がすすみません。

## GLSL Shaderを用いた近似色探索

そこで、GLSL Shader を用いた解決方法を思いつきました。

下図のように `表示したい色`と`Launchpadで定義された色`をRGBの三次元座標に置いて、それぞれの距離を算出し、一番近いパターンの物を採用するというイメージです。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/fd2bf5fe-1e9f-6aca-0149-5914e5aa2a0d.png">

LaunchpadのRGBカラーについては、運よくGitHubで公開している人がいたので、これを使用することにします。
https://github.com/mohayonao/launch-pad-color

## マトリックスディスプレイにする

Launchpadをマトリックスディスプレイとして表示するためには、以下のような流れになると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/156fee6b-9bc2-9af3-f9db-c64369caa40e.png)

実際には、以下のようなノード構成になりました。
![TouchDesigner_2020_26630___Users_a14802_Desktop_LEDDisp_toe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/7bd41c52-7937-e9ca-36de-a657d477764e.png)
8x8のためのクロップなどの処理を経て8x8のTOPは、まず`GLSL`に接続されます。
GLSL Shaderの中身は以下のようになっています。

```glsl
out vec4 fragColor;

// Launchpadの色パターン
vec4[128] colorMap = vec4[](
	vec4(  0,   0,   0,   0), vec4( 80,  80,  80,   1), vec4(160, 160, 160,   2), vec4(240, 240, 240,   3), vec4(255, 145, 133,   4), vec4(255,  35,  10,   5), vec4(230,  31,   9,   6), vec4(204,  28,   8,   7)
	// ...略...
);

void main()
{
	// 表示したいピクセルの色
	vec4 color = texture(sTD2DInputs[0], vUV.st);

	vec4 cl;
	float d;
	float min = distance(0, 1);
	int idx = 0;
	for (int i = 0; i < colorMap.length(); i++)
	{
		// ２点間の距離を計算して、一番近いもののインデックス(=MIDI値)を保持する
		cl = colorMap[i];
		d = distance(color.rgb, cl.rgb/255.0);
		if (d < min) {
			min = d;
			idx = i;
		}
	}

	// GLSLが出力するピクセルのαにMIDI値を代入する
	color = vec4(0.0, 0.0, 0.0, colorMap[idx].w/255.0);
	fragColor = TDOutputSwizzle(color);
}
```

`GLSL`のあとは、αの値(MIDI値)を取り出すための処理と、データをLaunchpadのチャンネルに変換するための処理が続いています。
![TouchDesigner_2020_26630___Users_a14802_Desktop_LEDDisp_toe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/d833743f-0410-5ecc-5a64-a93a7d520bb7.png)
![TouchDesigner_2020_26630___Users_a14802_Desktop_LEDDisp_toe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/8e0f8c5e-e0af-9143-0a7f-2533efb500f3.png)

ちなみに、`Rename CHOP`は `ch1n[1-8][2-9]` とすることにより `ch1n12, ch1n13, ..., ch1n19, ch1n22, ...`となることを知りました。べんり〜

## マトリックスディスプレイで遊ぶ

作りたいものが作れた後は、思いっきり遊ぶだけです！

#### 何かしら画像を出してみたり
<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33754/9b1b7088-ca71-cad0-1e39-71cff49fac75.jpeg">

#### 長い文字をスクロールしてみたり
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今日のTouchDesigner Advent Calendarのネタです <a href="https://t.co/nZlH9EjCyB">pic.twitter.com/nZlH9EjCyB</a></p>&mdash; みゆっき(@toriimiyukki) <a href="https://twitter.com/toriimiyukki/status/1335624821275693056?ref_src=twsrc%5Etfw">December 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#### `Video Device In TOP`とつないでカメラの画像を表示してみたり
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">続き <a href="https://t.co/LYFOcfhoyF">pic.twitter.com/LYFOcfhoyF</a></p>&mdash; みゆっき (@toriimiyukki) <a href="https://twitter.com/toriimiyukki/status/1335631666723704832?ref_src=twsrc%5Etfw">December 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## あとがき

今回はGLSLを映像処理ではなく、GPUを生かした並列処理を行うオペレーターとして扱ってみました。特にパフォーマンスにシビアなプロジェクトではいかにCPUなど時間のかかる処理をオフロードできるかが重要になってくるかと思います。

また、今回作った8x8のTOPをLaunchpadに表示するやつはtoxとしてGitHub上に公開してますので、是非ご利用ください。興味がある方は、toxの中も覗いてみてください。
https://github.com/miyukki/touchdesigner-launchpad-disp