プログラミングの備忘録

プログラムをつくる過程を残すもの

processingの備忘録 -マンデルブロ集合-

こんにちは。

今回は「マンデルブロ集合を描く」という題です。


マンデルブロ集合とは

マンデルブロ集合は、

$$ z_{n+1} = z_n^{2} + c $$

$$ z_0 = 0 $$

で表される複素数の数列で、$n \to \infty$にしたときに発散しないような複素数$c$の集合のことだそうです。


具体的には、ある複素数$c = a+bi$について先に挙げた式にあてはめて計算していき、

$$ \begin{align} z_0 & = 0 \\ z_1 & = c \\ & = a+bi \\ z_2 & = c^{2} + c \\ & = a^{2} - b^{2} + a + \left( 2ab + b \right)i \\ & \vdots \end{align} $$

$n \to \infty$、つまり$z_\infty$のときの値が無限大にならないようなとき、そのときの複素数$c$がマンデルブロ集合の要素ということになります。


そして、複素数$c$を$a+bi$としたときに、座標上の(a, b)の位置に点を打っていくとマンデルブロ集合の図になります。


プログラム化する

マンデルブロ集合

やっていることは$z_n$の値を2乗して$c$を足すことで次の$z_{n+1}$を計算する、ということを繰り返しているだけなので、プログラム上は、

z = z^2 + c

のような形になります。


さて、マンデルブロ集合を描く上では複素数の扱いが必須になりますが、わざわざ複素数のライブラリやなんかを使わなくてもできます。

実部と虚部に分けて計算すれば良いわけです。
つまり先のマンデルブロ集合の式は、複素数$z_n$を$c_n + d_n i$とすると、

$$ \begin{align} c_n {}_+ {}_1 & = c_n^{2} - d_n^{2} + a \\ d_n {}_+ {}_1 & = 2 c_n d_n + b \end{align} $$

$$ \begin{align} c_0 & = 0 \\ d_0 & = 0 \end{align} $$

となります。


ゴチャゴチャ書いても分かりづらいので、プログラムを書いてみます。

とりあえずは、発散しない部分を黒く塗るような描き方でプログラム化します。

size(500, 500);
background(255);

loadPixels(); //画面の読み込み
for(int i = 0; i < width; i ++){ //画面の全範囲に対して
for(int j = 0; j < height; j ++){

    //描画領域(x:-2~2、y:-2~2)に合わせる
    float x = map(i, 0, width, -2, 2);
    float y = map(j, 0, height, -2, 2);
    
    //複素数cの実部・虚部は座標上の位置になる
    float a = x; 
    float b = y;
  
    int n = 1;
    int n_max = 100; //n -> ∞の「∞」を設定

    int inf = 100; //zの値がどこまでいったら発散したと判断するかを設定
    
    while(n < n_max){ //n_maxまでの間

        //z^2を計算
        float xx = x*x-y*y;
        float yy = 2*x*y;
      
        //z^2にcを足してzを更新
        x = xx + a;
        y = yy + b;
  
        if(sqrt(x*x+y*y) > inf){ //もしzの絶対値が無限大になった(発散した)ら
            break; //ループから抜ける
        }
        
        n++; //nを増やす
        
        if(n == n_max){ //計算回数が最大になった(発散しなかった)ら
            int pos = i + j * width; //画面上の位置を計算
            pixels[pos] = color(0); //色を黒くする
        }
    }
} 
}
updatePixels(); //画面を更新


実行すると、

よく見るマンデルブロ集合の図が描けました。


先程のプログラムでは、nは1~100まで計算するという設定にしていましたが、発散したら全て白く塗っていました。
ですが、もしかしたらn=2で発散しているかもしれないし、n=99まで発散しなかったかもしれません。

ということで、発散する速度によって色を変えてみます。

if(n == n_max){
    int pos = i + j * width;
    pixels[pos] = color(0);
}

の部分を、

int pos = i + j * width;
for(int k = 1; k < n_max; k ++){
    if(n == k){ //発散したときのnによって場合分け
        colorMode(HSB, 360, 100, 100); //カラーモードの変更
        float c = map(k, 1, n_max, 250, 0); //kの値を1~n_maxの範囲から250~0の範囲へ変換
        pixels[pos] = color(c, 80, 80); //nに対応した色に塗る
    }
    if(n == n_max){ //発散しなかったら
        pixels[pos] = color(0, 0, 0); //黒に塗る
    }
}

に変えます。


実行すると、

カラフルになりました。

このコードではカラーモードを変えて、1つの変数の違いによってグラデーション的に色を変えられるようにしています。
カラーモードはデフォルトではRGBですが、HSBというモードに変更しています。
HSBはHue (色相)、Saturation (彩度)、Brightness (明度) によって色を変えるモードで、「色相」は「色相環」における位置を指し、これによって赤から青まで色を変えられます。

よって、nの1~100を色相の250~0(青から赤)に対応させて色を変化させているということになります。


探索できるようにする

さて、これでマンデルブロ集合は描けましたが、やはりフラクタルなので細かいところまで見てみたいものです。
ということで、気になるところに移動して、ズームできるようにしてみます。

今、描画範囲はxが-2~2、yが-2~2の範囲なので、ここをマウスの変位によって動かしたり、マウスホイールによって狭く(広く)することで実現できそうです。
(箱メガネのように、自分の見たい領域を図の中で動かしていく、というような感じです。)


しかし、ズームしていくと荒くなってしまいます。
これはn_maxが小さくて、今後計算すれば発散したであろうところも発散していないことにしてしまっているからです。

解決するには、n_maxを大きくすれば良いです。
(ただし、大きくするとそれだけ計算量も増えるので重くなります。)


あるところまでズームすると、いくらn_maxを大きくしてもドットっぽさが消えなくなってしまいます。
これはコンピュータの計算精度が関係した何かしらが原因だと思われます。


おまけ(ジュリア集合)

似たようなものに、「ジュリア集合」というものもあります。

これは式は同じですが、$c$を固定して$z_0$によって位置を変えていくというものです。
マンデルブロ集合では$z_0$を固定して$c$によって位置を変えていた。)


実装は簡単、a、bの値を適当なものに変えるだけです。

例えばn_max = 1000で、
a = 0.3、b = 0.45なら、

a = -0.77、b = 0.11なら、


map()を使ってマウスの位置によってa、bが変わるようにすれば、いちいち値を変えて実行しなくとも様々なジュリア集合を見ることができます。


まとめ

以上で、マンデルブロ集合についてprocessingで描画することができました。

今回は$z_{n+1} = z_n^{2} + c$としましたが、$z_{n+1} = z_n^{3} + c$などと式を変えててみたり、初期値$z_0$を変えてみたりしても面白いかもしれません。


参考