なんちゃってpixel sortを作ろう[TouchDesigner x GLSL入門]

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

1年ぶりの投稿です。

GLSLでこんなエフェクトを作ってみようと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/ddf8e1fb-562b-3efe-4140-edadb62561c2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/4970fd12-75ba-8104-f144-57c15acc3d62.png)

GLSL初めての人への、入門記事を意識して書きました。

僕：そもそもGLSLとは？

> ChatGPT：
GLSL（OpenGL Shading Language）は、OpenGL（Open Graphics Library）というグラフィックスAPIで使用されるシェーダ言語です。
> GLSLは、頂点シェーダー、ジオメトリシェーダー、ピクセルシェーダーなどの異なる種類のシェーダーを記述するためのプログラミング言語として機能します。

ここでは、ピクセルシェーダー（フラグメントシェーダー）としてのGLSLを記述することで画像にエフェクトを適用させます。
いろんな単語があって難しそうですが、シンプルなエフェクトならなんとなくで書けます。

# GLSLをはじめる

TOP Operatorsには GLSL TOP と GLSL Multi TOPがありますが、どちらも同一です（たぶん）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/611d74fa-1c11-debc-e003-5b8e1618a6a6.png)

## 白を出す。（すでに出てる）
GLSL Multiを配置すると次のように3つのオペレータが配置されます。

左下がGLSLコードを記述するText DAT
右下がコードのコンパイル結果を表示するInfo DATです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/99a1f999-cf2e-f3d3-6708-796602e1e4e3.png)

```glsl:op("glslmulti1_pixel").text
out vec4 fragColor;
void main()
{
	// vec4 color = texture(sTD2DInputs[0], vUV.st);
	vec4 color = vec4(1.0);
	fragColor = TDOutputSwizzle(color);
}
```
ポイントはこのコードが**各ピクセルごとに並列で実行される**ということです。
各ピクセルを`vec4(1.0)` で塗っています。
この意味は、RGBAをすべて1にする。すなわち各ピクセルを白に塗ります。
> Aとは透過情報(Alpha Channel)です。1で不透明、0で透明を意味します。

TouchDesignerのGLSLの特徴として、出力変数である`fragColor`に`TDOutputSwizzle()`を通して渡す必要があります。
理由→ https://docs.derivative.ca/Write_a_GLSL_TOP#Output_Swizzle

## 白以外を出す（座標を理解する）
少しだけコードを変更してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/b9780971-6ed3-2c2e-f1d3-10b1684c42e4.png)

```glsl
out vec4 fragColor;
void main()
{
	// vec4 color = texture(sTD2DInputs[0], vUV.st);
	vec4 color = vec4(vUV.st, 0.0, 1.0); //RGBAのうち、Blueは0、Alphaは1.0とする。
	fragColor = TDOutputSwizzle(color);
}
```
vUVには各ピクセルの座標が格納されています。
各ピクセルが並列実行される前に対応した座標をvUVに勝手に代入してくれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/2439726d-0b3d-1fda-aa31-eb554d21c729.png)
vUVは左下を0、右上を1とする座標系です。
そのため、RGにvUV.stを代入することで右に行くほど赤く、上に行くほど緑に、そして右上はそれらがミックスされた黄色として出力されます。


> Tips
> GLSLのswizzleという機能により、
> 次の3つの文は同じ意味を持ち、同じ内容にアクセスできます。
> https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling
> ```glsl
> vUV.xy
> vUV.rg
> vUV.st
> ```

## 画像を入力する（サンプリングを理解する）
コメントを入れ替えて、バナナを入力してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/416e2164-bdec-4948-f255-36e4eb69dff5.png)

