自分 advent calendar 2019, 14日目の記事です.
nannou は Rust でのお絵かきライブラリ a creative-coding framework です.ざっくりいえば processing
をイメージすれば良さそう.
Rust なので (i) 書きやすくて (ii) ちゃんと速い のが期待できそう.
ちょっと触ってみようかな…ということで,晩の23時頃まで開催されたアンケート結果をもとに
ふつうの2次元 cellular automata (Conway's game of life) を作ってみます.
公式ページはかっこいいんだけど今のところは guide のチュートリアルが未完成,なので, 冒頭流し見てあとはリポジトリの examples なんかを見ながらとりあえず触る感じで.
最終的なコードは https://github.com/lesguillemets/nnlz にあります.
nannou app の基本構成
チュートリアル にこの辺は書いてある.
基本的には,
- 今描画してる世界の状態の表し方
- (惑星シミュレーションなら各惑星の位置と速度とか,ライフゲームなら各 cell の生き死にとか)
- それを,各ステップでどう更新するか
- 各ステップでそれをどう描画するか
- 必要なら,マウスとかキーボードからの入力に応じて何をするか
…が揃えば,あとは初期状態を渡せば世界が進んでいく,ということになる.
これを踏まえてコードを書くことになる. 基本構造はわかりやすくて,上記を素直に落とし込む形だ
use nannou::prelude::*; // 状態をもっとく構造 struct Model {} fn main() { nannou::app(model) // app 作る .event(event) // イベントハンドラ .simple_window(view) // view .run(); } // &App: ウィンドウとかそういう情報をもってる, // から初期状態を作る fn model(_app: &App) -> Model { Model {} } // マウスの動きや,フレームの進みとかそういうイベントに応じて // 状態の更新 fn event(_app: &App, _model: &mut Model, _event: Event) { } // 状態を各フレームでどう描画するか fn view(_app: &App, _model: &Model, _frame: &Frame) { }
内部的には nannou::app
が Builder
を返すので,それに生えてるメソッドでハンドラとかを設定する形になります.
ということでこれをもとに書いていきましょう.
下準備
普通に
$ cargo new --bin cellular_automata_nannou
Cargo.toml
に依存を書き加えて
[dependencies] nannou = "0.12.0"
で準備は完了.
まずはふつうのセル・オートマトンの方を用意しておきます.以下 src/lib.rs
まず Cell
で各マスを表すことにします.生存してるかどうか教えてくれる is_alive()
と
ご近所さんを数えるときに使うかもしれない as_num()
を生やしておきましょう( c.is_alive().into()
でいいんだけどね)
use nannou::rand::prelude::*; #[derive(Copy, Debug, Clone)] pub enum Cell { Dead, Alive, } impl Cell { pub fn as_num(self) -> u8 { match self { Cell::Dead => 0, Cell::Alive => 1, } } pub fn is_alive(self) -> bool { match self { Cell::Dead => false, Cell::Alive => true, } } }
Model
== 世界の状態を保持しておく構造体,には,世界の縦幅横幅と,Cell
の一次元 vector の形で現在の情報を
もっておきます.それから指定された縦横でランダムにCellをばらまくメソッドを生やしておきます
#[derive(Clone)] pub struct Model { pub world: Vec<Cell>, pub width: u32, pub height: u32, } impl Model { pub fn random(width: u32, height: u32) -> Self { // こうやって使いまわしたほうが効率がよいらしい let mut rng = rand::thread_rng(); let world = (0..width * height) .map(|_| { if rng.gen::<bool>() { Cell::Alive } else { Cell::Dead } }) .collect(); Model { world, width, height, } } }
そして次世代の計算.眠くて急いでるときは効率が悪い頭の悪い方法でも
off-by-one とかを起こしにくい方法でやるのが良い.
端っこはつながるようにしておきます.
x.checked_sub(1).unwrap_or(self.width-1)
では例えば,負の座標になったときに
右端に戻すというのをしている.
impl Model { fn at(&self, x: u32, y: u32) -> Cell { self.world[(y * self.width + x) as usize] } pub fn neighbours_of(&self, loc: u32) -> u8 { // TODO: 累積和で書き直す let mut ns = 0; // 自分を含めた 3x3 マスのなかの生存者を数える let (x, y) = (loc % self.width, loc / self.width); for &neighbour_x in &[ x.checked_sub(1).unwrap_or(self.width - 1), x, (x + 1) % self.width, ] { for &neighbour_y in &[ y.checked_sub(1).unwrap_or(self.height - 1), y, (y + 1) % self.height, ] { ns += self.at(neighbour_x, neighbour_y).as_num(); } } // 自分のカウントを引けばご近所さんの数に ns -= self.world[loc as usize].as_num(); ns } }
それから,ルールを指定できる構造も一応作っておきましょう.
pub struct Rule { /// Cell::Dead の場所は birth_min <= neighbour <= birth_max で生誕 /// Cell::Alive の場所は alive_min <= neighbour <= alive_max で死滅 pub birth_min: u8, pub birth_max: u8, pub alive_min: u8, pub alive_max: u8, }
ここまでで下準備.
お絵かき
まずいくつか定数を定義して,main
を書いておきます
use nannou::prelude::*; use cellular_automata_nannou::*; const CELLSIZE: u32 = 2; // 1マス何pxで描くか const WIDTH_IN_CELLS: u32 = 300; // 横幅,何マスか const HEIGHT_IN_CELLS: u32 = 300; // 縦幅,何マスか const WIDTH_IN_PIX: u32 = WIDTH_IN_CELLS * CELLSIZE; const HEIGHT_IN_PIX: u32 = HEIGHT_IN_CELLS * CELLSIZE; const COLOUR_ALIVE: Rgb<u8> = LIMEGREEN; const COLOUR_DEAD: Rgb<u8> = DARKOLIVEGREEN; const RULE: Rule = Rule { birth_min: 3, birth_max: 3, alive_min: 2, alive_max: 3, }; fn main() { nannou::app(|app: &App| { // window の大きさを設定したいんだが… app.window(app.window_id()) .unwrap() .set_inner_size_pixels(WIDTH_IN_PIX as u32, HEIGHT_IN_PIX as u32); Model::random(WIDTH_IN_CELLS, HEIGHT_IN_CELLS) }) .update(update) .simple_window(view) .run(); }
ウィンドウの大きさをどこで設定したらいいのかが微妙にわからない.とりあえず,App::window(&self, id:ID)
でとれる Window
に Window::set_inner_size_pixel(&self, width: u32, height: u32)
が生えているのでこれを使った.
&mut self
じゃなくていいのが気持ち悪いけど,表示を変えるメッセージを送るだけで struct としての中身は変わらないのでいいのだろう1.
あとは update
と view
を埋めるだけ.前者から見てみましょう
fn update(_app: &App, model: &mut Model, _update: Update) { // 普通のオートマトン let current = model.clone(); for (i, &cell) in current.world.iter().enumerate() { let neighbours = current.neighbours_of(i as u32); if cell.is_alive() { if neighbours < RULE.alive_min || RULE.alive_max < neighbours { model.world[i] = Cell::Dead; } } else { // for dead cells if RULE.birth_min <= neighbours && neighbours <= RULE.birth_max { model.world[i] = Cell::Alive; } } } }
_update
には tick に関する情報とかが入ってるはずだが今は必要ない.とにかくひとコマ勧めるごとに
model.world
を1世代進めればよいのです.
view
については nannou::draw
が対応している.
大事なのはDrawing
.
基本的には App::draw()
で
Draw
を取ってきて,
そこに生えてる rect
とかで上の Drawing
を作る.
Drawing::width
,
Drawing::radians
,
Drawing::color
のように (Self -> setting -> Self
の形の)メソッドが生えてるので,
これらを使って設定をしていくという仕掛けだ.
fn view(app: &App, model: &Model, frame: &Frame) { // Begin drawing let draw = app.draw(); // 死亡のマスの色で埋めて,生きてるマスだけ描画する draw.background().color(COLOUR_DEAD); for (i, _cell) in model.world.iter().enumerate().filter(|(_, c)| c.is_alive()) { draw.rect() .x_y( // (i) 座標系として (0,0) が画面の中央にある // (ii) rect().x_y() も中心からの設定になる-のでこういうのが必要 // 多分設定できるんだけど見つからない ((CELLSIZE * (i as u32 % WIDTH_IN_CELLS) + CELLSIZE / 2) as i64 - WIDTH_IN_PIX as i64 / 2) as f32, ((CELLSIZE * (i as u32 / WIDTH_IN_CELLS) + CELLSIZE / 2) as i64 - HEIGHT_IN_PIX as i64 / 2) as f32, ) .width(CELLSIZE as f32) .height(CELLSIZE as f32) .color(COLOUR_ALIVE); } // Write the result of our drawing to the window's frame. draw.to_frame(app, &frame).unwrap(); }
問題は座標が全部中心を0としてるっぽいことで,上ではとりあえず手動で offset を計算したのだが,多分これ設定できるはずなんですよね…
ともかく
とりあえずこれで cargo run --release
, 結果はキビキビ動く.
(Kazam で綺麗に取れた screencast を上げてみたら結構気持ち悪い感じになった.いかにもmp4の苦手な動画ですねえ…)
まとめ
- 素直な構成で書きやすい
- わりとちゃんと速度出せるんだと思う
- もうちょっど docs 読み込めば若干シンプルにはできそう
-
ここに mut が必要になると,多分マウスで大きさを直接変えたときとかに整合性が取れない↩