クリエイティブコーディングとは
クリエイティブコーディングとは、何らかの表現を創造することを目的としたプログラミングのことです。ソフトウェア開発で行うプログラミングは一般的に何らかの機能を実現することを目的としますが、それとは異なっています。クリエイティブコーディングの愛好家たちによって、ビジュアルアートやサウンドアートなどの作品を制作する活動が行われています。アルゴリズムや数学的なルールに基づいて作品を生成するジェネラティブアートでも、クリエイティブコーディングの手法を使うことがあります。
クリエイティブコーディングのためのツールキットはいろいろ存在しています。たとえば、Processing1(JavaまたはPython)、p5.js2(JavaScript)、openFrameworks3(C++)などが有名です。有名なツールキットを使えば、情報が多くサンプルコードも豊富です。一方で、プログラミングが好きな人ならば、自分の好きな言語でクリエイティブコーディングをしたいとも考えるでしょう。
今回は、Rust言語でクリエイティブコーディングをするためのツールキットであるNannou4 を紹介します。なお、今回の記事ではRust言語の開発環境はすでに整っていることを前提とします。Rustの開発環境の構築については、Rust公式サイト5を参照してください。
はじめてのNannou
まずは実際に動かしてみましょう。cargo
コマンドでプロジェクトを新規作成します。
cargo new nannou-sketch
生成された Cargo.toml
の dependencies
に nannou
を追加します。
[dependencies]
nannou = "0.18.1"
src/main.rs
を次のように編集します。
use nannou::prelude::*;
fn main() {
nannou::sketch(view).run();
}
fn view(app: &App, frame: Frame) {
let draw = app.draw();
draw.background().color(WHITE);
draw.ellipse().color(STEELBLUE);
draw.to_frame(app, &frame).unwrap();
}
cargo run
で実行すると、ウィンドウが表示されて中央に円が描画されます。
コードをざっと見てみましょう。まず、main
関数の中で nannou::sketch
関数を呼んでいます。sketch
関数は view
関数を引数にとり、SketchBuilder
を返します。SketchBuilder
の run
関数を呼ぶことで、ウィンドウが表示されます。
view
関数は App
と Frame
を引数に取ります。view
関数の中で app.draw
関数を呼んで Draw
インスタンスを取得しています。Draw
インスタンスの各種関数で図形描画を指示したあと、to_frame
関数で実際にレンダリングして出力します。
クリエイティブコーディングとしてのキモは、view
関数の中で図形を描画している部分です。draw
の background
関数で背景を白で塗りつぶし、ellipse
関数で青い円を描画しています。この部分をいろいろと書き換えることで、さまざまな図形を描画して自分なりの表現を創造するわけです。
たとえば、view
関数を次のように書き換えると、右上に楕円が表示されます。
fn view(app: &App, frame: Frame) {
let draw = app.draw();
draw.background().color(WHITE);
draw.ellipse()
.color(STEELBLUE)
.w_h(300.0, 200.0)
.x_y(200.0, 150.0);
draw.to_frame(app, &frame).unwrap();
}
なお、コンピューター画面の座標系では左上を原点とすることが多いですが、Nannouはそれとは異なります。Nannouの座標系は、中心を原点としてx軸の正の方向は右方向、y軸の正の方向は上方向です。数学での平面座標系と同様です。
描画に使う関数
基本的な図形の描画に使う関数をいくつか紹介します。
関数名 | 描画する図形 |
---|---|
ellipse() |
楕円 |
rect() |
長方形 |
quad() |
四角形 |
tri() |
三角形 |
line() |
線 |
polyline() |
折れ線 |
polygon() |
多角形 |
これらの関数はどれも Drawing
インスタンスを返します。このインスタンスに対して color
関数で色を指定したり、w_h
関数で幅と高さを指定したり、x_y
関数で位置を指定したりできます。これらの関数は再び Drawing
インスタンスを返すので、メソッドチェインで呼び出すことができます。
次のコードは長方形を描画する例です。実のところ、先ほどの楕円を描画する例とほどんど同じで、ellipse
が rect
に置き換わっただけです。
draw.rect()
.color(STEELBLUE)
.w_h(300.0, 200.0)
.x_y(200.0, 150.0);
また、次のコードは三角形を描画する例です。points
関数で頂点の座標を指定しています。
draw.tri().color(STEELBLUE).points(
pt2(150.0, 200.0),
pt2(-100.0, 300.0),
pt2(-50.0, -150.0),
);
アニメーション
ここまでは静止画を描画してきました。Nannouは画像のアニメーションもできます。
実は view
関数は実行中に繰り返し呼ばれます。view
関数の中で描画する図形を変えることで、アニメーションを実現できます。たとえば、view
関数を次のように書き換えると、円が動くアニメーションを表示できます。アプリケーションを起動してからの経過時間をもとに円の位置を変化させて描画しています。
fn view(app: &App, frame: Frame) {
let draw = app.draw();
let sin = app.time.sin();
let sin2 = (app.time / 2.0).sin();
let window = app.window_rect();
let x = map_range(sin, -1.0, 1.0, window.left(), window.right());
let y = map_range(sin2, -1.0, 1.0, window.bottom(), window.top());
draw.background().color(WHITE);
draw.ellipse().color(STEELBLUE).x_y(x, y);
draw.to_frame(app, &frame).unwrap();
}
app.time
でアプリケーションの起動からの経過時間を取得できます。sin
関数を使って、-1.0
〜 1.0
の値を生成しています。map_range
関数で、-1.0
〜 1.0
の値をウィンドウの端から端の値に変換しています。なお、window_rect
でウィンドウの大きさを取得しています。
動画ファイルへの出力
アニメーションが実現できたら、それを動画ファイルとして保存したくなります。その方法を考えてみます。
ひとつは、Nannouの機能を利用する方法が考えられます。次のような関数を作って view
関数の中から呼び出すことで、各フレームを静止画ファイルとして出力できます。あとは、この静止画ファイルからFFmpeg6 などのツールを使って動画ファイルを生成できます。
fn capture_frame(app: &App, frame: Frame) {
let file_path = app
.project_path()
.unwrap()
.join("frames")
.join(format!("{:04}", frame.nth()))
.with_extension("png");
app.main_window().capture_frame(file_path);
}
ただ、この方法には欠点があります。アニメーションの描画処理とファイルへの出力処理を両方行うため処理が重くなり、場合によってはアニメーションが滑らかに動かなくなります。画面上の表示だけでなく、動画ファイルでも動きがかくついてしまいます。
そのような現象が起こる場合は、別の手段としてウィンドウの画面収録をするとよいでしょう。たとえばmacOSの場合、標準の画面収録機能(⌘+Shift+5)を使う、QuickTime Playerの画面収録機能を使う、サードパーティ製の画面収録アプリを使う、などの方法があります。
Nannou App
ここまでは nannou::sketch
関数を使ってきましたが、もうひとつ nannou::app
関数を使う方法があります。
nannou::app
関数を使う方法は、nannou::sketch
関数よりはコード量が増えますが、Modelが使えるようになります。最初の例を nannou::app
関数を使って書き換えてみましょう。src/main.rs
を次のように編集します。
use nannou::prelude::*;
fn main() {
nannou::app(model).run();
}
struct Model {
_window: window::Id,
bg_color: Srgb<u8>,
fg_color: Srgb<u8>,
}
fn model(app: &App) -> Model {
let _window = app.new_window().view(view).build().unwrap();
Model {
_window,
bg_color: WHITE,
fg_color: STEELBLUE,
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(model.bg_color);
draw.ellipse().color(model.fg_color);
draw.to_frame(app, &frame).unwrap();
}
こちらもコードをざっと見てみましょう。まず、main
関数の中で nannou::app
関数を呼んでいます。app
関数は model
関数を引数にとり、Builder
を返します。Builder
の run
関数を呼ぶことで、ウィンドウが表示されます。
model
関数は Model
構造体を返します。このModelを使うことで、描画に使う値を保持できます。また、model
関数の中でウィンドウを生成しています。nannou::sketch
の場合は明示的に生成する処理を書かなくてもウィンドウが生成されますが、nannou::app
の場合は自分で処理を書きます。生成したウィンドウに対して view
関数を指定しています。
view
関数は Model
を引数に取ります。先ほど生成したModelをここで使用します。このコードでは、描画に使う色をModelで保持して描画時に使用しています。
次に、アニメーションの例を書き換えてみましょう。先ほどのAppの例では、model
関数で Model
を生成して、view
関数で Model
を参照するだけでした。実行中に Model
の値を更新するには update
関数を使います。
use nannou::prelude::*;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
bg_color: Srgb<u8>,
fg_color: Srgb<u8>,
x: f32,
y: f32,
}
fn model(app: &App) -> Model {
let _window = app.new_window().view(view).build().unwrap();
Model {
_window,
bg_color: WHITE,
fg_color: STEELBLUE,
x: 0.0,
y: 0.0,
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
let sin = app.time.sin();
let sin2 = (app.time / 2.0).sin();
let window = app.window_rect();
model.x = map_range(sin, -1.0, 1.0, window.left(), window.right());
model.y = map_range(sin2, -1.0, 1.0, window.bottom(), window.top());
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(model.bg_color);
draw.ellipse().color(model.fg_color).x_y(model.x, model.y);
draw.to_frame(app, &frame).unwrap();
}
main
関数の中が変わっています。nannou::app
関数は Builder
を返しますが、それに対して update
関数を指定してから run
関数を呼んでいます。update
関数は Model
を引数に取ります。この Model
は &mut
で借用しているので、Model
の値を更新できます。
view
関数は &mut
を使っていないため Model
の値を更新できないことに注意してください。これによって、Model
の値を更新する箇所と描画する箇所が自然に分離されます。そのため、ロジックが複雑になっても分かりやすくなります。これは nannou:app
を利用するメリットのひとつです。
簡単な例:ランダムウォーク
ここまでの内容を踏まえた簡単な例として、ランダムウォークを実装してみましょう。ランダムウォークとは、行き先をランダムに決めて動き続ける運動です。物理現象のシミュレーションや、ジェネラティブアートの生成に使われます。
ここでは、一歩の長さが一定で方向を360度の全方向からランダムに決めるランダムウォークを考えます。運動の軌跡をアニメーションで描画する実装の例を次に示します。
use nannou::prelude::*;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
bg_color: Srgb<u8>,
fg_color: Srgb<u8>,
step_length: f32,
start: Point2,
end: Point2,
}
fn model(app: &App) -> Model {
let _window = app.new_window().view(view).build().unwrap();
Model {
_window,
bg_color: WHITE,
fg_color: STEELBLUE,
step_length: 10.0,
start: pt2(0.0, 0.0),
end: pt2(0.0, 0.0),
}
}
fn update(_app: &App, model: &mut Model, _update: Update) {
// 前回の終点を始点にする
model.start = model.end;
// 一歩の移動増分を求める
let angle = random_range(0.0, 2.0 * PI);
let vec = vec2(angle.cos(), angle.sin()) * model.step_length;
// 一歩進んだ先の点を求める
model.end = model.start + vec;
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
// 最初だけ背景を描画
if app.elapsed_frames() == 0 {
draw.background().color(model.bg_color);
}
// ランダムウォークの一歩を描画
draw.line()
.color(model.fg_color)
.start(model.start)
.end(model.end);
draw.to_frame(app, &frame).unwrap();
}
random_range
関数を使って角度をランダムに生成しています。これは実行のたびに異なる結果が得られます。実行したときの例を次に示します。
この例をさらに発展させることも可能です。たとえば、ウィンドウの外に出ていかないようにする、時間経過にあわせて色を変化させる、移動する方向を限定するなどが考えられます。
まとめ
Rust Nannouでクリエイティブコーディングをする方法を紹介しました。より詳しい情報はNannou公式のガイド7やAPIリファレンス8を参照してください。また、NannouのGitHubリポジトリ9にはサンプルコードもあります。
宣伝
本記事は「ゆめみ大技林 '23 (2)」に収録した記事です。他のメンバーの記事もありますので、ぜひご覧ください。