14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust でマンデルブロ集合のズーム・アニメーション

Last updated at Posted at 2017-03-25

この記事のコードに少し修正を施して以下のあらたな記事を書いたので,そちらも見てください。
Rust でマンデルブロ集合のズーム・アニメーション 再び - Qiita

はじめに

Rust の学習はぜんぜん進んでいないが,何か目に見えるものを作ってみたい。

Ruby では速度的につらいものをやろう。そうだ,マンデルブロ集合だ。しかもある点を中心にしてずんずん拡大していく,というアニメーションを作ろう。

「マンデルブロ集合って何だよ」という方は,例えば Wikipedia の「マンデルブロ集合」とか見てください。(書く元気がなかった,ごめん)

方針

Rust のプログラムで各コマの画像を PNG ファイルに書き出し,APNG Assembler(コマンド名は apngasm)というツールを使って APNG(Animated PNG)形式に変換する。

成果物

出来上がった APNG はこちら。

mandel.png

Google Chrome,Internet Explorer,Edge は APNG に対応していないので,Firefox か新しめの Safari でご覧あれ。

「Firefox なのにアニメーションにならん」という方は,たぶんここまでスクロールする前にアニメーション再生が終わって最後のコマが表示されてるんだと思う。で,よく分からないけどページをリロードしても再び再生されたりはしない(される場合もあるみたい?)。
そういうときは画像をローカルに保存して Firefox で開いてみてください。

計 200 コマ,25 秒の映像だ。

1 コマ進むごとに 1/0.87 倍に拡大されるので,最後のコマは最初のコマの約 1 兆倍の倍率になっている。

コード

使うクレートは num と image。

Cargo.toml(依存関係部分)
[dependencies]
num = "*"
image = "0.12.1"

image のおかげで簡単に画像ファイルが生成できる。

書いたコードはこれだけ。

main.rs
extern crate num;
extern crate image;

use std::fs::File;
use std::path::Path;
use std::f64::consts::PI;
use num::complex::Complex;
use num::Float;

fn n2color(n: usize) -> image::Rgb<u8> {
    let c0: [f64; 3] = [ 77.0,  67.0, 178.0];
    let c1: [f64; 3] = [178.0,  74.0, 114.0];
    let cz: [f64; 3] = [255.0, 255.0, 255.0];
    let v = (n as f64 * 0.001).atan() / PI * 2.0;
    let c = if n % 2 == 0 {c0} else {c1};
    let c = [
        (c[0] + (cz[0] - c[0]) * v) as u8,
        (c[1] + (cz[1] - c[1]) * v) as u8,
        (c[2] + (cz[2] - c[2]) * v) as u8
    ];
    image::Rgb(c)
}

fn m(c: Complex<f64>, iter_max: usize) -> usize {
    let mut z: Complex<f64> = Complex::new(0.0, 0.0);
    for i in 0..iter_max {
        z = z * z + c;
        if z.norm_sqr() > 4.0 {
            return i;
        }
    }
    iter_max
}

fn write_image(center: Complex<f64>, span: f64, filename: &str) {
    let black = image::Rgb([0, 0, 0]);
    let mut imgbuf = image::ImageBuffer::new(400, 400);
    let iter_max: usize = 1800;
    let palette: Vec<image::Rgb<u8>> = (0..iter_max).map(|i| n2color(i)).collect();
    let unit: f64 = span / 400 as f64;
    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
        let c = center + Complex::new((x as f64 - 199.5) * unit, (200.5 - y as f64) * unit);
        let n = m(c, iter_max);
        *pixel = if n == iter_max {
                black
            } else {
                palette[n]
            };
    }
    let ref mut fout = File::create(&Path::new(filename)).unwrap();
    let _ = image::ImageRgb8(imgbuf).save(fout, image::PNG);
}

