2007.11.14.
石立 喬

Visual C++ 2005 Express Edition の易しい使い方(20)

――― Mandelbrot図形の一部をクリックして、対応する条件でJulia図形を描く―――

 Mandelbrot(マンデルブロー)集合(「易しい使い方(12)」で紹介)の図形にすっかりはまってしまって、その親戚関係に当たるJulia集合図形も手がけてみることにした。すでに「易しい使い方(12)」で説明した部分は一部省略されているので、そちらも参照して欲しい。

Julia集合とは
 フランスの数学者Gaston Maurice Juliaが1918年(Benoit Mandelbrotは1924年生まれであるから、Mandelbrotの生まれる前)に論文発表したものであるが、当時は計算時間が非常に長く掛かることから、実際には永らく図形化されていなかった。 Mandelbrot集合が先に注目され、次いでJulia集合も見直された。Julia集合がMandelbrot集合と違うところは、Z=Z2+Cの繰り返し計算において、Mandelbrot集合がZの初期値Z0を常にZ0=0+0iとしてC平面状にCを変化さて図形を描画したのに対し、Julia集合では、Cを固定してZ0を変化させ、Z0平面上に図形を描画する。何回繰り返すとZの絶対値が一定の値に達するか、またはあらかじめ設定した回数では到達しないか(ゼロに収束してしまう場合もある)を調べて、繰り返し回数を色で表現するところはMandelbrot集合と同じである。Julia集合を図形化するには、CのみならずZ0の初期値も外部から与える必要がある。

Julia集合の画像の作成方法
 パソコン画面上の座標を(x,y)としたとき、
  zr(Z0の実数部)=x*step(ピクセル当りの変化分)+z0real(Z0の実数部の中心)
  zi(Z0の虚数部)=y*step(ピクセル当りの変化分)+z0imag(Z0の虚数部の中心)
で与える。
 step(ピクセル当りの変化分)は、xとyに対して同じ値を使用する。stepを大きくすると、画面上でのJulia図形が縮小され、小さくすると部分が拡大される。繰り返し回数を、どのような色の系列で表示するかによって、図形の感じが全く変わったものになる。

高速化の工夫
 Julia図形の描画には、非常に時間がかかる場合がある。なかなか発散しない条件で発散を確認するためには、最大繰り返し回数(count_max)を上げて実行する必要がある。
 ZとCを共に複素数として、Z=Z2+Cを計算するには、Zの実数部をzr、虚数部をzi、Cの実数部をci、Cの虚数部をciとして、
  新しいzr=zr*zr-zi*zi+cr ----------------- (1)
  新しいzi=2*zr*zi+ci -------------------- (2)
  新しいZの絶対値の平方=zr*zr+zi*zi ------ (3)
を多数回計算する必要がある。ただし、(1)式で得た結果の新しいzrをすぐ(2)式に入れることはできないので、一旦tempに保存しておく。これらの計算は、乗算6回、加減算4回である。
 そこで、zr2=zr*zr、zi2=zi*ziをあらかじめ計算しておく方法を用いると、
  zr2=zr*zr  ----------------------- (1)
  zi2=zi*zi ------------------------- (2)
  新しいzr=zr2-zi2+cr ---------------- (3)
  新しいzi=2*zr*zi+ci ---------------- (4)
  新しいZの絶対値の平方=zr2+zi2 ----- (5)
となって、乗算4回、加減算4回となり、乗算回数が2回減る。

プログラムの使用方法
1) プログラムを起動させると、「Mandelbrotモード」になっており、Mandelbrot図形が表示される。
2) Mandelbrot図形上でマウスを移動させると、その位置に対応するCの実数部(creal)、Cの虚数部(ciamg)が右側に表示される。
3) Mandelbrot図形上の希望する位置でマウスをクリックすると、対応するcrealとcimagが設定され、「Juliaモード」に変わる。以後、再び「Mandelbrotモード」に戻ると、Mandelbrot図形上に設定された新しい位置が白い小さな正方形で示される。
4) 表示されたJulia図形は、設定されたcrealとcimagを使用しているが、Z0の中心の実数部(z0real)、Z0の中心の虚数部(z0imag)は共に0.0に初期設定され、ピクセル当りの変化量(step)は0.006に初期設定される。
5) Julia図形上でマウスを移動させると、対応するZ0の実数部(z0real)と虚数部(z0imag)が右側に表示される。
6) Julia図形上の希望する位置でマウスをクリックすると、対応する場所が新しいz0realとz0imagとして設定されて、図形の中央に移動する。左クリックの場合は、stepが1/2になり(図形が拡大される)、右クリックの場合は、stepは変化しない。
7) Julia図形上のクリックを一回だけ元に戻すことができ、その場合は「戻す」ボタンをクリックする。
8) 希望する各データを直接入力したい場合には、画面左上の「条件の設定」メニューをクリックすると「条件の設定」ダイアログボックスが開くので、そこに入力する。「設定」ボタンをクリックすると、ダイアログボックスが閉じる。
9) ダイアログボックスが閉じても、Julia図形のすべてが再描画されないので、「再描画」ボタンをクリックして再描画する。

プログラムの仕組み
 creal、cimagなどの各データは、FormDialogに設定されているものを原則として参照し、Form1.h内ではFormDialog::crealなどを呼び出して使用する。ただし、Julia図形の描画は繰り返し回数が多いので、高速化のためにローカルにコピーしたものを使用する。
◎Form1_Load()
1)creal、cimagなどの各データを初期設定する(FormDialog::crealなどに初期値を入れる)。
2)Mandelbrot図形を作成し、bmap_mandelとして保存する(以後、再作成は行わない)。
◎Form1_Paint()
[Mandelbrotモードの場合]
1) bmap_mandelを描画する。
2) その時に設定されているcrealとcimagの位置をMandelbrot図形上に白い四角で表示する。
[Juliaモードの場合]
1) 各データによるJulia図形を作成して描画する。
2) 各データを下部に表示する。
◎Form1_MouseMove()
図形描画領域内の場合は下記による。
[Mandelbrotモードの場合]
1) それまでに文字が表示されている場合があるので、表示領域を消す。
2) マウス位置から、対応するCの実数部の候補(cr)、Cの虚数部の候補(ci)を計算し、画面右方に表示する。
[Juliaモードの場合]
1) それまでに文字が表示されている場合があるので、表示領域を消す。
2) マウス位置から、対応するZ0の実数部の候補(zr)、Z0の虚数部の候補(zi)を計算し、画面右方に表示する。
図形描画領域外の場合は、それまでに文字が表示されている場合があるので、表示領域を消す。
◎Form1_MouseDown()
[Mandelbrotモードの場合]
1) Mandelbrot図形上のクリックした位置からcrealとcimagを求め、設定する。
2) z0real、z0imag、stepを初期設定する。
3) Juliaモードに切り替える。
[Juliaモードの場合]
1) それまでのz0real、z0imag、stepを、z0real_old、z0imag_old、step_oldに保存する。
2) マウス位置から計算してz0real、z0imagを更新する。stepは、左クリックの時のみ1/2に更新する。
3) buttonUndo(「戻る」ボタン)を有効にする。
再描画する。
◎buttonUndo_Click()
1)z0real_old、z0imag_old、step_oldをz0real、z0imag、stepに代入する。
2)buttonUndoを無効にする。
3)再描画する。
◎countToColor()
これは、「易しい使い方(12)」で用いたものと同じで、Mandelbrot図形の作成に使用する。
◎countToColor1()
これは、(株)翔泳社のCodeZineに「ジュリア集合の色付けを工夫して芸術的なフラクタル図形を描く」として紹介したものと同じで、白と黒を含み、彩度を少し下げてある。

プログラム
 主な変数の役割は次の通り。
  creal -------- 外部から与えるCの実数部の値
  cimag ------- 外部から与えるCの虚数部の値
  z0real ------- Z0(Zの初期値)の実数部で、表示するJulia画像のx座標の中心部に当たる。
  z0imag ------- Z0(Zの初期値)の虚数部で、表示するJulia画像のy座標の中心部に当たる。
  step --------- xまたはyが増加する度にZ0の実数部または虚数部の値が変化する量で、小さい程、表示される画像は拡大される。
  value -------- Zの絶対値の平方で、これ以下であれば発散しないと判断する。
  count_max --- 外部から与える繰り返し回数の上限である。大きすぎると計算時間が長くかかり、小さくするとvalueが4.0に達しない前に計算を打ち切ることになる。
  color_number -- 繰り返し回数を表示するための階調数である。

