• このエントリーをはてなブックマークに追加

Pixi.jsで雪を降らせてWebGLの速さを感じる


こんにちは。HTMLやCSSに触るよりも前にちょっとFlashで遊んでいたフロントエンドエンジニアの上祖師谷です。

さっそくですが、そう、Flash、Flashなんですよ……。
Flash Player の提供が2020年に終了してしまうそうですね。ああ、なんてこと……。
参考: Adobe、「Flash Player」の更新と提供を2020年末で終了 – 窓の杜

Flashの黄昏につきましては、セキュリティやパフォーマンスその他、多くの観点から語られていますので今ここで深く掘り下げは致しませんが、過去のFlash資産が失われてしまうのが純粋に惜しいと思っています。
また、作る側としてもFlashは結構とっつきやすく、アニメーションがわからなくてもプログラミングが初心者でも、なんとなく動くところまでもっていける。そういう簡単さがあんなにも裾野を広げたのかなあと。
ありがとうFlash、青春をありがとう……。

Flash代替としてのcanvas

さて、そのFlashの代替といえばHTML5のcanvas。
HTML5のcanvasタグを書くだけで、プラグインを追加することなくブラウザ上でリッチな表現ができてしまうありがたいAPIです。
canvasでの描画方式にはContext2DとWebGLの2つがありますが、ものすごくかいつまんで解説しますと以下のとおりです。
– Context2D: CPUで描画、2D専用、従来のFlash寄り
– WebGL: GPUで描画、3Dもできる、FlashでいうところのStage3D

Adobe Animate (旧Flash)からの書き出しをする観点では、使いやすいのは前者だと思います。
Animateからcanvasへ書き出せば、Flashから移行する場合でも過去の知見や資産を活かせます。CreateJSを挟むために書き方がActionScriptとそんなに変わらず、アセット管理もAnimate側でできて、さすがAnimateという印象です。
しかしContext2Dで書き出されるのが……これがつらい……。
パフォーマンス優先の実装を目指す場合、ここがどうしてもネックになってしまうのです。GPUを活用しないと描画速度がいまひとつ出ないのです。
一応AnimateからWebGLでの書き出しもできるようで少しだけ手を出してみたことはあるのですが、もう別の環境で開発するのとほとんど変わらず、開発環境としてのAnimateをいまいち活かせずにすぐ諦めてしまった記憶があります。

WebGLでゴリゴリ動かせるライブラリを……Flasherに優しいライブラリを……何か……!

WebGLを楽に触りたいのでPixi.jsを使ってみる

そこでPixi.jsというライブラリのお話になります。
こちらは以前にこのブログで弊社坂本の紹介したthree.jsと同様にcanvas描画系のライブラリです。ぜひ公式サイトをご覧ください。
PixiJS v4
2Dですがけっこうな派手さ!

Pixi.jsの良さは、おもに以下の3点かと思います。

  • 2D特化
  • 動作が高速
  • Flashライクなコーディング

他のライブラリと比較しての強みはやはり動作が高速であること。
高速であればロースペックのスマートフォンでも軽快な動作を期待できますし、Web上の演出は処理落ちがないに越したことはありませんから、ここは重視したいところです。
それでいてFlashのように書けるとあれば学習コストも低め。2D一本槍なのでカメラや光源なども気にする必要なし。このとっつきやすさ、とてもありがたいです。

しかしどうしても3Dは苦手で、3Dっぽさを求めるのであれば演出で疑似3Dのように見せかけるような作り方になってしまいます。Three.jsみたいに3Dバリッバリに動かせるのと比べてしまうと、なんとなく派手にするという点では見劣りしてしまいます。
一見すると地味なのですが、どうせなら割り切ってこの高速さを生かしていきたいですね。

Pixi.jsのハイパフォーマンスを実際に感じる

それでは、どのくらいパフォーマンスが出るのか実際に試してみましょう。

昨年 ICS MEDIA さんで書かれている記事の追っかけのような内容になってしまい恐縮なのですが、こんなときは作りやすくて負荷をいくらでもかけられるパーティクルの出番です。
参考: HTML5 CanvasとWebGLの使い分け – ICS MEDIA

で、秋も深まりつつある今日このごろですし、雪を降らせてみました。
よろしければこちらのサンプルをブラウザで開いてみてください。

