January 1, 2022

Creative Coding - sketch_211231a

Rust+Nannouで過去作の写経してて気づいたこと色々メモ

モチベ

Nannouに慣れるために以下の過去作と同じアニメーションを作った:

https://openprocessing.org/sketch/545616

つくったもの

コード

use nannou::prelude::*;

const BEAM_MAX_LIFETIME: f32 = 7.0;
const BEAM_MAX_RADIUS_STEP: i32 = 7;
const BEAM_RADIUS_STEP_SIZE: f32 = 50.0;

enum ShapeType {
    Circle,
    Polygon(u32),
}

struct Beam {
    pos: Point2,
    radius: f32,
    lifetime: f32,
    timestep: f32,
    shape_type: ShapeType,
    rot: f32,
    col: Srgb,
    alive: bool,
}

fn hex2rgb(c: i32) -> Rgb {
    let r: i32 = (c >> 16)  & 0x0000FF;
    let g: i32 = (c >> 8)   & 0x0000FF;
    let b: i32 = c          & 0x0000FF;
    rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
}

impl Beam {
    pub fn new() -> Beam {
        Beam {
            pos: pt2(0.0, 0.0),
            radius: 64.0,
            lifetime: 0.0,
            timestep: random_range(0.025, 0.075),
            shape_type: ShapeType::Circle,
            rot: 0.0,
            col: rgb(1.0, 0.0, 0.0),
            alive: true,
        }
    }

    fn generate_beam(pos: Point2, color_palette: &Vec<Rgb>) -> Beam {
        let nvert = random_range(3, 8);
        let shape_type = if nvert == 7 {
            ShapeType::Circle
        } else {
            ShapeType::Polygon(nvert)
        };

        let beam = Beam::new();
        Beam {
            pos,
            shape_type: shape_type,
            col: color_palette[random_range(0, color_palette.len())],
            ..beam
        }
    }

    pub fn compute_radius(t: f32, r: f32) -> f32 {
        let a: f32 = (-t).exp() - (-3.0*t).exp();
        let b: f32 = (3.0.pow(-0.5) - 3.0.pow(-3.0/2.0)) as f32;
        (r * a / b).max(0.0)
    }

    pub fn update(&mut self) {
        self.lifetime = self.lifetime + self.timestep;
        if self.lifetime >= BEAM_MAX_LIFETIME {
            self.alive = false
        }
    }

    pub fn view(&self, draw: &Draw) {
        let radius = Beam::compute_radius(self.lifetime, self.radius);
        match self.shape_type {
            ShapeType::Circle => {
                draw.ellipse()
                    .no_fill()
                    .stroke_color(self.col)
                    .stroke_weight(2.0)
                    .w_h(radius, radius)
                    .xy(self.pos);
            },
            ShapeType::Polygon(n) => {
                let points = (0..n).map(|k| {
                    let t = k as f32 / n as f32 * (2.0 * PI);
                    let point = pt2(
                        radius*t.cos(),
                        radius*t.sin()
                    );
                    point + self.pos
                });
                draw.polygon()
                    .stroke_color(self.col)
                    .stroke_weight(2.0)
                    .no_fill()
                    .rotate(self.rot)
                    .points(points);
            },
        }
    }
}


struct Model {
    _window: window::Id,
    color_palette: Vec<Rgb>,
    beams: Vec<Beam>,
    recording: bool,
}

fn main() {
    nannou::app(model).update(update).run();
}

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

    let mut color_palette: Vec<Rgb> = Vec::new();
    color_palette.push(hex2rgb(0x95ECD7));
    color_palette.push(hex2rgb(0x6BB8Fa));
    color_palette.push(hex2rgb(0xEC889a));
    color_palette.push(hex2rgb(0xFBC52a));
    let color_palette = color_palette;

    let n = 128;
    let beams: Vec<Beam> = (0..n).map(|k| {
        let t = k as f32 / n as f32 * (2.0 * PI);
        let r = random_range(32.0, 128.0);
        let pos = pt2(r*t.cos(), r*t.sin());
        Beam::generate_beam(pos, &color_palette)
    }).collect();

    Model {
        _window,
        color_palette,
        beams,
        recording: false
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let elapsed_frames = app.elapsed_frames() as f32;
    let r_base = (elapsed_frames) / 60.0 % BEAM_MAX_RADIUS_STEP as f32;

    for k in 0..model.beams.len() {
        if model.beams[k].alive {
            model.beams[k].update();
        } else {
            let t = random_range(0.0, 2.0*PI);
            let r = random_range(
                r_base * BEAM_RADIUS_STEP_SIZE as f32,
                (r_base + 1.0) * BEAM_RADIUS_STEP_SIZE as f32
            );
            let pos = pt2(r*t.cos(), r*t.sin());
            model.beams[k] = Beam::generate_beam(pos, &model.color_palette)
        }
    }
}

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

    for beam in &model.beams {
        if beam.alive {
            beam.view(&draw);
        }
    }

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

    if model.recording {
        // Capture the frame!
        let file_path = captured_frame_path(app, &frame);
        app.main_window().capture_frame(file_path);
    }
}