Form1.h

#include "FormDialog.h"

static int X0=10,Y0=35;   //図形の表示位置
static int X1=10,Y1=450;  //設定条件の表示位置
static int X2=430,Y2=35;  //マウス移動位置表示
static int SIZE=200;

int count_max,color_number;
double z0real_old,z0imag_old,step_old;  //元に戻すための記憶場所
static double MANDEL_CREAL=-0.5;
static double MANDEL_CIMAG=0.0;
static double MANDEL_STEP=0.006;
Rectangle^ rectangle1;
Bitmap^ bmap_mandel;

private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) {

   radioMandel->Checked=true;  //最初はMandelbrotモードなので、radioMandelにチェック
   radioJulia->Checked=false;   //最初はMandelbrotモードなので、radioJuliaはチェックしない
   buttonUndo->Enabled=false;   //クリックによる座標指定がされるまで、buttonUndoは不要
   rectangle1=gcnew Rectangle(X0,Y0,SIZE*2,SIZE*2);  //MandelbrotまたはJulia図形の領域

   //各種データーを初期設定
   FormDialog::creal=0.0;
   FormDialog::cimag=0.0;
   FormDialog::z0real=0.0;
   FormDialog::z0imag=0.0;
   FormDialog::step=0.006;
   FormDialog::count_max=256;
   FormDialog::color_number=64;

   //Mandelbrot図形を作成して、bmap_mandelを用意する
   int MANDEL_COUNT_MAX=1024;
   int MANDEL_COLOR_NUMBER=64;

   int count,x,y;
   double cr,ci,zr,zi,temp,value;
   bmap_mandel=gcnew Bitmap(SIZE*2,SIZE*2);

   for(x=-SIZE;x<SIZE;x++){
      cr=x*MANDEL_STEP+MANDEL_CREAL;
      for(y=-SIZE;y<SIZE;y++){
         ci=y*MANDEL_STEP+MANDEL_CIMAG;
         zr=0.0;
         zi=0.0;
         value=0.0;
         count=0;
         do{
            temp=zr*zr-zi*zi+cr;  //Zの二乗の実数部
            zi=2.0*zr*zi+ci;      //Zの二乗の虚数部
            zr=temp;
            value=zr*zr+zi*zi;    //Zの絶対値の平方
            count++;
            if(count>MANDEL_COUNT_MAX){
               count=-1;
               break;
            }
         }while(value<4.0);
         bmap_mandel->SetPixel(SIZE+x,SIZE-1-y,countToColor(count,MANDEL_COLOR_NUMBER));
      }
   }

}

private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) {

   Graphics^ gr=e->Graphics;
   Bitmap^ bmap=gcnew Bitmap(SIZE*2,SIZE*2);
   String^ string1;

   int count,x,y;
   double creal,cimag,z0real,z0imag,step;
   double zr,zi,zr2,zi2,zreal,temp,value;

   if(radioMandel->Checked){   //Mandelbrotモード
      gr->DrawImage(bmap_mandel,X0,Y0);
      x=(FormDialog::creal-MANDEL_CREAL)/MANDEL_STEP+SIZE;
      y=-(FormDialog::cimag-MANDEL_CIMAG)/MANDEL_STEP+SIZE;
      gr->FillRectangle(Brushes::White,X0-1+x,Y0-1+y,3,3);
   }
   if(radioJulia->Checked){    //Juliaモード
      creal=FormDialog::creal;
      cimag=FormDialog::cimag;
      z0real=FormDialog::z0real;
      z0imag=FormDialog::z0imag;
      step=FormDialog::step;
      count_max=FormDialog::count_max;
      color_number=FormDialog::color_number;

      //Julia図形を作成する
      for(x=-SIZE;x<SIZE;x++){
         zreal=x*step+z0real;
         for(y=-SIZE;y<SIZE;y++){
            zr=zreal;
            zi=y*step+z0imag;
            count=0;
            do{
               r2=zr*zr;
               zi2=zi*zi;
               temp=zr2-zi2+creal;   //Zの二乗の実数部
               zi=2.0*zr*zi+cimag;    //Zの二乗の虚数部
               zr=temp;
               value=zr2+zi2;       //Zの絶対値の平方
               count++;
               if(count>count_max){   //発散しなかった
                  count=-1;
                  break;
               }
            }while(value<4.0);
            bmap->SetPixel(SIZE+x,SIZE-1-y,countToColor1(count,color_number));
         }
      }
      
      gr->DrawImage(bmap,X0,Y0);
      string1=String::Format("Cの実数部の中心={0}、Cの虚数部の中心={1}",creal,cimag);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1);
      string1=String::Format("Z0の実数部の中心={0}、Z0の虚数部の中心={1}",z0real,z0imag);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1+20);
      string1=String::Format("ステップ/ピクセル={0}、最大繰返し回数={1}、表示色の階調{2}",step,count_max,color_number);
      gr->DrawString(string1,Font,Brushes::Black,X1,Y1+40);
   }

}