fn main() {
    let center = Complex::new(-0.0938884202514072, 0.959822827920789);
    for i in 0..200 {
        let s = format!("png/foo-{:04}.png", i);
        write_image(center, 4.0 * 0.87.powf(i as f64), &s);
    }
}

いろいろ定数を直書きしてるのがイタいけどまあいいことにして。関数名なんかもテキトーすぎる。

説明

関数 n2color は発散判定までに繰り返した回数 n に対して適当な色を割り当てるもの。

きれいな絵を描くにはこれがポイントで,いろんな人がいろんな着色を試してきた。私はこれというのが思いつかなかったが,試行錯誤して適当に決めた。

関数 m は発散判定。複素数と繰り返し上限を与えて,途中で発散と判定されればそのときの繰り返し数を,そうでなければ上限値を返す。

関数 write_image は,いわば本体で,与えられたパラメーターのもとで一つの PNG 画像をファイルに書き出す。

引数 center は,正方形の画像の中心が複素平面上のどこかなのかを与える。

引数 span は,画像の一辺が,複素平面上でどんな長さかを与える。つまり,複素平面を切り取る大きさのこと。この値が小さいほど複素平面を拡大して見ていることになる。

image クレートの面白いところは,

let mut imgbuf = image::ImageBuffer::new(400, 400);

でイメージバッファーというものを作っておいて,

for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
    *pixel = hogehoge(x, y) // x, y から算出した色を代入
}

のようにイメージバッファー自身にピクセルのイテレートをさせて描画することができる,ってところ。

今回のような場合,ループの中でピクセルの位置 (x, y) が複素平面のどこに当たるかという変換をして,その複素数をもとに色を決定していく。

発散判定する繰り返し回数

write_image 関数の中で,iter_max という変数に 1800 という数を割り当てている。

これは,発散するか収束するかを決める最大繰り返し数。

この値が大きいと計算に時間がかかり,小さいと精密な絵にならない。

速度と精度のバランスをとるためには,倍率に応じてこの値を加減するのがいいはず(倍率を上げたら回数を大きくする)。

でもどうやって決めたらいいか分からなかったので固定値にしちゃった。この 1800 という値は,1 兆倍のときにさほど悪くない絵が描ける値なのだけど,倍率が低いときは無駄に多くの計算をやっていることになる。

並列化は

並列計算(という言い方でいいのだろうか?)が得意な Rust で書いているのに,全く並列化していない。

各コマの画像を計算して書き出す処理は互いに完全に独立しており,並列化が最も易しい部類に入るはず。

それでもよく分かんなかった。いずれ挑戦してみたいと思う。

ほかの記事,ほかのサイト

この記事は実は @kmtoki さんの下記の記事に触発されたもの。

マンデルブロ集合 - Qiita

Ruby でマンデルブロ集合のズーム・アニメーションを作ったんだ。えらい。

それから,ある日,下記のすばらしいページも見つけた。

ぞうさんの何でもノート - [フラクタル] マンデルブロー集合 10の200乗倍への挑戦

これはすごいよ。

Rust でのマンデルブロ集合の計算としては,下記の記事がある。

rust で SIMD -- x86intrinsic を移植した話 - Qiita

並列計算してる! 私にはちんぷんかんぷん。

Rust でマンデルブロ集合を計算する Electron アプリを作ったという記事もあった。

Rust と Electron で GUI アプリケーションを作る - Qiita

(2017-04-06 追記)
@kmtoki さんが新しい記事を書かれました。今度は Rust で。

マンデルブロ集合再び - Qiita

インタラクティブに位置や倍率や精度が変えられるというもの。「あー,このへんもうちょっと拡大して見たいんだけど」とかゆーときにいいね! 精度を変えるとどんなふうに図形が変形するかを見るのも面白い。

さいごに

マンデルブロ集合の全体図を見て おしりたんてい を連想するのは私だけでしょうか。

てゆか,おしりたんていを見るたびに頭のもげたマンデルブロ集合のような気がしてしまう。

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?