shadertoy でマンデルブロ集合を描いた話

この記事は、[AmusementCreators AdventCalender](http://www.adventar.org/calendars/1029) 通称 ACAC 2日目の記事です。
担当は AmusementCreators 2年のランです。

# shadertoy とは？
[shadertoy](https://www.shadertoy.com/) というサイトがあるのですが、ざっくりいうとオンラインで **shader** を書いてそれを実行して描画するサービスを提供しているサイトです。
**shader** の toy というそのままの意味を取れば、「 **shader** のおもちゃ 」で、**shader** 以外に必要なプログラムを気にせず、気軽に**shader** だけを書くことができるサービスになってます。

みんなもアカウントを作成して、自分の**shader**を書いて保存しよう！

# そもそも shader とは？
私が **shader** に出会ったきっかけは、ゲームを作っている時に、「それぞれのオブジェクトにテクスチャを用意するのが面倒！！計算で画像生成したい！！！」と思ったところにあります。その時に、WebGLに出会って、「ブラウザを使うWebGLを勉強すればクロスプラットフォームな開発ができるのでは…？」となったわけです。**shader**は、そのWebGLの一部の処理にあたり、WebGLを勉強する上でぶち当たった壁でもありました。

「 **shader**で遊べるサイトがあるよ？ 」

なるほど、そんなサイトが……そこで出会ったのが **shadertoy** なわけですが、ここで書くの**shader**というのは、いわゆる「フラグメントシェーダ」と呼ばれるもので、頂点情報をxy座標として、その座標の色を決めるという役割を担っています。簡単に言えば、座標を入力、色を出力とするわけです。

つまるところ、ここでいう**shader**は、座標を入力、色を出力とするアルゴリズムが実装されたプログラムなわけです。

# マンデルブロ集合を書こう
とりあえず書いてみよう。

マンデルブロ集合は、

```math
z_{n+1} = z^{2}_{n} + c\\
z_{0} = 0
```

という漸化式で表したとき、zが発散しない点の集合で表される。

ここでcを、複素平面上の座標として定義すると、その発散模様が面白くなる集合である。

単純にアルゴリズムを考えると、

1. cに座標を代入
2. zの漸化式を進める
3. 発散したときのnを記録
4. nに応じて色を出力

これを各座標ごとにやっていけばいいわけと。

実際にプログラム組むときは、nに上限を与えて、漸化式ループを止める必要がある。

実際に組んでみたものが[こちら](https://www.shadertoy.com/view/4tBSWy)

```glsl
#define PI 3.14159265359
#define speed 10.
#define dim 2.
#define limit 500
#define zoom 6.

//// choose one and comment out
//#define NORMAL
#define ZOOM
//#define MOVE_Z0
//#define MOVE_DIM
//#define JULIA


//// color variation
#define COLORFUL
//#define GRAY
//#define CYAN
//#define WHITE



vec2 cx_pow(in vec2 z, const float n)
{
    float r = length(z);
    float theta = atan(z.y/z.x);
    if (z.x<0.) theta+=PI;
    if (theta<0.) theta+=2.*PI;
    theta -= PI;
	return pow(r,n)*vec2(cos(n*theta),sin(n*theta));
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float scale = .3;
    vec2 offset = vec2(-.34,.0);
    
    #ifdef NORMAL
    scale = .3;
    offset = vec2(-.34,.0);
    #endif
    #ifdef ZOOM
    scale = pow(.3,zoom);
    offset = vec2(-.05,.6805);
    #endif
    #ifdef MOVE_Z0
    scale = .3;
    offset = vec2(-.34,.0);
    #endif
    #ifdef MOVE_DIM
    scale = .3;
    offset = vec2(-.34,.0);
    #endif
    #ifdef JULIA
    scale = .3;
    offset = vec2(-.34,.0);
    #endif
    float t = iGlobalTime;
    t *= speed;
	vec2 c = vec2(
        (fragCoord.x / iResolution.x) * 16. - 8.,
        (fragCoord.y / iResolution.y) * 9. - 4.5
    );
    c *= scale;
    c += offset;
    vec3 col = vec3(0.,0.,0.);
    vec2 z = vec2(0,0);
    #ifdef MOVE_Z0
    float zr = 1.;
    vec2 zim = cx_pow(vec2(0.,1.),mod(t/100.,6.));
    z = zr*zim;
    #endif
    #ifdef JULIA
    float zr = .5;
    vec2 zim = cx_pow(vec2(0.,1.),mod(t/20.,6.));
    z = c;
    c = zr*zim;
    #endif
    const int h = limit;
    int border = h;
    #ifdef ZOOM
    border = int(mod(1.5*t,2.*float(h)));
    #endif
    for (int i = 0; i < h; i++) {
        if(i >= border) {
            break;
        }
        if (length(z) >= 2.) {
            col = vec3(1.-1./(.001+mod(float(i),4.))+.3,1./(.001+mod(float(i),7.))+.3,1.-1./(.001+mod(float(i),3.))+.3);
            #ifdef WHITE
            col = vec3(1.,1.,1.);
            #endif
            #ifdef GRAY
            col = min(1.,1./log(.1*float(i)))*vec3(1.,1.,1.);
            #endif
            #ifdef CYAN
            col = min(1.,1./log(.1*float(i)))*vec3(0.,1.,1.);
            #endif
            break;
        }
        float n = cos(t/3.)+1.;
        n = dim;
        #ifdef MOVE_DIM
        n = t/10.;
        #endif
        z = cx_pow(z,n) + c;
    }
    fragColor = vec4(col,1.);
}
```

と、まあ色を変えたり、漸化式を2乗からn乗に拡張して、zとcをひっくり返してジュリア集合にしたり、それらを#defineのコメントアウトで操作できるようにしたらコードがだらだらと長くなってしまいました。


**結果だけ見ると面白い。**


shadertoyにハマったとき、コード読むのにつまずいたところだけ拾っていきたいと思います。

## 各種使う定数
```glsl
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    ...

    vec2 c = vec2(
        (fragCoord.x / iResolution.x) * 16. - 8.,
        (fragCoord.y / iResolution.y) * 9. - 4.5
    );
```

ここで、cは使用する座標を設定しています。

### vec2 fragCoord
画面上の座標をピクセル単位で取得できます。
例として、 fragCoord.x で　ピクセル単位で x座標を float で取り出すことができます。

### vec2 iResolution
画面の解像度を取得できます。画面を最大化したり、ブラウザのサイズを縮めたりする時に、値が変わります。
これは、fragCoordのxyのそれぞれの最大値にあたるので、fragCoordをiResolutionで割ることで、画面上の座標を[0, 1]にそれぞれ正規化することができます。

```glsl
float t = iGlobalTime;
```

ここでは時間tを設定しています。

### float iGlobalTime
フレームの経過時間を取得できます。内部的にはどうなってるのか詳しく知りませんが、アニメーションができたあとに、tを何倍か、割ったりとか、すればアニメーションスピードを調整することができます。

```glsl
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    ...
    fragColor = vec4(col,1.);
}
```

ここでは、その座標の色を代入しています。

### vec4 fragColor
mainImageの返り値となるのがこのfragColorです。vec4の書き方も独特で、RGBAの値を割り振るのですが、色RGBを先にvec3 colとして設定して、vec4 fragColorに代入する際にはvec4(col, 1.)を与えたりする記法があったりします。わざわざcolの中身を取り出さないあたりが簡潔で、お決まりになっているようです。

## 使える関数
この辺は実際よくわかってないのですが、数学の関数は大体使えるのでやってみてください。色の決定に結構使ってるmod関数ですが、これは実数範囲まで拡張されており、余りが実数ででるようになっています。整数関数の扱いは若干違うと思いますが、floorやceilもあるので、それらを使ってうまいことやりましょう。

(何にも気にしないでlog使ったりしてますが、未定義範囲は0.が返ってくるようです。)

# まとめ

とまあ、あれやこれやするとこんな感じにマンデルブロが映るわけです。

![mdb0.PNG](https://qiita-image-store.s3.amazonaws.com/0/103625/b126730a-8c24-0b88-347e-21535c4d4250.png)

![mdb.PNG](https://qiita-image-store.s3.amazonaws.com/0/103625/30340dbe-917d-afd5-9b8a-7d4d21812483.png)

![mdb4.PNG](https://qiita-image-store.s3.amazonaws.com/0/103625/77150f59-74e2-37a3-5433-e7922d91ea1f.png)

![mdb6.PNG](https://qiita-image-store.s3.amazonaws.com/0/103625/f18e66ef-b266-dc20-38f5-c4b67f77899a.png)

実際に動く場面を見た方が面白いと思うので、[こちら](https://www.shadertoy.com/view/4tBSWy)に飛ぶなり、自分で実装するなりして楽しんでください。

AmusementCreators AdventCalender 2日目　ランの記事でした。