private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int x,y;
   double cr,cizr,zi;
   String^ string1;

   if(rectangle1->Contains(e->X,e->Y)){
      x=e->X-X0;
      y=e->Y-Y0;
      if(radioMandel->Checked){   //Mandelbrotモード
         cr=(x-SIZE)*MANDEL_STEP+MANDEL_CREAL;
         ci=-(y-SIZE)*MANDEL_STEP+MANDEL_CIMAG;
         g->FillRectangle(Brushes::White,415,95,110,40);
         string1=String::Format("Cの実部={0:F3}",cr);
         g->DrawString(string1,Font,Brushes::Black,420,100);
         string1=String::Format("Cの虚部={0:F3}",ci);
         g->DrawString(string1,Font,Brushes::Black,420,120);
      }
      if(radioJulia->Checked){    //Juliaモード
         zr=(x-SIZE)*FormDialog::step+FormDialog::z0real;
         zi=-(y-SIZE)*FormDialog::step+FormDialog::z0imag;
         g->FillRectangle(Brushes::White,415,95,110,40);
         string1=String::Format("Z0の実部={0:F3}",zr);
         g->DrawString(string1,Font,Brushes::Black,420,100);
         string1=String::Format("Z0の虚部={0:F3}",zi);
         g->DrawString(string1,Font,Brushes::Black,420,120);
      }
   }
   else g->FillRectangle(Brushes::White,415,95,110,40);

}

private: System::Void Form1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {

   Graphics^ g=this->CreateGraphics();

   int x,y;

   if(rectangle1->Contains(e->X,e->Y)){
      x=e->X-X0;
      y=e->Y-Y0;
      if(radioMandel->Checked){   //Mandelbrotモード

         //Mandelbrot図形からcrealとcimagを取り込む
         FormDialog::creal=(x-SIZE)*MANDEL_STEP+MANDEL_CREAL;
         FormDialog::cimag=-(y-SIZE)*MANDEL_STEP+MANDEL_CIMAG;
         radioJulia->Checked=true;
         radioMandel->Checked=false;

         //Julia図形のz0とstepを初期設定
         FormDialog::z0real=0.0;
         FormDialog::z0imag=0.0;
         FormDialog::step=0.006;
      }
      else{                 //Juliaモード

         //それまでのz0real,z0imag,stepを保存する
         z0real_old=FormDialog::z0real;
         z0imag_old=FormDialog::z0imag;
         step_old=FormDialog::step;

         //クリックした場所を新しいz0realとz0imagとする
         FormDialog::z0real=(x-SIZE)*FormDialog::step+FormDialog::z0real;
         FormDialog::z0imag=-(y-SIZE)*FormDialog::step+FormDialog::z0imag;

         //stepを1/2にする(図形を2倍に拡大する)
         FormDialog::step/=2.0;

         //buttonUndoを使用できるようにする
         buttonUndo->Enabled=true;
      }
      Invalidate();
   }

}

private: System::Void menuSet_Click(System::Object^ sender, System::EventArgs^ e) {

   FormDialog^ form_d=gcnew FormDialog();
   form_d->ShowDialog();

}

private: System::Void buttonUndo_Click(System::Object^ sender, System::EventArgs^ e) {

   FormDialog::z0real=z0real_old;
   FormDialog::z0imag=z0imag_old;
   FormDialog::step=step_old;
   buttonUndo->Enabled=false;
   Invalidate();

}