やっていることは単純で、あらかじめ画像として書き出しておいたスプライトを毎フレーム生成し、加算合成して下へ動かしつつ、表示範囲から出たらステージから消しているだけです。
単純なスプライトイメージにしているのはそのほうが描画コストが低いのではないかと、Flashでは低かったしなあと思ったためです。パフォーマンス改善の余地はありそうですが、今回はこれを使ってWebGLを使う場合と使わない場合のパフォーマンスを実際に見比べてみましょう。

最初にWebGLを使うかどうかダイアログで確認しますので、どちらかお選びください。そうすると雪っぽい何かが毎秒70個生成されて降りはじめます。その雪的なものが降っているcanvas要素をクリックすると生成量が当初の2倍3倍と増えていきます。現在の倍率と現在ステージにある要素数はcanvas左上に、フレームレートは右上にそれぞれ表示してあります。増やしていけばパフォーマンスが落ちていくのでそこを見ようというわけです。ただし増やしすぎには何卒ご注意下さい、とくに冷却の不十分なスマートフォンでは端末が過熱してしまうおそれがございます(そもそも負荷をかけるのが目的ですので)。

今回、以下ではCore i5 3.1GHz(2コア4スレッド)、 Iris Plus 650 を積んだMacBook Proを使い、Chrome 69で表示テストをしています。

ではまずダイアログでキャンセルを選び、WebGLを使わずに見てみましょう。
WebGLで動いているかどうかはブラウザの開発者ツールでコンソールを見るとわかります。ここが “canvas” であればWebGLが使われておらず、 “WebGL” であれば使われています。

最初は60fpsをキープしていましたが、3回もクリックするともう50fpsくらいまで落ち込んでしまいました。3000オブジェクトにちょっと届かないくらいが限界のようです。コンテンツ性を考えるとパーティクルを出して終わりではないだろうと思うとちょっと物足りない性能です。

今度はWebGLを使ってみましょう。先ほど開いたページをリロードし、はじめのダイアログでOKをクリックしてみてください。環境が対応していればWebGLで描画されます。
最初はもちろん60fps出るわけですが、さあさあ増やしていきましょう。2倍、3倍……2000オブジェクトを超えてもフレームレートが落ちる気配はありません。この時点で既にContext2Dより早いのが十分わかります。
ではもっと増やしてみましょう。5倍いってみましょう。
3500オブジェクト! まだ60fps!

こいつはいい! まだまだ出せるぞ!

調子に乗って雪を増やしていったところ、7000を超えたあたりで60fpsをやや割り込みました。
明らかにガクガクしはじめ50fpsまで落ち込んだのは結局13000オブジェクトあたり。Context2Dで50fpsまで落ち込んだあたりと比較すると画面内に4倍は出せちゃいますね。
ただCPU負荷がかなり高いのが気になるところ。毎フレームのように配列をspliceしているなどの非効率な処理を最適化すればもっといけそうですが、とりあえずロジックはこのままでハードウェア構成を変えてもう少し動かしてみようと思います。

ボトルネック探し1: ハードウェアをよいものに変える

まずは開発機の構成を変えてみます。

・Ryzen 1600X (3.2GHzに固定), DDR4-2800 16GB, Radeon RX 480 8GB のデスクトップPC

性能的にもう、全面的に盛ってみました。
Context2Dでは先ほど同様に3000オブジェクトくらいでもう50fps程度に落ち込んでしまいました。JSはシングルスレッドですので、CPUのコアあたりの性能が同じくらいであれば物理コア数が2個であれ6個であれ大きな差は出ませんね。
対するWebGLでは……なんと7000個くらいで60FPSを少し割り込んでしまいました。あれ? さっきと変わってないような?
タスクマネージャで見てみるとGPUの負荷がたいしたことないようで、性能を持て余しているはずなのですが……やや腑に落ちない結果となってしまいました。
11700オブジェクトくらいまで50FPS以上をキープしていましたので、ここも同じということに。

・上のPCのグラフィックカードを Radeon Vega 56 8GB へとさらに盛る

そこそこの贅沢品を投入してみました。
CPUはそのままですのでContext2Dでのテストは割愛します。
そしてWebGLでは13800オブジェクトまで60FPSをキープ。さすがにここまで盛れば影響は出るようですが、こちらもGPUは持て余しているようですので、違いがあるとすればVRAM帯域でしょうか? 一応HBM2の広帯域の恩恵を受けているのかもしれません、が……。
ええ、それよりもですね、スプライト描画の負荷が低すぎてCPUのほうがボトルネックになってしまっているような! 恥ずかしい!
つまりWebGLの本気はきっとこんなものではないのでしょう。

