Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

この記事は、AmusementCreators AdventCalender 通称 ACAC 2日目の記事です。
担当は AmusementCreators 2年のランです。

shadertoy とは?

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

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

そもそも shader とは?

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

shaderで遊べるサイトがあるよ? 」

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

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

マンデルブロ集合を書こう

とりあえず書いてみよう。

マンデルブロ集合は、

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

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

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

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

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

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

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

実際に組んでみたものがこちら

#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にハマったとき、コード読むのにつまずいたところだけ拾っていきたいと思います。

各種使う定数

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]にそれぞれ正規化することができます。

float t = iGlobalTime;

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

float iGlobalTime

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

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

mdb.PNG

mdb4.PNG

mdb6.PNG

実際に動く場面を見た方が面白いと思うので、こちらに飛ぶなり、自分で実装するなりして楽しんでください。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした