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

# はじめに

三年前に以下の記事を書いた。内容はタイトルそのまんま。
[Rust でマンデルブロ集合のズーム・アニメーション - Qiita](https://qiita.com/scivola/items/f730e66d5020aa350b11)

今回の記事は，そのコードをちょこっと修正しただけ。元記事を修正しようかと思ったが，ややこしいので新たな記事を起こした。

## 成果物

こんな APNG アニメーションができました。

![mandelbrot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48101/10b24932-5761-5bda-7b79-0a305c15654b.png)

（2020-07-20 追記）これ，APNG 画像を貼り付けのにアニメーションとして表示されません。下の小節の追記参照。

## 新しい記事を書いたきっかけ

きっかけは，久々に件の記事を見たところ，記事に貼り込んだ APNG アニメーション画像が動かなかったこと。どうやらいつの間にか APNG がただの PNG になっていたっぽい。
原因は分からないが，Qiita さんが PNG を最適化でもして，その際に APNG の最初のフレーム以外が消えてしまったのかも？と想像している。濡れ衣だったらごめんなさい。

（投稿直後の追記）
上の画像はやっぱり静止画として表示される。うーむ。投稿後最初の閲覧ではちゃんと動画になってたのになあ。
調べたら WebP（静止画像フォーマットの一つ）に変換されていた。なんでやねん！
とりあえず以下の URL にアクセスしてもらうと，ちゃんと APNG 動画として表示されるぽい。でもこの URL が恒久的に生きるのかどうかはよく分からない。
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48101/10b24932-5761-5bda-7b79-0a305c15654b.png

## 変更点

大きな変更点としては，元記事で「よく分かんなかった」と書いた並列化に再挑戦し，実現したこと。

小さな点としては，画像サイズを一箇所で定義し，そこだけ変えればよいようにしたことなど。
ちなみに，当初 800×800 px で 200 コマの APNG を作ったらファイルサイズが大きくて Qiita に投稿できなかったので，以下のコードでは 400×400 px にしている。

# コード

```toml:Cargo.toml（依存関係部分）
image = "0.23.7"
num = "0.3.0"
rayon = "1.3.1"
```

```rust:main.rs
use num::complex::Complex;
use num::Float;
use rayon::prelude::*;
use std::f64::consts::PI;

fn n2color(n: usize) -> image::Rgb<u8> {
    const A0: [f64; 3] = [255.0,  32.0,  64.0];
    const A1: [f64; 3] = [210.0,  10.0,  30.0];
    const B0: [f64; 3] = [255.0, 255.0, 255.0];
    const B1: [f64; 3] = [ 60.0,  20.0, 255.0];
    let v = (n as f64 * 0.02).atan() / PI * 2.0;
    let v_1 = 1.0 - v;
    let (a, b) = if n % 2 == 0 { (A0, B0) } else { (A1, B1) };
    image::Rgb([
        (v_1 * a[0] + v * b[0]) as u8,
        (v_1 * a[1] + v * b[1]) as u8,
        (v_1 * a[2] + v * b[2]) as u8,
    ])
}

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(
    size: u32,
    center: Complex<f64>,
    iter_max: usize,
    palette: &Vec<image::Rgb<u8>>,
    span: f64,
    filename: &str,
) {
    let black = image::Rgb([0, 0, 0]);
    let mut imgbuf = image::ImageBuffer::new(size, size);
    let unit: f64 = span / size as f64;
    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
        let c = center
            + Complex::new(
                (x as f64 - (size as f64 - 1.0) * 0.5) * unit,
                ((size as f64 + 1.0) * 0.5 - y as f64) * unit,
            );
        let n = m(c, iter_max);
        *pixel = if n == iter_max { black } else { palette[n] };
    }
    imgbuf.save(filename).unwrap();
}

fn main() {
    const ITER_MAX: usize = 3600;

    let center = Complex::new(-0.0938884202514072, 0.959822827920789);

    let palette: Vec<image::Rgb<u8>> = (0..ITER_MAX).map(|i| n2color(i)).collect();
    (0..200).into_par_iter().for_each(|i| {
        let s = format!("png/foo-{:04}.png", i);
        write_image(
            400,
            center,
            ITER_MAX,
            &palette,
            4.0 * 0.87.powf(i as f64),
            &s,
        )
    })
}
```

これだけ。
事前に `png` というディレクトリーを作成しておく必要がある。

## rayon による並列化

このプログラムは計 200 コマの PNG を書き出すのだが，各コマの計算と画像出力は完全に独立している。ならば並列に実行すれば実行時間が減るのではあるまいか。

貧弱なマシンで CPU のコアが二つしかないが，半分とは言わないまでも，たとえば 2/3 くらいの時間で終了すればうれしいかも。

これには rayon というクレートを使った。
冒頭で

```rust
use rayon::prelude::*;
```

としておき，画像の計算・出力部分を

```rust
(0..200).into_par_iter().for_each(|i| {
  # なんとかかんとか
})
```

と書いたら動いた（ここに至るまでだいぶ試行錯誤した）。
画像が書き出されるとき，確かにファイル名の順にならなかったりするので，並列化できているぽい。

macOS の `time` コマンドで時間を測ったところ，並列化した場合は，ただの `for` でループしたときと比べて，CPU のユーザー時間は 1.5 倍くらいになったが，実時間は半分以下になった（そのときどきで時間は変動）。

え？　半分以下？　コアが 2 個しかないので倍速以上にはなりえない気がするんだが？？？
なんかファイル I/O 関係の事情があるのかな？　←意味がよく分からずテキトーに言ってる

# APNG 化

```
apngasm -o mandelbrot.png png/*.png -l 1 -d 1:20
```

とやって APNG 画像を作成している。

apngasm は macOS なら

```
brew install apngasm
```

でインストールできる。Windows とか Linux でも使える。

`-o` オプションは出力ファイルの名前。`-l 1` は 1 回だけアニメーション表示の意。もし `-l 0` とすると，0 回じゃなくて無限に繰り返す。

`-d` はフレームレート（というか各コマの表示時間）の指定なのだが，指定方法に二通りある。`-d 1:20` のように書くと，各コマの表示時間が $\frac1{20}$ 秒になる。つまり，毎秒 20 コマだ。`-d 2:3` だと $\frac23$ 秒。またコロンを使わずに `-d 200` と書くと 200 ミリ秒の意になる。

# 感想

Ruby ばかり使っている私は，「速い計算」とか「並列処理」に憧れがある。
一方，何十年も前だが，不注意でメモリーを無茶苦茶にするプログラムを書いてしまって以来，「コンパイラこわいよコンパイラ」とトラウマになっている。
Rust は自分の足を撃つのがやりにくくなっているので，「もしや初心者にやさしい？」と思って遊んでみているが，Ruby と全然違う言語ということもあって興味深い。

並列化については，スーパー素人の私でも，「並列化に伴うオーバーヘッドがあるから CPU が 2 個あっても倍速にはならないんだぞ」ということは何となく分かっている。
なので，あんまり期待していなかったのだが，今回のコードについては面白いように高速化したので，ニンマリしている。