2DガルガンチュアGLSLの数式的な解説

## 発端
Xで見かけたガルガンチュアっぽいグラフィックを再現するGLSLをnukeで実装してみた。
元ポスト: 

https://x.com/XorDev/status/1894105806267711499

このポストに対してみんなが各々のフィールドで再現していて、”お！やってみるか”ってなってやってみた。

https://x.com/tiraoka/status/1895444958980825373

GLSLなので最初Blinkでやってみたが内容は非常にシンプルだったの後でexpressionノード版もポストした。元のGLSLはたった二行で書かれており、nukeで再現するのも本当にそのままで、プラットフォームの変更による工夫やトリックは皆無であった。ただ、二行で書かれているゆえに数式的な中身はとても面白かったのでそれを解説していこうと思う。


## モチベーション
これはまさしくミラー博士の星が周回している軌道中心に存在するガルガンチュアっぽい絵になっていて、シェーダーの中身がそれを意識して書かれているのかそれとも多くの物理法則がそれを用いることが多いからそうなってるのか、にわかのボクには理解できませんしたが、面白いことに”重力は距離の二乗に反比例する”的なことをやっています。実際には距離の二乗に反比例させると”グラデーションの幅が割と急激につくため絵的にグラフィック(アイコン)ぽさが増す”のが理由かどうかはわかりませんが、これは単純に距離に反比例させてます。でも多分そうだと思います。ただ重力レンズをシミュレーションしているわけじゃないです。でもでもやっぱりこれはこれで面白いです。


![Screenshot 2025-03-01 at 13.49.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/60a2c0a3-76ac-4c47-a3a1-7477cb0298a4.png)

```
X = (x*2. - width)/width
Y = (y*2. - height)/heigth
dist = sqrt(pow2(X)+pow2(Y))
rgb  = 0.1 / abs(dist - 0.5 + 0.01 / (X-Y))
```

と、これだけです。

## 解説
前述のように、オリジナルのシェーダー記述としては二行で完結していて、一行目は位置座標を扱いやすい形にしているようです。nukeのexpressionに実装した方の書き方になりますが、X,Yと後々見づらくなるのでlength(p)にあたるものをdistとして定義しました。
追記: ２次元の距離であればhypotが使えます。対象のベクトルが(X,Y)とするとこのベクトルの長さは
```
dist = hypot(X,Y)
```
で記述可能です。これで `dist = sqrt(pow2(X)+pow2(Y))` と同等です。
<br>

#### 一行目
###### ・正規化
X,Yに関してですが、x,yはそれぞれピクセルのx,y座標です。nukeは左下すみ起点です。ちなみに全く今回の話題には関係ないですがcomputer vision等での記述だと左上すみが起点です。これは機械学習モデルをnukeに実装するときかとかにいつもちょっと面倒だなぁって思います。ともあれ、各座標値の2倍からその該当軸を引くと( -> x*2. - width)、これは[-width, width]の範囲にx座標をフィットさせたものになり、それをwidthで割ってるので[-1,1]にフィットさせてます。yも同様です。つまり解像度は何であれxの関数X, yの関数Yは[-1,1]で扱えるものになっています。いわゆる正規化とかって言われる行為ですかね。と言うことで一行目でやってることは座標の正規化で[-1,1]で表現しています。これで画像中心が(0,0)になり扱いやすくなります。

<br>

#### 二行目

###### ・円錐
本題は二行目ですが、これが少しややこしいというか逆数が2回出てきます。オリジナルのコードではlength(p)となっていましたが、これをdistとして扱ってます。これは正規化された座標位置(X,Y)の長さ、つまり画像中心からどれだけ離れてるかのスカラー値です。X,Yをx,yとして、その距離をzとしてポイントクラウド表示するとこうなります。


![Screenshot 2025-03-01 at 14.02.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/a4fec792-44ea-461a-945d-d27441b8f2b8.png)

画像からわかるように円錐です。円錐を高さに垂直な面でカットすると断面が円になりますが、画像だと対角線方向にさらに拡張されてます。なのでその4点が母線方向に伸びて面白い形で表示されています。これは中央から距離にリニアに変化しています。

<br>

###### ・逆数化
で、この二行目がざっくりやっていることとしてはこの距離の逆数をグレースケールで表示させているって感じです。めちゃくちゃ乱暴に言ってますが。そうすると画像中央では0距離なのでinfになり、画像の外の方に行けば行くほど大きい値が分母に来るので、出力される値としては減衰していきます。つまりこれは画像中央からの距離に反比例する値の画像になっています。

![Screenshot 2025-03-01 at 13.57.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/dcb8253b-8814-4012-80ff-5b4b8e317599.png)

スクリーンショットでは1/distをblueチャンネルとして表示させてそれをviewしています。実際には画像中心は0距離の逆数で無限大に行ってしまうので、viewerのexposerを下げてグラデーションがわかりやすいように表示させてます。
それで[-1,1]で正規化した座標の範囲内で距離の反比例を行っているので、画像中央から一番遠いところの値でもその距離は√2でおおよそ1.4です。でこれの逆数なので1/1.4 ≈ 0.7 とかなのでピクセルで表示させるには十分明るいです。これが理由で恐らくオリジナルのコードでは0.1/…. としていると思います。つまり画像中心からの距離の反比例の0.1倍ってことです。それでも中央はいわゆる0距離を分母にするので無限大に行ってしまいます。ともあれそんな感じです。
ちなみに正規化されたX,Y値と中心からの距離をz方向に捉えてポイントクラウドとしてプロットすると視覚的にどういう値なのかって言うのがわかりやすいかもです。

