クリエイティブコーディングとは
クリエイティブコーディングとは、何らかの表現を創造することを目的としたプログラミングのことです。ソフトウェア開発で行うプログラミングは一般的に何らかの機能を実現することを目的としますが、それとは異なっています。クリエイティブコーディングの愛好家たちによって、ビジュアルアートやサウンドアートなどの作品を制作する活動が行われています。アルゴリズムや数学的なルールに基づいて作品を生成するジェネラティブアートでも、クリエイティブコーディングの手法を使うことがあります。
クリエイティブコーディングのためのツールキットはいろいろ存在しています。たとえば、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)」に収録した記事です。他のメンバーの記事もありますので、ぜひご覧ください。