private: System::Void buttonUpdate_Click(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

//カウント数を色に変換するメソッド
private: Color countToColor(int n,int base){

   if(n<0) return Color::Black;  //Zの絶対値が一定値を超えなかった
   else{
      int d=n % base;
      d*=(256/base);
      return HueToRGBColor(d/256.1f);
   }

}

//カウント数を色に変換するメソッド
private: Color countToColor1(int n,int base){

   int r,g,b;
   if(n<0) return Color::Black;   //Zの絶対値が一定値を超えなかった
   int d=(n % base)*256/base;
   int m=d/32;
   switch(m){
      //青→マゼンタ
      case 0: r=63+6*d;      g=63;         b=255;       break;
      //マゼンタ→白
      case 1: r=255;        g=63+6*(d-32);   b=255;       break;
      //白→シアン
      case 2: r=255-6*(d-64);  g=255;        b=255;       break;
      //シアン→緑
      case 3: r=63;         g=255;        b=255-6*(d-96); break;
      //緑→黄
      case 4: r=63+6*(d-128);  g=255;        b=63;        break;
      //黄→赤
      case 5: r=255;        g=255-6*(d-160); b=63;        break;
      //赤→黒
      case 6: r=255-6*(d-192); g=63;         b=63;        break;
      //黒→青
      case 7: r=63;         g=63;         b=63+6*(d-224); break;
   }
   return Color::FromArgb(r,g,b);

}

//色相(0〜1)をColorに変換するメソッド
private: Color HueToRGBColor(float hue){

   int h=(int)(hue*6.0f);   //0から5までの整数
   float f=hue*6.0f-h;    //小数点以下の端数
   int q=(int)(255*(1.0f-f));
   int t=(int)(255*f);
   int r,g,b;

   switch(h){
      case 0:r=255; g=t;   b=0;   break;
      case 1:r=q;   g=255; b=0;   break;
      case 2:r=0;   g=255; b=t;   break;
      case 3:r=0;   g=q;   b=255; break;
      case 4:r=t;   g=0;   b=255; break;
      case 5:r=255; g=0;   b=q;   break;
   }
   return Color::FromArgb(r,g,b);

}

private: System::Void radioMandel_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

private: System::Void radioButton2_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {

   Invalidate();

}

FormDialog.h

private: System::Void FormDialog_Load(System::Object^ sender, System::EventArgs^ e) {

   //各種データをテキストボックスに表示する
   textBox1->Text=creal.ToString();
   textBox2->Text=cimag.ToString();
   textBox3->Text=z0real.ToString();
   textBox4->Text=z0imag.ToString();
   textBox5->Text=step.ToString();
   textBox6->Text=count_max.ToString();
   textBox7->Text=color_number.ToString();

}

private: System::Void buttonOK_Click(System::Object^ sender, System::EventArgs^ e) {

   //テキストボックスの内容を各種データに変換する
   creal=double::Parse(textBox1->Text);
   cimag=double::Parse(textBox2->Text);
   z0real=double::Parse(textBox3->Text);
   z0imag=double::Parse(textBox4->Text);
   step=double::Parse(textBox5->Text);
   count_max=int::Parse(textBox6->Text);
   color_number=int::Parse(textBox7->Text);
   this->Close();

}

private: System::Void buttonCancel_Click(System::Object^ sender, System::EventArgs^ e) {

   this->Close();

}

得られた画面
 図1は、プログラム起動時の画面である。最初は「Mandelbrotモード」になっていて、Mandelbrot図形が表示されている。図形上に、creal=0、cimag=0に相当する点が、白い正方形で表示されている。図形の中心は、MANDEL_CREAL=-0.5、MANDEL_CIMAG=0により設定されているので、点は図形の右寄りで、上下方向には中心にある。
 Mandelbrot図形の右には、Cの実(数)部、Cの虚(数)部が表示されているが、たまたま置かれていたマウスの位置を示していて、特に意味は無い。
 マウスクリックによるJulia図形の変更が行われていないので、「戻す」ボタンは無効になっている。


図1 起動直後の画面


 図2は「条件の設定」ダイアログボックスを示したもので、図3は、それによって得られたJulia図形を示す。ダイアログボックスでデータを指定した場合には、「戻す」ボタンが無効になっている。


図2 キー入力によって各データを設定したダイアログボックス



図3 ダイアログボックスで設定された条件によるJulia図形