知人からGenerative Artというものを聞いて興味を持ったので、私もRustのライブラリnannouを用いて簡単なコードを書いてみました。

いくつか作成において試行錯誤もしたのでその記録を行っておきます。

Generative Art

Wikipediaより定義を持ってくると、以下のようになるようです。

ジェネレーティブアートまたはジェネラティブアート(英: Generative Art)は、コンピュータソフトウェアのアルゴリズムや数学的/機械的/無作為的自律過程によってアルゴリズム的に生成・合成・構築される芸術作品を指す。コンピュータの計算の自由度と計算速度を活かし、自然科学で得られた理論を実行することで、人工と自然の中間のような、統一感を持った有機的な表現を行わせる作品が多い。

大分定義は広いようですが、私のざっくりした理解だとプログラムで一定のアルゴリズム基に生成されるグラフィックのこと、という認識です。

良く利用されるツールとしてはProcessingとそのJavascript版実装であるp5.jsのようです。ビジュアルを容易に出力できるCreative Codingが得意な言語・ライブラリがよく利用されているようです。他にもUnityやPIL(Python)など、画像・映像を出力できるツールであれば表現そのものは可能みたいですね。

Generative Design with p5.jsという書籍のソースコードが公開されているので、こちらを見てみるとどのようなものかイメージがつかみやすいのではないでしょうか。

Rust/nannou

RustにおけるCreative Codingライブラリとして、nannouが公開されています。

GithubのStar数でみると、p5,js(17.3k)には劣っている(3.9k)ものの、しっかりとユーザーはいそうです。

Rustは好きな言語なのですが、仕事ではPythonばかりで腕が錆びついていることもあり、錆落としを兼ねて今回はnannouで試しに書いてみることにしました。

nannouのチュートリアルやガイドは残念ながら途中までしかありませんが、上記p5.jsのソースコードのnannou移植版がgenerative_designに保存されており、さらに自然現象系のサンプル(これも別の書籍をnannou移植している模様)がnature_of_codeに保存されています。

これらを参考にすれば、ある程度はイメージしやすいのではないかと思います。

作成してみた作品

今回は単純に100個の円がランダム性でブレながら進んで壁に反射し続けるというコードを作成してみました。

色々他作品を見ていると、アルファ値を使って軌跡を残したり重ねたりしていると結構雰囲気が出そうに思えたので、反射の軌跡を残してみています。

その結果、以下のように軌跡を残して反射し続ける動作が実現できました。

画像の保存と動画化

命令としてはapp.main_window().capture_frame()でpngなどでフレームを画像化して保存出来ます。。

キー入力時のイベントとして保存するようにすれば、簡単にスクリーンショットを取ることが出来ます。

ただ、動画化は残念ながら機能として提供されてはいません。保存した画像をffmpegやopencvを使って各自で連結することになるかと思います。あるいはスクリーンキャプチャで直接録画するのも手ですね。

以下のソースコードではmを入力してから5秒間のフレームを連続で保存するように記述しましたが、I/Oが重たいのか処理落ちが発生し、満足に保存されませんでした。 おそらく経過秒数ではなくて経過フレーム数にすれば遅くなるものの保存はできるのではないかと思います。

結果的に私は動画の保存はWin+Gでできる録画を使いました。こちらの方が手軽かつ手っ取り早いかと思います。

そして出力mp4をffmpegでffmpeg.exe -i '.\alpha.mp4' -vf scale=320:-1 -r 6 alpha.gifのようにサイズ・フレームレートを変換したのが以下のgifとなります。

きれいな画像・動画を残したい場合はcapture_frame()関数で、手軽に残したいならスクリーンショットやキャプチャで、というのが無難ではないでしょうか。

ビルド時間について

nannouはRustの他ライブラリと同様、プロジェクトの初回ビルドに結構な時間がかかります。

なので作品ごとにプロジェクトを作成するのはあまり推奨できないのではないかと思います。

cargo run --bin xxxcargo run --example xxxなどを使って、同一プロジェクト内で複数の作品を共存させてビルド・実行することが、色々作品を作るには向いているのではないでしょうか。

感想

Generative Artは色々と面白そうな世界に思えたので、今後も勉強しながら作品作ってみたいと思います。

同時にRustの習熟と錆防止もできると思えば一石二鳥ですね。