ちなみにこの逆数にしたものをポイントクラウド表示すると、

![Screenshot 2025-03-01 at 14.05.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/3c311c04-389b-4746-be76-3a4658e7c729.png)

となり、反比例で見られるカーブがここにあらわれます。

<br>

###### ・オフセットと絶対値
元のシェーダー記述に戻るとこの距離から-0.5を加えてます。逆数の母数として考えるとややこしいので一旦画像中央からの円状のグラデーションとして考えることにします。そうするとポイントクラウド的にはZ方向の奥の方へと0.5オフセットかけたことになります。なので距離の分布は中心では-0.5となり、今まで距離0.5だったところが0になります。

![Screenshot 2025-03-01 at 14.19.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/8d96dcda-cf76-4b09-a1c1-bbd131d8cf31.png)
*オフセットされているのがわかる*
<br>
なので距離の分布は中心では-0.5となり、今まで距離0.5だったところが0になります。そして画像の外に向かって同じく値が増えていってると言う感じです。立体的に見るとわかりやすいです。難しく言ってしまいましたが、要するに奥方向へ0.5押した感じです。でそれの絶対値をとってるので、0.5距離のところを境にして奥に行っていた分だけ逆に跳ね返って手前にきます。これで距離0.5の部分あたりに明示的な境目を見ることができます。で、これの逆数なので、0.5距離の部分は無限大になりそこから反比例で減衰しています。だんだん、ガルガンチュアっぽくなってきました。

![Screenshot 2025-03-01 at 14.23.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/5c3d36b8-775a-4b8c-8f44-6ac7ebeff519.png)
*絶対値を視覚化*

![Screenshot 2025-03-01 at 14.25.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/d22ae3ae-c65d-4241-a912-97fa2305ba55.png)
*それの逆数化*
<br>
###### ・非対称な感じはどこからきているか？
ガルガンチュアの画像の特徴としては上下対称になっていないというか、上と下で少し曲率が違う曲線が見られて画像的に複雑さを増しています。これを再現するために、ピクセル位置に応じた補正値を与えています。シェーダーの記述では左上と右下で分割されているみたいです。これを再現するためにX-Yを用いています。左下から右上の対角線(y=x)上ではXとYは同じ値になっているのでX-Yは0です。そして右下の隅ではX=1, Y=-1でX-Y=2となり、左上隅ではX=-1, Y=1でX-Y=-2となります。つまり左上から右下にかけて対角線に沿った[-2,2]の線形なグラデーションです。

![Screenshot 2025-03-01 at 14.29.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/17930d28-8a65-4ced-9735-cf6fea282525.png)
![Screenshot 2025-03-01 at 14.29.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/680e1f2a-6188-4b39-97d9-0ff6a3edc6e9.png)
*y=x対角線を堺にして左上エリアと右下エリアに分離している。左上エリアは画像では黒だけど実際には負の値*
<br>
それの逆数なので、y=x対角線より左上エリアでは負の値の反比例なでy=x対角線近傍で大きな負の値で外に行くほど0に近づき影響がなくなります。また、同様に逆のことがy=x対角線より右下エリアで起こっています。つまりy=x対角線近傍で非常に大きな正の値で外に行くほど0に近づきます。
![Screenshot 2025-03-01 at 14.33.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/b1686b0e-6582-4613-a68a-70fd0917893f.png)
*反比例が見て取れる。この値が補正値として加算される。*

![Screenshot 2025-03-01 at 14.32.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/b0c91c58-1e63-4ab5-ab1d-87ac8549b7ff.png)
*同じく左上エリアは画像では黒だけど実際には負の値*
<br>
###### ・最後に補正値を加えて逆数化
このままだと値が大きくてこれの影響が大きくて元の円っぽいグラデーションのルックが崩れるので、これに0.01と言う係数をかけて補正値として足してます。ただy=x対角線近傍では0.01をかけても非常に大きな値を返すことになり、そのためこの補正値の性質が色濃くでます。具体的にはdist-0.5にこの補正値を加えるので、y=x対角線付近では左上エリアも右下エリアもほぼ無限大となります。これは後に逆数化されるので0に近づきます。そこから急激に補正値は減衰されて、dist-0.5の逆数で円状の筋のピークあたりではほぼ補正値の影響が消えます。ただそこに辿り着くまでは左上では負の値が加えられ、右下エリアでは正の値が加えられることになるので、全体的に見て左上エリアと右下エリアで曲率の異なる逆数による円状のグラデーションがあらわれます。


![Screenshot 2025-03-01 at 14.37.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3057501/dc3120b3-1767-4665-82b0-a7d8ba2f5c23.png)
*実際には円軌道状にとても大きい値が現れるのでポイントクラウドにするとわかりづらくなるのを避けるためにここでは適度にクランプしています。*


と言うのがこのガルガンチュアシェーダーの大まかな解説です。