LoginSignup
20
10

More than 5 years have passed since last update.

Rust製クリエイティブコーディング環境 nannou

Last updated at Posted at 2018-08-04

Rustでもクリエイティブコーディング。グラフィク描画、音の取り扱い、GUIを使ったインタラクティブなアプリ等に対応している。

READMEに

The project was started out of a desire for a creative coding framework inspired by Processing, OpenFrameworks and Cinder, but for Rust.

とある通り、ProcessingやOpenFrameworksやCinderの影響を受けているプロジェクト。

Processingにあるような機能はだいたいある(もしくは実装予定がある)。

原点はデフォルトで画面中央であり、変更する手段があるのかどうかはよくわからない。

examples/simple_draw.rsを見ればグラフィックスAPIの雰囲気をつかめる。

Nannou Examplesの手順に従えば各種サンプルを実行できる。

ただし、プロジェクト開始から1年経過しておらず、未完成な点や未調整な点が多数ある。一応動く、程度の状態。tri()が最速でrect()は問題なし、だがellipse()は体感できるほど遅かったりもする。

バイナリが巨大になる(1GB程度)ことやコンパイル時間の長さに対処するため、スケッチをいくつも作って遊ぶような用途ではCargoのワークスペース機能で一括管理するとよいだろうか。

こんな感じに。

playground
├── Cargo.lock
├── Cargo.toml
├── sketch_one
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── sketch_two
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target
    ├── ...

ライフゲームを作る例

nannouにはviewモードappモードがある。

viewモードではメインの関数を一つ定義し、その関数を毎フレーム実行し続ける。

appモードでは、モデル作成関数とイベントハンドリング関数と描画関数を用意し、イベントハンドリングと描画を毎フレーム行っていく。

上述のsimple_draw.rsでは前者を使っている。この例では後者を使う。

main.rs
extern crate nannou;
use nannou::prelude::*;

fn main() {
    nannou::run(model, event, view);
}

main関数でrun関数を呼び出し、modeleventviewを渡す。modelとeventとviewはそれぞれ特定の引数を受け取る関数である

main.rs
#[derive(Copy, Clone, PartialEq)]
enum Cell {
    Live,
    Dead,
}

pub struct Model {
    grid_size: Vector2<f32>,
    cell_size: f32,
    width    : f32,
    height   : f32,
    game     : Vec<Cell>
}

Model構造体を定義する。このModel構造体は、model関数内でインスタンスを作成し、event関数で内容を更新し、view関数から閲覧される。

また、今回はライフゲーム用にCell列挙型も定義している。

続いてmodel関数で準備をする。ここでは最終的にModel構造体のインスタンスを返せればよいが、Model構造体と関係なくても最初の1回だけ実行したい処理もここに一緒に書いている。

main.rs
fn model(app: &App) -> Model {
    use self::Cell::*;

    app.set_loop_mode(LoopMode::rate_fps(12.0));

    let grid_size = vec2(9, 9);
    let cell_size = 20.0;
    let width  = (grid_size.x as f32) * cell_size;
    let height = (grid_size.y as f32) * cell_size;

    app.main_window().set_title("Conway's Game of Life");
    app.main_window()
        .set_inner_size_pixels(width as u32, height as u32);


    Model {
        grid_size, cell_size, width, height,
        game: vec![
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Live, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Live, Dead, Dead, Dead,
            Dead, Dead, Dead, Live, Live, Live, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
            Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
        ],
    }
}

eventを定義する。ライフゲームのルールに則り次世代を作成し、modelに再代入して返している。

main.rs
fn event(_app: &App, mut model: Model, event: Event) -> Model {
    use self::Cell::*;

    match event {
        Event::Update(_update) => {
            let x_len = model.grid_size.x;
            let y_len = model.grid_size.y;
            let next_generation = model.game.iter().enumerate()
                .map(|(i, current_cell)| {
                    let middle = i / x_len;
                    let center = i % x_len;
                    let top    = ((middle+y_len - 1) % y_len) * x_len;
                    let bottom = ((middle + 1) % y_len) * x_len;
                    let left   = (center+x_len - 1) % x_len;
                    let right  = (center + 1) % x_len;

                    let middle = middle * x_len;
                    let around = [
                            top + left,
                            top + center,
                            top + right,
                            middle + left,
                            middle + right,
                            bottom + left,
                            bottom + center,
                            bottom + right,
                        ]
                        .iter()
                        .filter(|around_cell_i| model.game[**around_cell_i] == Live)
                        .count();
                    match around {
                        3 => Live,
                        2 => *current_cell,
                        _ => Dead
                    }
                })
                .collect();
            model.game = next_generation;
        }
        _ => ()
    }

    model
}

最後にview関数。

main.rs
fn view(app: &App, model: &Model, frame: Frame) -> Frame {
    let draw = app.draw();
    draw.background().color(BLACK);

    model.game.iter().enumerate()
    .for_each(|(i, cell)| {
        let color = match cell {
            Cell::Live => WHITE,
            Cell::Dead => return,
        };

        let xi = i % model.grid_size.x;
        let yi = i / model.grid_size.x;
        let x = model.cell_size/2.0 - model.width/2.0
                + (xi as f32) * model.cell_size;
        let y = -model.cell_size/2.0 + model.height/2.0
                - (yi as f32) * model.cell_size;

        draw.rect()
            .x_y(x, y)
            .w_h(model.cell_size, model.cell_size)
            .color(color);
    });

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

これで

$ cargo run

すれば、白いグライダーが右下に向かって進んでいくはずだ。

黒背景に白一色のグライダー

20
10
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
20
10