特にこだわりがないのであればp5.jsやProcessingの方がお手軽かとは思いますけどね。

ソースコード

今回作成したコードを残しておきます。 まだRustやnannouに習熟できてないので参考程度にどうぞ。

use nannou::prelude::*;
use std::path::PathBuf;

const WIDTH: u32 = 720;
const BALL_WIDTH: f32 = 5.0;
const BALL_NUM: usize = 100;
const VELOCITY: f32 = 5.0;
const RAND_ANGLE: f32 = 10.0 * PI / 180.0;   // ランダムでブレる角度[rad]
const ALPHA_RATE: f32 = 0.03;
const MOVIE_SEC: f32 = 5.0;

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

struct Elem {
    point: Point2,
    velocity: Vec2,
}

impl Elem {
    fn new() -> Elem {
        let x: f32 = (random_f32()-0.5) * WIDTH as f32;
        let y: f32 = (random_f32()-0.5) * WIDTH as f32;
        let ang: f32 = random_f32() * 2.0 * PI;

        Elem {
            point: pt2(x, y), 
            velocity: vec2(ang.cos()*VELOCITY, ang.sin()*VELOCITY),
        }
    }

    fn update(&mut self, win: &Rect<f32>) {
        let ang: f32 = (random_f32()-0.5) * RAND_ANGLE;  // 進行方向変更
        self.velocity = self.velocity.rotate(ang);
    
        self.point += self.velocity;
    
        if self.point.x > win.right() || self.point.x < win.left() {
            self.velocity.x = -self.velocity.x;
            self.point.x += self.velocity.x;
        }
    
        if self.point.y > win.top() || self.point.y < win.bottom() {
            self.velocity.y = -self.velocity.y;
            self.point.y += self.velocity.y;
        }
    }

    fn draw(&self, draw: &Draw){
        draw.ellipse()
            .w_h(BALL_WIDTH, BALL_WIDTH)
            .color(BLACK)
            .x_y(self.point.x, self.point.y);
    }
}

struct Model {
    points: Vec<Elem>,
    save_start: Option<f32>,
}

impl Model {
    fn new(len: usize) -> Model {
        let mut pts: Vec<Elem> = vec!();
        for _ in 0..len {
            pts.push(Elem::new());
        }

        Model {
            points: pts,
            save_start: None,
        }
    }

    fn update(&mut self, win: &Rect<f32>) {
        for p in self.points.iter_mut() {
            p.update(win);
        }
    }

    fn draw(&self, draw: &Draw){
        for p in self.points.iter() {
            p.draw(draw);
        }
    }
}

fn model(app: &App) -> Model {
    let _window = app
        .new_window()
        .size(WIDTH, WIDTH)
        .view(view)
        .key_pressed(key_pressed)
        .build()
        .unwrap();
    Model::new(BALL_NUM)
}

fn update(app: &App, model: &mut Model, _update: Update){
    let win_rec = app.window_rect();
    model.update(&win_rec);

    if let Some(t) = model.save_start {
        if app.time - t  < MOVIE_SEC {
            app.main_window().capture_frame(capture_path(app, "mov"));
        } else {
            model.save_start = None;
            println!("end: captured movie frames");
        }
    }
}

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

    if frame.nth() == 0 {
        draw.background().color(WHITE);
    } else {
        // 透明で上書きして軌跡を残す
        draw.rect()
            .wh(app.window_rect().wh())
            .rgba(1.0, 1.0, 1.0, ALPHA_RATE);
    }
    model.draw(&draw);
    draw.to_frame(app, &frame).unwrap();
}

fn key_pressed(app: &App, model: &mut Model, key: Key){
    match key {
        Key::S => {
            app.main_window().capture_frame(capture_path(app, "cap"));
            println!("captured frame");
        }
        Key::M => {
            model.save_start = Some(app.time);
            println!("start: captured movie frames");
        }
        _ => {}
    }
}

fn capture_path(app: & App, prefix: &str) -> PathBuf {
    app.project_path()
        .expect("project path not found")
        .join("./out/")
        .join(app.exe_name().unwrap())
        .join(format!("{}_{:04}", prefix, app.elapsed_frames()))
        .with_extension("png")
}

まとめ

  • Generatice artというものに興味を持った
  • Rust/nannouを使って試しに動かしてみた
  • 今後も勉強して色々と作品を作ってみたい