```glsl
out vec4 fragColor;
void main()
{
	vec4 color = texture(sTD2DInputs[0], vUV.st);
	// vec4 color = vec4(vUV.st, 0.0, 1.0);
	fragColor = TDOutputSwizzle(color);
}
```
バナナが出力されるようになりました。
`texture(sTD2DInputs[0], vUV.st);`
これは、texuture関数(https://registry.khronos.org/OpenGL-Refpages/gl4/html/texture.xhtml)
を使用して、入力画像`sTD2DInputs[0]`の指定したUV座標`vUV.st`にある色を取得するというコードです。

さらにコードをいじってみます。
```glsl
vec4 color = texture(sTD2DInputs[0], vUV.st);
// ↓次のように変更
vec4 color = texture(sTD2DInputs[0], vUV.st - vec2(0.5,0));
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/50f81449-d6f9-4f63-e19b-2b6b657f2801.png)

X座標(U座標)を-0.5すると、全体的に画像が右にズレました。
これは、あるピクセルを塗るに当たって0.5左にある入力ピクセルの色を採用するためです。
ということで、入力する座標をいい感じにイジると画像にエフェクト変形のエフェクトを掛けられます。



## 外部からのパラメータ入力に対応する。
さきほどの移動量(-0.5)を外からイジれると嬉しいですね！外部パラメータに対応させましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/4888fea7-253c-b0e2-7e8a-fb64d82fb48f.png)

```glsl
uniform vec2 uOffset;
out vec4 fragColor;
void main()
{
	vec4 color = texture(sTD2DInputs[0], vUV.st- uOffset);
	//vec4 color = vec4(1.0);
	fragColor = TDOutputSwizzle(color);
}
```

変更点は次の3つです。
- `uniform vec2 uOffset`を追加した
- GLSL Multi TOPのVectorsページに`uOffset`のパラメータを追加した
- `vec2(0.5,0)`と記述していたところを`uOffset`に変更した

このようにすることで、TD上のパラメータからGLSLに影響を与え、コントロールできます。
このuniformによるパラメータ連携には `float`、`vec2`、`vec3`、`vec4`が対応しています。


大体、これらのコードを使いまわしてシンプルなピクセルシェーダーが作れると思います！

この他にも便利な機能はたくさんあります。↓は公式チュートリアル。
https://docs.derivative.ca/Write_a_GLSL_TOP

> Tips
> TouchDesigner2022で OpenGLからVulkanへ移行しました。これにより、構文エラーによるTouchDesignerのクラッシュが起きやすくなったようです。以下を参照ください。
> https://docs.derivative.ca/Debugging_crashes_triggered_by_GLSL_errors

# Pixel Sortシェーダを作ってみた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/cfebe40b-461d-5076-8e0f-6d1d4da06b64.png)

pixel sortはこの画像みたいなやつです。

## とりあえず作った
つくったネットワークとパラメータ定義はこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/fd18666c-c1d4-ce9a-dd61-aad35181f069.png)

pixel sortの作例を見ると、複数点サンプリングして、ランダムソートなどしている雰囲気がありますが、自分のはだんだん暗くしているだけです。

## コード解説
```glsl
// [me.inputs[0].widht, me.inputs[0].height ]
uniform vec2 uResolution;
uniform float uAttenuation;
out vec4 fragColor;
uniform int uRadius;
void main()
{
	// texelCoord: 正規化されていない座標情報[0~me.inputs[0].wdith-1, 0~me.inputs[0].height-1]
	ivec2 texelCoord = ivec2(gl_FragCoord);
	vec4 maxValue =  vec4(0.0);
	float  distance =  0.0;
	
	
	for(int j = -uRadius; j <= 0; ++j){
		// 今のピクセル座標から jピクセルだけ下の座標を参照->出力は入力画像が上に移動する
		// しかし、ここでjはマイナス値を取るので実際は上の座標を参照->出力は入力画像が下に移動する
		ivec2 neighborCoord = texelCoord - ivec2(0, j);
		//座標系[0~横の解像度-1, 0~縦の解像度-1] -> [0~1, 0~1]に変換し、をピクセル値を取得
		vec4 value = texture(sTD2DInputs[0], (vec2(neighborCoord)+vec2(0.5,0.5))/vec2(uResolution) );
		//今まで取ってきたピクセル値の中で最大輝度を記憶
		if(length(value) > length(maxValue)){
			maxValue = value;
			// 最大輝度を採用したピクセルの座標と出力するピクセルの座標の距離を記憶
			distance = length(vec2(0,j) );
		}
		else{
			maxValue = maxValue;
		}
	} 
	// 最大輝度を採用したピクセルの座標が出力するピクセルの座標と遠ければ暗くする
	maxValue.rgb = maxValue.rgb * (max((uAttenuation - distance)/uAttenuation,0.0));
	fragColor = TDOutputSwizzle(maxValue);
}
```
- `gl_FragCoord`はvUV.stが0から1に正規化されていたのに対して、0から(解像度-1)に正規化して座標を取得できます。

簡単なアルゴリズム解説
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/eebdcc36-31dc-0b54-fe73-295651f7909a.png)
探索は計算中のピクセル対して上から計算します。
クロマキーで切り抜かれた画像の中で最も明るい色を採用し、計算中のピクセルに対して塗りつぶします。
これにより、下に溶け落ちるようなエフェクトが作れました！

動かしてみたらこんな感じ

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">もうちょっとpixel sortっぽくした<br>pixel sortというよりもpixel meltingみある<a href="https://twitter.com/hashtag/TouchDesigner?src=hash&amp;ref_src=twsrc%5Etfw">#TouchDesigner</a><a href="https://twitter.com/hashtag/CitronDailyCoding?src=hash&amp;ref_src=twsrc%5Etfw">#CitronDailyCoding</a> <a href="https://t.co/ufPMxl1RJB">pic.twitter.com/ufPMxl1RJB</a></p>&mdash; シトロン/Citron (@CitronSeason) <a href="https://twitter.com/CitronSeason/status/1731187102334038271?ref_src=twsrc%5Etfw">December 3, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以上です。座標とサンプリングが分かればエフェクトが作れそうですね！
ShaderToyには面白いエフェクトがたくさん共有されています。
ぜひ、遊んでみてください！
https://www.shadertoy.com/results?query=&sort=hot&filter=webcam

# GLSL作例
座標系の変換が少しむずかしいですが、なれるとこんなのがサクッと作れて楽しいです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/d228e707-87d8-8019-5b09-dfefa6b5875a.png)
```glsl
out vec4 fragColor;
uniform vec2 uOffset;
void main()
{
	vec2 myCoord = vUV.st - uOffset;
	float kaitenryou = texture(sTD2DInputs[1], vUV.st).r * 3.141592;
	mat2 kaiten_gyouretu = mat2(
		cos(kaitenryou), -sin(kaitenryou),
		sin(kaitenryou),  cos(kaitenryou)
		);
	vec2 myCoord_rotated = kaiten_gyouretu * myCoord;
	vec2 vUV_rotated = myCoord_rotated + uOffset;
	vec4 color = texture(sTD2DInputs[0], vUV_rotated);
	fragColor = TDOutputSwizzle(color);
}
```