ボトルネック探し2: 配列操作処理の変更

さきほど「毎フレームのように配列をspliceしている」と申しましたが、実際Chrome開発者ツールのPerformanceタブで計測してみましたらそれを含むメソッドが比較的時間を食っていましたので、ちょっとそこを改めてみようと思います。
具体的にはコードのこの部分を、

      for (var i = bullets.length - 1, j = 0; i >= j; i--) {
        if (bullets[i].erase === true) {
          bullets.splice(i, 1);
        }
      }

以下のように書き換えてみることに。

      var len = bullets.length;

      var tmpBullets = new Array(len);
      var idx = 0;
      for (var i = 0; i < len; i++) {
        if (bullets[i].erase !== true) {
          tmpBullets[idx] = bullets[i];
          idx++;
        }
      }

      if (len > idx) tmpBullets.splice(idx);
      bullets = tmpBullets;

今ある配列をちまちま縮めるのが重いなら、新しい配列を毎度作ってしまえばいいやと。また、配列長が何度も変わるとメモリ確保でパフォーマンスが落ちかねないため、あらかじめ最大長を確保しておこうと。forこそ最速なのでfilter()を使わずなるべく原始的な書き方をしています。

結果、フレームレートが落ち始めるタイミングには大きな差を認められませんでした。
しかし更に雪を増やしていくと、16800オブジェクトぐらいまで50fps以上を保ち比較的安定するように。当初2~4msかかっていた雪の除去が1~2msで済むようになっているようです。これは悪くないですね。
悪くないですが……涙ぐましい……。
今回はベンチマーク目的なので、オブジェクト数を取りづらくなるようなリファクタリングまでは行いませんでした。しかし、たとえば配列自体を使いまわしつつ配列長を固定すれば、メモリ確保やガベージコレクションを減らせるでしょうから、もうわずかにパフォーマンスが向上する可能性を感じます。
感じますが……涙ぐましい……。

ボトルネック探し3: 言語レベルから変える。WebAssemblyに

改善の余地はまだあるわけですが、もっと根本的なところを変えてみようかと思います。
描画を高速化するときのお役立ちがWebGLであるように、プログラムの実行を高速化するならWebAssemblyだろうと!
JITコンパイラが機械語に逐次翻訳していくJSと違って、バイトコードで渡せるWebAssemblyならもっと速くなるはず! IE11さえ気にしなければブラウザ的にはもう実用段階ですからね!
でも高級言語に甘やかされて育ってきた自分にはいきなりC++なんてちょっとつらいかな! じゃあUnityかな! C#も書いたことないけどUnityScriptならJSみたいなもんだしなんとかなるんじゃないかな!
と思ったのですがUnityScriptって最近廃止されていたのですね。そ、そんな……。
参考: Unity 2018.2、リリース – Unity Blog
しかしこれも良い機会、というわけで先の雪っぽいあれをC#でUnityにえっちらおっちら不完全移植してWebAssemblyでビルドしてみた次第です。
おおおブラウザで動いてる! ちゃんとWebGL2.0で動いてる! すごい! さあ雪だすぞどんどんだすぞクリッククリック!

はい。5,000オブジェクトくらいで60fps割ってました。あれえ? Pixi.jsよりパフォーマンス出てない?
いや、最適化なんて一切ないであろう、ひよこ感あふれるコードではJSにも及ばなかったようです。安直にWebAssemblyを使えばなんでも早くなるわけではないんだ、ということを知れただけでもよしとしようと思いました。いったん出直してきます。
多量のオブジェクトの当たり判定を取り続けるみたいに計算量が増えれば変わってくるかもしれませんし、UnityのWebAssembly最適化がまだこれからで今後改善されていくのかもしれませんし。

まとめ

以上を総合しますと、
– WebGLは高速(それはそうだ)
– Pixi.jsは触れ込み通り高速
– CPUパワーは必要だが、JSはシングルスレッドなので最近のPCであれば大きな差は出ない。スマートフォンではかなり影響が出そう
– GPUパワーはスプライトをばらまく程度であれば大して必要ない。ローエンドのグラフィックでもWebGLの恩恵を十分受けられるので環境が許すなら積極的に使っていく価値あり
– ただしオンボード・オンチップのグラフィックはVRAM帯域がどうしても弱くなるので思ったよりパフォーマンスが出にくい場合もあるかも
– 単純なスプライト生成程度の低負荷な描画であればCPUのほうが先に限界を迎える。GPUを呼ぶのもまたCPU
– ちゃんと計測してボトルネックを探してロジックを見直そう

