C#とWPFでマンデルブロ集合を表示してみた

せっかく夏休みなのでc#とWPFでマンデルブロ集合を表示してみました。
ちなみに数学的な理解はそんなにしていませんので数式の意味とか方程式的な解説は他のサイトさんをご確認ください。

ちなみに前回テトリスを作った時の記事はこちら。
takachan.hatenablog.com

成果物

以下プログラムを起動したときの画面です。
色付けは(上手く色が付けられなかったので)グレースケースのみです。

起動するとデフォルトのMainWindowが表示され集合の初期値が表示されます。

色を決定する処理がちょっと変なので画面が超暗いですがご了承下さい。

f:id:Takachan:20150814152958p:plain:h300

そこから赤枠をドンドン拡大していきます。

f:id:Takachan:20150814153008p:plain:h300

f:id:Takachan:20150814153013p:plain:h300

f:id:Takachan:20150814153017p:plain:h300

f:id:Takachan:20150814153022p:plain:h300

c#のdouble型を計算に使っているので、最終的に64bit浮動小数点があふれるまで拡大できます。
有効桁数15-16桁なので100兆倍前後くらいまでは普通に拡大できるのかと思います。

今回も成果物をGitHubに上げてみました。VisualStudio2013と.net 4.5.1 が必要です。
github.com

以下プログラムの説明なのですが、プログラム見たほうが早いかもしれません。

操作方法

このアプリの操作方法はこんな感じです。

画面の任意の場所を左クリック
→ クリックしたところを中心に持ってくる

ホイールアップ
→ 20%拡大

ホイールダウン
→ 20%縮小

丸め誤差をそのままにしているので拡大 → 縮小をしても同じ倍率に戻りません(汗)多分、1%ぐらいずれてます。

画面の任意の場所を右クリック
→ 最大計算回数+5回

拡大すると画面全体が白く霞んでくる(計算結果の下限が上がってくる)ので計算回数を増やして画面の色を調整します。

画面デザイン

WPFでこういった1ドットずつ描画するソフト作るときの最大の問題点は、計算結果を表示する先が無いことなんですよね…。CanvasにRectangleを1ドットずつオブジェクトで表示なんてしたらパフォーマンスが終わります。しかも、WPFってWindowsFormにあったSetPixel関数が存在しません。

そこで、今回は自分でRGP24のバイナリを自分で用意して以下のようにMainWindowの背景画像に貼り付けを行います。

f:id:Takachan:20150814160444p:plain:h300

クラス構成

自分でRGB24画像形式をサポートして計算結果に応じたSetPixel出来るクラスと、マンデルブロー集合の計算を以下のようにアプリに持っています。画面からのイベントを受けて各オブジェクトを操作するコントローラはMainWindowが全て行っています。

f:id:Takachan:20150814163152p:plain:h300

マンデルブロー集合のクラスのフィールドは以下の概念に対応しています。

f:id:Takachan:20150814162007p:plain:h300

SetPixelの実装

ImageクラスにSetPixel無いじゃん!という事で自分で実装します。WPFは高級すぎてSetPixel忘れてしまったのですね。今回は「RGB24」という形式で画像を描画します。

MainWindowクラスでバッファ管理するとコードが混ざるので、上記形式をそのままクラス化します。その名もRGB23ImageBufferクラスです。そのんま。RGB24形式のバッファを管理するクラスです。

バッファーは1次元のbyte配列で持ちます。但し、ユーザーの座標しては(x,y)座標系で来るので間を埋める必要があります。RGS24は3バイトそれぞれに[1byte:R][2byte:G][3byte:B]が格納されそれが繰り返されるようにデータを持ちます。

まずは以下のフィールドを用意します。xとyは(x,y)座標系の最大値で、RawStrideは1次元を2次元に変換するときの定数です。

private byte[] buffer;
private int x;
private int y;
private int rawStride;
public int RawStride { get { return this.rawStride; } }

初期化する場合以下のように指定を行います。rawStrideの計算がポイントですね。

public Rgb24ImageBuffer(int x, int y)
{
    this.x = x;
    this.y = y;
    this.rawStride = (this.x * PixelFormats.Rgb24.BitsPerPixel + 7) / 8;
    this.buffer = new byte[this.rawStride * this.y];
}

SetPixelするときは以下のように指定します。座標を間違えないようにしましょう。

public void SetPixel(int x, int y, Color c)
{
    int xIndex = x * 3;
    int yIndex = y * this.rawStride;
    this.buffer[xIndex + yIndex] = c.R;
    this.buffer[xIndex + yIndex + 1] = c.G;
    this.buffer[xIndex + yIndex + 2] = c.B;
}

WPF画面への描画

あらかじめマンデルブロ集合に以下のように計算を依頼し、その結果を1つずつRGB24ImageBufferクラスへ設定していきます。

int[,] result = this.mandel.CalculateParallel();
for (int j = 0; j < (int)this.mandel.ScreenSizeY; j++)
{
    for (int i = 0; i < (int)this.mandel.ScreenSizeX; i++)
    {
        this.buffer.SetPixel(i, j, ColorPallet.GetColor(mandel.MaxAmount, result[i, j]));
    }
}

で、1ピクセルごとの計算結果を画面に表示するときはマンデルブロクラスから結果を以下のように取得します。

実装側(RGB24ImageBufferクラス内)

public byte[] GetBuffer()
{
    byte[] _tempBuffer = new byte[this.buffer.Length];
    for (int i = 0; i < this.buffer.Length; i++)
    {
        _tempBuffer[i] = this.buffer[i];
    }
    return _tempBuffer;
}

上記のメソッドから取得できるバッファを以下のように画面で使える形に変換します。

利用側(MainWindowクラス内)

var brush = new ImageBrush();
brush.ImageSource =
    BitmapSource.Create((int)this.mandel.ScreenSizeX, (int)this.mandel.ScreenSizeY, 96, 96,
        PixelFormats.Rgb24, null, this.buffer.GetBuffer(), this.buffer.RawStride);

Background = brush;

そうすると冒頭の画面が表示されます。

まとめ

今回は計算自体は結構簡単だったのですが、結果を画面に表示するところが前回のテトリス同様大変でした。WPFみたいに高級言語の上にさらに高級階層が付け足されているので1ピクセルごとの処理となるとわざわざ低階層処理を自分で書く必要があり、その情報もあまり無いので計算より描画系実装のほうが時間がかってしまいました。

あと、数式の解が本当にあってるのか微妙にわかりづらいのが大変でした。全部で20万以上の計算結果があると1つ1つ合ってるか確認すると終わらないので検算に別のプログラムを用意して再計算したりしていました。

本当は色を綺麗につけたりしたいのでもしそれができたら別途アップしようかと思います。