p5.js, nannou, @types/p5 + Parcelで速度比較を行う
はじめに
creative coding(クリエイティブコーディング)に興味を持ち手を出し始めたのですが、 いろいろ環境を探っていきうちに、Rustでもクリエイティブコーディングのフレームワーク nannou があることに気づきました。 nannouは機能もそろっており、Rustの長所の一つである実行速度を活かせるのかどうかを確認するために、 p5.jsとnannouと@types/p5で速度比較を行いました。@types/p5のコンパイラはParcelになります。
p5.jsと@types/p5は速度はあまり変わらないと予想していますが、念のためどちらも比較対象にしました。 nannouはp5.jsより数倍は速いことに期待します。
実行環境
テストを行った環境は以下の通りです。一昔前のものでスペックはそこまでよくはありません。
- OS: Windows10 professional
- CPU: Intel(R) Core(TM) i5-7600
- メモリ: 16.0 GB
- ストレージ: SSD
- GPU: 内臓グラフィックカード
ブラウザはchrome、Rustはcargo 1.60.0を使用します。
@types/p5 + parcelの設定に関しては前回の通りです。
nannouのインストール
cargo initを行い、Cargo.tomlに以下を付け加えればいいです。 念のため最適化レベルも追加しています。
[profile.release]
opt-level = 3
[dependencies]
nannou = "0.17"
nannouの最新バージョンは2022/5現在0.18.1ですが、 このバージョンでexampleを実行したところコンパイルは成功しましたが実行時エラーが出たので、 バージョンを下げて0.17.1にしました。
速度計測について
速度計測は開始40フレーム目から100フレーム目までにかかった時間を計測します。 フレームレートは60fpsに設定しましたので、速度的に十分で遅延がなければ計測結果は1000ミリ秒となります。 比較のため、かなり重めの描画を比較例として採用します。
p5.jsの時間計測
p5.jsの時間計測に使用したコードのテンプレートは以下になります。
function setup() {
createCanvas(W, H);
background(0);
colorMode(HSB);
frameRate(60);
}
function draw() {
if(frameCount == start){
console.time("test");
}
//ここに描画コード
if(frameCount == end){
console.timeEnd("test");
}
}
@types/p5 + parcelの時間計測
@types/p5の時間計測に使用したコードはp5.jsとほとんど同じなので省略します。
nannouの時間計測
nannouの時間計測に使用したコードは 少し長いので比較例時に示すコード(Gist URL)を参照してください。 簡単に言えばModel構造体に開始時間を保存して、update関数内でapp.elapsed_framesを使用してフレームカウントを確認します。
比較1 パーリンノイズ
一つ目の比較としてパーリンノイズを使用します。 常に同じパーリンノイズインスタンスを使用しますが、フレームとともにノイズスケールを変更します。 パーリンノイズはオクターブで速度は大きく変わりますので、 p5.jsのnoise関数のデフォルトに合わせてnannouのオクターブも 4 に設定します。
コードの流れとしてはp5.jsの例では以下のようになります。 @types/p5の場合もほぼ同じようなコードになります。
let scale_val = Math.min(Math.max(frameCount-start, start), end) / (end-start) * (scale_end - scale_start) + scale_start;
background(0);
for(let x = 0; x < W; ++x) {
for(let y = 0; y < H; ++y) {
let val = noise(x*scale_val, y*scale_val);
set(x, y, color(val*360, 100, 100));
}
}
updatePixels();
nannouでの実装では、まずimage crateのImageBufferに相当するオブジェクトを作成し、そこに画素情報をセットしていきます。 その後、bufferをテクスチャオブジェクトにアップロードしてフレームにロードする流れとなります。 今のところ直接pixelにアクセスする手段がありませんし、p5.jsのpoint関数に相当するものもありません。 また、nannou::noise::Perlin を使用するのではなくp5.jsのnoise関数に相当するのは nannou::noise::Fbm になります。
nannou実装: https://gist.github.com/tk87s/4478695a70c27a8f4674a468176f8cd2#file-noise_test-rs
時間計測結果
時間計測結果の比較は以下のようになります。単位はミリ秒で、値が1000であれば60fpsとなり遅延がありません。
canvasサイズ | p5.js | @types/p5 | nannou |
---|---|---|---|
200x200 | 2685 | 1667 | 1054 |
300x300 | 6848 | 3201 | 2340 |
500x500 | 17426 | 8733 | 6334 |
700x700 | 42109 | 16913 | 12400 |
1000x1000 | (省略) | 34369 | 25248 |
個人的にはtypes/p5が速かったのと、nannouが思ったより速くなかったのが気になります。 types/p5が速かったのはParcelの最適化か何かが機能していたのでしょうか? ということで、nannouが思ったより速くなかった原因を見つけるため続いて比較2を行います。
比較2 ランダム色
上記比較1の結果を受けて、パーリンノイズの取得がパフォーマンスのボトルネックとなっているのではと考え、以下のようにコードを置き換えました。 パーリンノイズ取得部分をrandomに置き換えました。描画したときの見た目としてはカラフルな砂塵のようになります。
background(0);
for(let x = 0; x < W; ++x) {
for(let y = 0; y < H; ++y) {
let val = random();
set(x, y, color(val*360, 100, 100));
}
}
updatePixels();
時間計測結果
結果は以下になります。
canvasサイズ | p5.js | @types/p5 | nannou |
---|---|---|---|
200x200 | 3699 | 1057 | 999 |
300x300 | 6599 | 2392 | 999 |
500x500 | 20545 | 3163 | 999 |
700x700 | (省略) | 12176 | 1263 |
1000x1000 | (省略) | 24503 | 2616 |
予想した通りnannouのパーリンノイズの取得部分がボトルネックとなっていたようです。 これならRustの速度を利点を活かせていると言えるでしょう。
比較3 図形敷き詰め
次に図形を描画する速度を確認するため、図形を大量に描いた時の比較を行いたいと思います。 具体的には直径10の円と1辺10の正方形を交互に敷き詰めたような描画になります。 これをランダムに色を変えながらフレームごとに更新を行います。
background(0);
for(let x = 0; x < W / interval; ++x) {
for(let y = 0; y < H / interval; ++y) {
if ((x+y)%2 == 0) {
fill(random(0,360), 100, 100);
rect(x*interval + 1, y*interval + 1, sz, sz);
}
else{
fill(random(0,360), 100, 100);
ellipse(x*interval + 5, y*interval + 5, sz);
}
}
}
nannouにおいて図形を描くときの注意としては、座標系はp5.jsのWEBGLモードと同じです。 キャンバスの中心の座標が(0,0)と表現され、キャンバスの幅と高さをW,Hとすれば幅の値域は[-W/2, W/2], 高さの値域は[-H/2, H/2]というようになります。
nannou実装: https://gist.github.com/tk87s/4478695a70c27a8f4674a468176f8cd2#file-shape_test-rs
時間計測結果
結果は以下になります。
canvasサイズ | p5.js | @types/p5 | nannou |
---|---|---|---|
200x200 | 1003 | 1003 | 1000 |
300x300 | 1005 | 1002 | 1001 |
500x500 | 1309 | 1049 | 1000 |
700x700 | 2547 | 2360 | 1227 |
1000x1000 | 5070 | 4412 | 2529 |
図形の数が2000個程度までなら60fpsでも遅延はほとんどないようです。 この例でもnannouが一番速い結果となりましたが、倍程度の速度でした。 nannouは比較例2とほぼ同じ速さですので、もしかしたらPCのスペックなど他の要素がパフォーマンスに影響を受けている可能性もあります。
まとめ
それぞれの状況に対して速度パフォーマンスの比較を行いましたが、 nannouは条件に差はあれど全般的に高い速度パフォーマンスを有しているようです。 また、原因は不明ですが@types/p5 + Parcelも意外に速かったのが興味深くもありました。
ただnannouはコンパイルに時間がかかります。 手元の環境では10秒程度かかりますので、速度が欲しい時に使用するなどの使い分けをするのがよさそうです。
また今回は紹介していませんが、nannouはcapture_frameを使用すれば指定フレームの画像を保存できますのでffmpegと組み合わせて動画を作ることも容易です。