このポテンシャルならば実案件でも使ってみたいなという印象でした。

〆にかえて、このパーティクルを撃たない弾幕シューティングゲームにしてみた

これだけですとただ採用実績が巷に溢れたライブラリを使ってありふれたパーティクル生成、もう何番煎じかわからない記事になってしまいますね。しまいました。
さすがにしのびないので、このパーティクル生成を使って何かしらの形にしてみましょう。

まず、雪をばらまきましょう。ただ撒くだけではつまらないのでいろいろな形で動くようにしましょう。回ったって上に昇ったっていいんです。いくつもパターン作りましょう。ちょっと遠慮はしますので。

次に、せっかく撒いたのでこれを避けてみましょう。小学生の頃に降ってくる雪を避けようとしたことって一度はありますよね。ありますよね。じゃあ先ほどの坂本さんの記事から箱のサンプルを拝借しまして、WASDキーやマウスで動かせるようにしてみましょう。当たり判定もつけましょう。
せっかくなので周りに光みたいなエフェクト回しましょう。ブラーフィルタかけてぼかした○を円運動させればできますね。こういうのロマンですよね。

なんか地味ですね。ちょっと背景つけましょう。ただつけるだけじゃ地味なので3Dにしましょう。Pixi.jsは3Dが苦手なのでここだけThree.jsでやっちゃいましょう。Pixi.jsで生成したcanvas要素にさらにThree.jsで生成したcanvas要素を重ねてやります。力技!
その中にPhotoshopで作ったパーリンノイズ画像を入れてスクロールさせて地面と言い張ってみます。まだ寂しいので枯れ木のフリー素材を使わせていただいて木を生やしてみましょう。葉っぱはレンダリングコスト高そうなので枯れ木です。これは寒そう。

最初から Three.js で作ればよかったのではという気もしてきますが、まあこれなら背景だけ30fpsに落とすみたいな器用なこともできるようになりますし……。

調子に乗ってパターン増やしてたら避けきれなくなってきました。この雪消せるようにしましょう。スペースキーを押すと箱の位置に半透明の画像が重なって接触した雪オブジェクトを削除するシンプルなやつです。いつ壊れるかわかるように耐久力バーをarcで描いてブラーかけて重ねておきましょう。

はい、これで弾を避けるだけのシューティングっぽい……何か……何かができましたね!

なるべくGPUに負荷をかけない方向で実装していたのは実はここで弾をなるべくたくさん出すためだったんです! すみません!
実のところ、これは半年ほど前に弊社フロントチームのLTで発表して遊んでいたもので、本ブログにも写真がちょっと出ていたりします(私自身は恥ずかしがりなので見切れてます)。

あそびかた

よろしければこちらのサンプルをブラウザで開いてみてください。
雪っぽい何かを避けるだけのもの

  • 回転する立方体を動かして3分半くらい雪っぽい白丸を避け続けてください
  • 上のほうにある■がいわゆる残機です。当たると減ります。なくなったらおしまいです。続ける場合はすみませんリロードしてください
  • キーボード: WASDで移動 Shiftを押している間は移動速度が遅くなります(押しっぱなしのほうが楽かもしれません)
  • マウス: ポインタを動かすと真下についてくる
  • 両方: Spaceキーでいわゆるバリア的なあれの出し入れ(耐久力は時間経過で回復しますのでお気軽に)
  • スコアは時間経過で自然に上がります。雪にかすると上昇速度に倍率がかかりたいへんインフレします。当たってしまうと倍率が半分くらいごそっと下がります
  • 弾幕パターンが変わるたび残機がおひとつ増えるのであきらめないで
  • 実は当たり判定は中心2px四方だけで、しかも判定が甘いのでたまに抜けます。あきらめないで
  • 音は出ません。職場でも安心
  • とりあえずめざせクリア! 余裕があったらめざせハイスコア!

習作レベルで恐縮ですが、幾ばくかのお暇つぶしとなれば幸いです。色々な方面から影響を受けていますので、オマージュ元にお気づき頂けると嬉しいです。
ただ、Pixi.jsではどうにもコード量が多くなってしまうので、作り込むならUnityのほうが楽なんじゃないかなあと思いました。