Nannou で始める Rust / Creative-Coding

公開:2020/12/21
更新:2020/12/21
15 min読了の目安(約13600字TECH技術記事

この記事は Sansan Advent Calendar 2020 22 日目の記事です 🎄

https://adventar.org/calendars/5250

はじめに

こんにちは。Eight でフロントエンドエンジニアをしている @pvcresin です。[1]

WebAssembly の波に乗り遅れまいと Rust を勉強しているのですが、なかなか仲良くなれません。そこで、何か楽しんで学べる題材はないかと探していたところ、見つけたのが Creative Coding でした。

なぜそこで Creative Coding なのかというと、これには理由があります。私は大学の授業で本格的にプログラミングを学び始めたのですが、当時最も使っていた言語が Processing という言語でした。

https://processing.org/

Processing はビジュアルアートをはじめとする Creative Coding に特化[2]したプログラミング言語(及び統合開発環境)です。文字や図形を描くためのメソッドが豊富に用意されているため、比較的簡単にビジュアルを作ることができ、初心者の私でも楽しみながらプログラミングを学ぶことができました。

この経験から、同じように手軽にビジュアルを作ることができるような環境が Rust にもあれば、学習が捗るのではないかと考えました。そこで出会ったのが Nannou です。
この記事では Nannou に入門し、Rust による Creative Coding を体験してみたいと思います。

Nannou

Nannou logo

https://nannou.cc/

Nannou はオープンソースの Rust 向け Creative Coding ツールキットです。現時点(2020 年 12 月 22 日)で、図形描画や音声出力、OSC、レーザー出力などをサポートしています。

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

とあるように、Processing や openFrameworks 等に影響を受けているようです。
Rust を用いることで、

  • C や C++ に匹敵する実行速度
  • パッケージマネージャによる依存関係の管理
  • クロスコンパイル
  • メモリ安全性

などのメリットを享受できます。特に Creative Coding において、実行速度が速いということはそれだけで表現の幅が広がるので嬉しい点だと思います。

Nannou についての情報

Nannou についての情報は以下のサイトが参考になりそうでした。

公式情報

コミュニティ

その他

中でも、The Nannou Guide のチュートリアルは Rust のインストールから始まり、VS Code の設定、簡単な作品を制作するまでを一通り学ぶことができます。Nannou の基礎的な部分を必要最低限の時間で学ぶことができるので、最初の一歩として最適だと思います。

作例

次にオススメなのが、作例を見て表現方法を学ぶことです。
GitHub のリポジトリ には様々な作例が含まれています。

cargo run --release --example <example_name>

と実行することで、各作例を動かすことができます。

1. examples/

examples/ フォルダには、Nannou の各機能を用いた作例が収録されています。

example1 example2 example3

2. nature_of_code/

The Nature of Code は自然現象をコードでシミュレーションするための方法について書かれた書籍です。

https://natureofcode.com/

元のコードは Processing で書かれていますが、nature_of_code/ フォルダ内には Nannou で書き直したコードが収録されています。

natureofcode1 natureofcode2 natureofcode3

3. generative_design/

Generative Design はコードによって新たなビジュアルを生成する方法について書かれた書籍です。

https://www.amazon.co.jp/dp/4802510977/

元のコードは p5.js(Processing の JavaScript 版)で書かれていますが、generative_design/ フォルダ内には Nannou で書き直したコードが収録されています。

generative_design1 generative_design2 generative_design3

基本を学ぶ

簡単な例をもとに、Nannou の使い方について学んでいきます。なお、私の開発環境は以下の通りです。

  • MacBook Pro (15-inch, 2018)
  • OS: macOS Catalina v10.15.7
  • プロセッサ: 2.9 GHz 6-Core Intel Core i9
  • メモリ: 32 GB 2400 MHz DDR4
  • グラフィックス: Radeon Pro 560X 4 GB, Intel UHD Graphics 630 1536 MB

基本的な構成

空のウィンドウを表示する例から、Nannou アプリケーションの基本的な構成を見ていきます。

// 基本的なアイテムをインポート
use nannou::prelude::*;

// アプリケーションの状態を定義
struct Model {}

// アプリケーションの開始
fn main() {
    nannou::app(model)
        .event(event)
        .simple_window(view)
        .run();
}

// Model のインスタンスを生成
fn model(_app: &App) -> Model {
    Model {}
}

// イベントハンドラ
fn event(_app: &App, _model: &mut Model, _event: Event) {
}

// 画面の描画処理
fn view(_app: &App, _model: &Model, _frame: Frame) {
}

所々、MVC っぽい命名がされています。アプリケーションの状態を Model で管理し、view 関数で画面を構築します。イベントなどでアプリケーション全体に変更を加えたい場合は Model 経由で行います。
Nannou では、アプリケーションの雛形にいくつかのパターンがあるのですが、ここでは代表的なものを載せています。

座標

続いて座標について見ていきます。
Nannou のおける座標は図のように、原点が画面中央に存在しており、右上に行くほど x と y 座標が + に、左下に行くほど - になります。[3]

Coordinates

また、位置を表す単位として、points をつかいます。
points は pixels をスケール係数(画素密度)で割ったものです。
points を使うことで様々なデバイスでおおよそ同様の体験を提供することができます。

Square

例えば、上記の画像のように、600x400 のウィンドウの中心に 100x100 の正方形を表示するコードは次のようになります。

use nannou::prelude::*;

struct Model {}

fn main() {
    // 600x400 のウィンドウを用意
    nannou::app(model).simple_window(view).size(600, 400).run();
}

fn model(_app: &App) -> Model {
    Model {}
}

fn view(app: &App, _model: &Model, frame: Frame) {
    // キャンバスを取得
    let draw = app.draw();

    // 背景色を設定
    draw.background().color(WHITE);

    // 1辺100の正方形を原点に表示
    draw.rect().x_y(0.0, 0.0).w_h(100.0, 100.0).color(BLUE);

    // フレームに書き出し
    draw.to_frame(app, &frame).unwrap();
}

app.draw() で取得したキャンバスに対し、図形を描くメソッドを使って画面を作成していきます。
細かいパラメータなどの設定はメソッドチェーンで行うのが特徴的です。

アニメーション

次はアニメーションに挑戦します。図形を動かすには座標の更新を行う必要があります。

Animation

今回は、アプリケーションが起動してからの秒数を表す app.time を使って、上記の円運動を実装してみます。

use nannou::prelude::*;

struct Model {}

fn main() {
    nannou::app(model).simple_window(view).size(600, 400).run();
}

fn model(_app: &App) -> Model {
    Model {}
}

// アプリケーションが起動している間、ループ
fn view(app: &App, _model: &Model, frame: Frame) {
    let draw = app.draw();

    draw.background().color(WHITE);

    // アプリケーションが起動してからの秒数を t に格納
    let t = app.time;

    // sin、cos を使って円運動を表現
    let center = pt2(t.cos(), t.sin()) * 100.0;

    draw.rect()
        .x_y(center.x, center.y)
        .w_h(100.0, 100.0);
        .color(BLUE)

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

コードはこのようになりました。アプリケーションが起動している間、何回も view 関数が呼ばれるため、その中で座標を更新していきます。アプリケーションが起動してからの時間 t は絶えず増え続けるため、sin, cos と組み合わせることで円運動を実現できます。

イベント

最後に、イベントハンドラについて学びます。今回は、クリックで四角の色が変化するというものを作成します。

Event

クリックに対応するイベントハンドラを設定し、その関数内で Model の中の色の状態を更新します。そして、Model 経由で画面の四角の色が変化するという形になります。では、コードを見ていきましょう。

use nannou::prelude::*;

// 現在設定している色の状態を保持
struct Model {
    color: Rgb8,
}

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

fn model(app: &App) -> Model {
    // イベントハンドラなどを設定
    app.new_window()
        .size(600, 400)
        .mouse_pressed(mouse_pressed)
        .view(view)
        .build()
        .unwrap();

    // 色の初期値を指定してインスタンス生成
    Model {
        color: gen_random_color(),
    }
}

// クリック時のイベントハンドラ
fn mouse_pressed(_app: &App, model: &mut Model, _button: MouseButton) {
    model.color = gen_random_color()
}

// ランダムな色を返す自作関数
fn gen_random_color() -> Rgb8 {
    let r = random::<u8>();
    let g = random::<u8>();
    let b = random::<u8>();
    let random_color = rgb8(r, g, b);
    random_color
}

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

    draw.background().color(WHITE);

    draw.rect()
        // 設定した色で塗りつぶす
        .color(model.color)
        .x_y(0.0, 0.0)
        .w_h(100.0, 100.0);

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

まずはじめに、ウィンドウの設定が model 関数に移動し、simple_window ではなく new_window でウィンドウの初期化を行っています。これは、.mouse_pressed() という関数でクリック(厳密にはボタン押下)のみに反応するハンドラを設定するためです。
イベントハンドラの設定自体は、「基本的な構成」の章で述べたように、main 関数内で .event() しても良いです。ただ、こちらの .event() 関数に渡すハンドラーは mouse_pressed 以外の様々なイベントに対しても呼び出されてしまいます。
そのため、目的の event を見つけるためにパターンマッチを行う必要があり、処理としても無駄が多いと感じたため、.mouse_pressed() を使う形にしています。
そして、Model に color という Rgb8 型の色情報のデータを保持しています。初期値はランダムな色を返す自作関数(gen_random_color)によって決められ、クリック時にも同じようにランダムな色に更新されます。
最後に view 関数で四角を描く時に、Model の color を使ってやるだけです。

Nannou の基本的な部分の説明は以上になります。
あとは触りながら、詰まった時に作例や API ドキュメントを眺めることで学んでいけると思います。

実際に作ってみる

ここまでは文法などを学ぶためにコードを写経してきましたが、最後に自分の好きなように作ってみました。

https://neort.io/art/bvgherk3p9fb02meqtpg

実際に作ってみる

use nannou::image;
use nannou::noise::{MultiFractal, NoiseFn, Fbm};
use nannou::prelude::*;

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

struct Model {
    texture: wgpu::Texture,
    noise: Fbm,
}

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

    let window = app.main_window();
    let win = window.rect();
    let texture = wgpu::TextureBuilder::new()
        .size([win.w() as u32, win.h() as u32])
        .format(wgpu::TextureFormat::Rgba8Unorm)
        .usage(wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::SAMPLED)
        .build(window.swap_chain_device());

    Model {
        texture,
        noise: Fbm::new().set_octaves(5).set_persistence(0.5 as f64),
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    frame.clear(BLACK);

    let win = app.window_rect();
    let noise = &model.noise;

    let noise_x_range = win.w() / 50.0;
    let noise_y_range = win.h() / 50.0;

    let image = image::ImageBuffer::from_fn(win.w() as u32, win.h() as u32, |x, y| {
        let noise_x = map_range(x, 0, win.w() as u32, 0.0, noise_x_range) as f64;
        let noise_y = map_range(y, 0, win.h() as u32, 0.0, noise_y_range) as f64;
        let noise_value = map_range(
                noise.get([noise_x, noise_y, app.time as f64]),
                1.0,
                -1.0,
                0.0,
                std::u8::MAX as f64,
            );
        let n = noise_value as u8;
        if x % 10 == 5 && y % 10 == 5 {
            return nannou::image::Rgba([n, n, 0, std::u8::MAX])
        }
        nannou::image::Rgba([0, 0, 0, std::u8::MAX])
    });

    let flat_samples = image.as_flat_samples();
    model.texture.upload_data(
        app.main_window().swap_chain_device(),
        &mut *frame.command_encoder(),
        &flat_samples.as_slice(),
    );

    let draw = app.draw();
    draw.texture(&model.texture);

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

Noise を使って、image::ImageBuffer 経由でまだら模様のグラフィックを生成しています。う〜ん、、、Rust だけじゃなく Creative Coding の勉強も必要そうです 😓

まとめ

今回は Rust 向けの Creative Coding 用ツールキット、Nannou に入門しました。
以下、良かった点とまだまだな点になります。

良かった点

  • 特に問題もなく、チュートリアル通りに進めるだけですんなり動く
  • GitHub のリポジトリ内に豊富な作例が収録されており、見ているだけでも勉強になる
  • (Release ビルドをすれば)速度面で気になる点は特になかった

まだまだな点

  • ネット上に情報が少ない
  • API ドキュメントにサンプルコードがついていないので読解に少し時間がかかる
  • 初回ビルドが遅い
  • (私だけかもしれませんが)Audio まわりやウィンドウのリサイズ時など、挙動が少し怪しい時があった

Nannou はまだまだ発展途上のプロジェクトですが、WebAssembly や WebAudio を見据えているといった話もあり、Rust とともに非常に今後が楽しみな存在です。
Creative Coding に速度を求めている人や Rust を勉強したい人にはピッタリな題材だと思いました。これからも少しづつ Rust やっていきます 💪

脚注
  1. ピーブイシー・レジンと読みます。(= 塩ビ樹脂) ↩︎

  2. インスタレーションや研究システムのプロトタイピング、デザインの分野でも活用されています。 ↩︎

  3. デカルト座標系というらしいです。 ↩︎