fn event(_app: &App, model: &mut Model, event: WindowEvent) {
    match event {
        KeyPressed(key) => {
            match key {
                Key::S => {
                    model.recording = !model.recording;
                    println!("recording: {}", model.recording);
                },
                _ => {},
            }
        }
        _other => {}
    }
}

fn captured_frame_path(app: &App, frame: &Frame) -> std::path::PathBuf {
    // Create a path that we want to save this frame to.
    app.project_path()
        .expect("failed to locate `project_path`")
        .join("./cap/")
        .join(app.exe_name().unwrap())
        .join(format!("{:04}", frame.nth()))
        .with_extension("png")
}

Sキーを押すとフレームの保存を開始してくれる(もう一度押すとおわる). 動画化するときは下記のffmpegコマンドでできる:

ffmpeg -y -r 60 -i %04d.png -vcodec libx264 -pix_fmt yuv420p -qscale 0 -r 60 output.mp4

Beamのインスタンスは生成と破棄を繰り返してるので一度作ったもののパラメタを初期化して使いまわせば動作がもっと軽量になるかもしれない.

NannouをProcessingやp5.jsと比べた感想

感想

  • drawに更新と描画処理を両方書くテンプレとdrawupdateに分けて書くテンプレが用意されている
    • 今回はdrawupdateにわけた
    • openFrameworksに近い使用感
  • frameCount に相当するものは app.elapsed_frames で得られる
  • 図形のサイズ,位置,色などはメソッドチェーンで指定するので他のフレームワークと比べるとクセ強め
    • 三角関数や指数関数の計算も x.sin() とか x.exp() なのでRust自体にそういう風習があるのかもしれない
  • Rustの文法やマナーを知らないとサンプルコードが全く読めない
    • 宣言したけど使ってない変数の頭に _ をつける
    • 所有権と借用の概念
    • 列挙型の概念
    • スコープは戻り値を持つことができる
    • ifmatch は文ではなく式なので値を返すのにも使う
  • エラーメッセージは親切なのでRustをある程度理解してたら何が悪いのかはすぐわかる
  • スケッチごとにRustのプロジェクトを作ると(cargo new xxxxx),Nannou自体のビルドに時間がかかってディスクも喰うのでひとつのでかいプロジェクトに複数スケッチを作る構成にするのが良さそう
    • src/binの中にsketch_211231a.rsを置いて,cargo run --bin sketch_211231a で実行するかんじ

所有権と借用について

NannouやるならRustのテキストは絶対読んだ方が良い.自分は6章までは読んだ.

The Rust Programming Language 日本語版

所有権と借用は難しいけど,

  • 代入・関数呼び出しで&をつけたら「値を貸し出した」と読む(&mut なら書き換えも許可する)
  • 関数の引数で&をつけたら「値を借りてきた」と読む(&mut なら書き換えも許可してもらえてる)

と考えられれば大丈夫なはず. これを理解してると,以下の宣言:

fn update(app: &App, model: &mut Model, update: Update) {}
fn view(app: &App, model: &Model, frame: Frame) {}

を見たとき

  • update はモデルの更新処理なので model: &mut Model(モデルの書き換えを許可してもらえてる)
  • view は描画をするだけなので model: &Model(モデルの読み取りだけ許可されてる)

というのがすぐわかる.万が一権限を勘違いしていてもコンパイルの時点で弾かれるのでわかりやすい. Nannouはコンピュータに管理されたい人にオススメなクリエイティブコーディング環境だと思う.

おわり

© eqs 2021