Processingとnannouの違いについてのメモ(1)

タイトルに(1)とつけたものの次回があるかわからないけどとりあえずメモ。

(私はProcessingもnannoutも、軽くいじったことがある、くらいのレベルです。ここに書かれていることはあまりアテにしないでください)

nannouとは

nannouはRust製のクリエイティブコーディングフレームワークです。まだまだ発展途上なのでProcessingと比較すると、あれもないこれもない、みたいな感じになるけど、「今フレームワークをゼロからつくるとこうなる」みたいな議論をリアルタイムで見れるのが面白くてなんとなく追っています。

ちょっと触ってみたい、というひとは、チュートリアルが充実しつつあるので読んでみると雰囲気わかると思います。

座標系の違い

Processingでは左上がウィンドウの原点ですが、nannouではウィンドウの中心が原点になっています。なのでたとえば、

    draw.ellipse()
        .x_y(0.0, 0.0)
        .stroke_weight(3.0)
        .color(WHITE);

とかやるとウィンドウの真ん中に白い点が表示されます。(ちなみにx_y()はX座標とY座標をそれぞれ数値で指定するメソッドで、xy()というPoint2(2次元の点を表すオブジェクト)で指定するメソッドもある)

f:id:yutannihilation:20200504170600p:plain:w450

これはこれで便利だったり不便だったりしますが、まあ決めの問題なので慣れれば困らない気はします。(ちなみに、座標ごと回転させるのもできる感じがするけどまだそこまでたどり着いてないのでまた次回...)

width, height

Processingではウィンドウの幅と高さが入ったwidth, heightという変数が用意されていました。 nannouにはそういうのはなくて、ウィンドウをRectオブジェクトとして取得して使います。

let win = app.window_rect();

このRectのメソッドが充実していて、たとえばこのウィンドウの左上に100x100の正方形を描くには、

let r = Rect::from_w_h(100.0, 100.0).top_left_of(win);

という感じのコードで実現できます。このtop_left_of()みたいな位置合わせのメソッドが充実していて便利(c.f.nannou::geom::rect::Rect - Rust)。

ちなみに、Rectオブジェクトの幅、高さ、中心の座標にもそれぞれw()h()x()y()のようなメソッドでアクセスできるので、

    let w = 100.0;
    let h = 100.0;
    let r = Rect::from_x_y_w_h(
        win.x() - win.w() / 2.0 + w / 2.0,
        win.y() + win.h() / 2.0 - h / 2.0,
        100.0,
        100.0,
    );

みたいに書くこともできます(めんどくさいので書かないけど)。

⇣公式の説明はこのへん

setup()

nannouには、Processingでいうsetup()に相当するものがありません。 1度だけしか呼ばれないものはあるのでパラメータのセットアップとかの準備はそこでやればいいですが、画面の描画はview()(実行中、繰り返し呼ばれ続ける)の中でしか許されないので、Processingでやっていたようにsetup()でまず画面を白く塗る、みたいなことができません。 ただまあ、setup()は提供しない、みたいなdesign decisionがあったわけではなく、単にどう提供するのがいいか思いついてないから、というコメントがあるのでそのうち変わるかもしれません。

で、今のところどうするのかというと、↑のissueにあるように

    if app.elapsed_frames() == 1 {
        draw.background().color(WHITE);
    }

という感じでapp.elapsed_frames()に今のフレームが入っているので、それが1のときだけ実行するようにすればいいそうです。..ただ、なぜか私の環境では動かなかったんですけど(表示直後、ウィンドウマネージャーによって勝手にウィンドウがリサイズされて描画がふっとぶ...なぜ...?)

必要なときだけ描画する

Rustは速いとはいえ、毎フレーム同じ画像を描画するのは重すぎるので、更新があったときだけ描画したい、ということもあるでしょう。そういう場合は、描画するかしないかのフラグをmodelに持たせて状態を管理することになります。

struct Model {
    needs_refresh: bool,
}

ただ、ここで「描画したらmodel.needs_refreshfalseにする」というのをview()の中でやることはできません。 view()にできる関数のシグネチャは決まっていて、model&mut Modelではなく&Modelでしか渡せず、変更が加えられないのです。 なので、

  1. modelに、最後に描画しようとしたフレームの番号を記録しておく
  2. そのフレームが過ぎていたらneeds_refreshtrueにする

みたいな処理が必要です。具体的にはこんな感じ。

struct Model {
    current_frame: u64,
    needs_refresh: bool,
}

fn model(app: &App) -> Model {
    app.new_window()
        .size(500, 500)
        .event(event)
        .view(view)
        .build()
        .unwrap();

    Model {
        current_frame: 0,
        needs_refresh: true,
    }
}

fn event(app: &App, model: &mut Model, event: WindowEvent) {
    // ウィンドウがリサイズされるかクリックされたときは needs_refresh を true にする
    match event {
        Resized(_) | MouseReleased(_) => {},
        _ => return,
    }

    model.current_frame = app.elapsed_frames();
    model.needs_refresh = true;
}

fn update(app: &App, model: &mut Model, _update: Update) {
    // 描画すべきフレームがもう過ぎていたら needs_refresh は false に戻す
    if model.current_frame != app.elapsed_frames() {
        model.needs_refresh = false;
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // needs_refresh のときのみ描画
    if !model.needs_refresh {
        return;
    }

    // ...snip...
    draw.to_frame(app, &frame).unwrap();
}

感想

そもそもMVCモデルのプログラミングしたことない、みたいなところもあると思うんですが、考え方にまだまだついていけてない感じがするので修行あるのみ...