はじめに
こんにちは、OIEです。
本記事では、VCIのなかで多数のサブアイテムを動かす場合に、どうしたら負荷を抑えられるかについて、自身の試行錯誤で得られたノウハウを共有したいと思います。
表題である負荷を抑える方法をサクッと確認されたい方は、「どうしたら負荷が減らせるの?」節まで飛んでご覧くださいね。
2020年11月にインテリア用VCIとしてリリースした、変形するインテリア「CUBE」では、サブアイテムとして180個のパーツ(小さな四角錐)を用いて、様々な動きのあるシーンを表現したり、シーン間を変形させたりしています。公開しているバージョンではある程度負荷が抑えられていますが、作成中はシーン構築と並行して性能グラフとにらめっこする日々でした。。
そんな開発の中で調べたこと、考えたこと、実践したことをまとめたのが本記事です。
前提
今回の執筆のきっかけとなるVCIは、以下の環境で開発・動作確認したものです。
- VirtualCast(安定版) Ver.1.9.4g, 1.9.4h, 1.9.5d
- VirtualCast(ベータ版) Ver.2.0.0a1
- UniVCI 0.29
リリース時期の関係もあり、今回の開発はUniVCI 0.29 & VirtualCast 1.9系の組み合わせで行っていますが、VirtualCast(ベータ版) Ver.2.0.0a上でも、少し確認した限り、スタジオでもルームでも問題なく動作するようでした。
【ちょっと脇道】どんな VCI を作ったの?
作成した VCI 自体の説明は本記事の主旨から外れますが、このあとの流れを理解いただく際の手がかりとして、簡単に概要とプログラム処理の流れを説明します。
本VCIの概要を、The Seed Onlineの商品ページから引用すると・・
キューブが変形しながら、テーマに応じた様々なシーンを描くインテリアです。サイズ可変なので、スタジオ/ルームに大きく設置したり、手元にちょこんと置いて眺めたりできます!
操作によりテーマ切替間隔の変更、電源ON/OFF可能。
というものです。
この変形やアニメーションを実現するため、計180個の四角錐パーツをサブアイテムとしてコントロールし、テーマごとに動きをプログラミングしています。あるテーマを規定時間再生し、シーン間の変形を挟んだら次のシーンを再生する・・という繰り返しが、プログラム全体のおおまかな流れになります。
上の画像のとおり、シーン中もシーン間も常に全サブアイテムを表示しており、一部を隠したり軽いモデルに差し替えたりしないのがこだわりポイントなのですが、そのおかげで深刻な負荷問題と戦う日々が始まったのでした。。
なお、製品版は有料での販売としていますが、ほぼ同じ動作をするお試し版のほうは無料で公開していますので、機会があればぜひ触ってみてください。各サブアイテムが動いているさまをご覧いただくと、本記事の実感度が増すかなと思います。
サブアイテムが多いと何が起こるの?
「サブアイテムが多いと重くなる」というお話はウワサ程度に聞いていたのですが、いざ実際に作成して自分のスタジオで動かしてみると、「うわ重い!」とすぐ実感できました。
ここでは、この「重い」の中身を詳しく見てみます。
FPSが落ちる
これは見た目的にも分かりやすいですね。
FPSは、バーチャルキャストの設定で「スタジオにデバッグ情報を表示する」を有効にした状態でコンソールから確認できますが、性能が十分に確保されているマシン環境であれば、恐らくHMDの仕様上の上限値付近で安定するかと思います。たとえば自分が使用しているHMDの場合、自分のスタジオで一人でいるぶんには 90.0 FPS と 90.5 FPS のあいだを行ったり来たりします。しかし、スタジオ内に VCI アイテムをたくさんロードしていくと、そのうち 89.5, 89.0 ・・と落ちていき、画面表示のガタつきもだんだん認識できるようになります。
これが、サブアイテムを多数含む VCI を読み込んだ場合では、たった1アイテムでもこのような FPS の落ち込みが発生しやすくなります。FPS の低下はVR酔いを招きやすいため、利用される方の目線で考えると、できる限りこのような事態は避けたいですね。
なお、ポリゴン数は四角錐180個とベースのみでたかが知れているし、マテリアルのシェーダーはすべてStandardなので、GPUの処理能力の超過がFPS低下の原因とは考えづらい状況です。なにより、何も処理を行わないコードではサクサク動く現実が見えていたので、今回はモデルのせいにはせず、コードの中身を疑っていくことにします。
ネットワークの単位時間当たりアップロードデータ量が増加する
起動中のバーチャルキャストでは、インターネットを介したデータのアップロード・ダウンロード双方が行われています。このうちアップロードについては、サブアイテムの数が多いと通信量が増えがちになります。
私自身がタスクマネージャーのスループット項目を確認した限りでは、通常300kb/s かそれ以下で安定している環境で、開発中の上記VCIでは読み込んだ直後に1.6Mb/s まで跳ね上がっていました。この数値自体はバーチャルキャスト内の処理やマシン環境、ネット環境が影響している可能性があるため、利用される方によってはこれ以上の速度になるかもしれません。
アップロード速度の増加は、上記のFPS低下ほど直感的に実感できる変化ではありませんが、もし動画配信サイトへ生配信を行っている場合、VCI によってアップロード帯域が1.6Mb/sも圧迫されるとなると、配信で遅延やコマ落ちなどの影響が出るかもしれません。配信ソフト側の設定変更も対処策のひとつですが、利用者の方の手間になるし、それ以前にこの VCI の利用をやめてしまう方のほうが多そうです。。
また、ネットワーク処理の負荷増大により、結果的にFPS低下にもつながる恐れがあるかと思います。
以上、ここに挙げた2点のほかにも「重い」を構成する要素はあるかもしれませんが、本アイテム作成の中では、主にこの2点に対して調査を進めることにしました。
どんな処理が重いの?
ここまでで、サブアイテムが多い場合に起こる重さについて、その大きさを示す指標として「FPS」と「アップロード速度」が見えてきました。今度はコードの一部をコメントアウトするなどして、どのあたりの処理がこれらの指標に影響しているかをざっと調べてみたところ、以下の2つの処理が特に大きく影響していそうだということがわかってきました。
※あくまで本VCI開発のなかで目立った・着目した処理ですので、ひとつの参考としてみていただければと思います。
(1) 回転処理(Quaternion * Quaternion, Quaternion * Vector3)
各サブアイテムのアニメーションをプログラムするうえで、今回はベクトルやクォータニオンを回転させる計算を多用することになりました。この部分が、他の単純な計算処理に比べ CPU に負荷を与えるらしく、結果的に FPS 低下に大きく関与しているように見受けられました。
(2) 各サブアイテムにおける位置・回転の指定(SetPosition, SetRotation)
本 VCI は「変形する」ことを1つのテーマとしているため、180個のパーツはほぼ常にどれかしらが動いています。結果的に、位置と回転を指定する SetPosition, SetRotation を毎フレーム、多くのサブアイテムに対して実行しているわけですが、これらの負荷が想定以上に大きいものでした。特に、1フレーム内で多数のサブアイテムに対して実施すると、ネットワーク負荷として影響が如実に表れてきます(おそらく、これらの関数が呼ばれたタイミングで、その情報をサーバに上げるため)。
以上2点は、前節で見てきた問題「FPS低下」と「ネットワークの単位時間当たりアップロード量の増加」をもたらしています。逆に言えば、これらへの対処を行うことで、VCI としての「重さ」を低減できる可能性が見えてきました。
どうしたら負荷が減らせるの?
負荷が高いと考えられる上記2点の処理に対して、以下のような対策を実施しました。
(A) 回転処理に効く対処
(A-1) 計算回数を減らす
コードレベルで見直していくと、同じ回転処理を別の場所で都度実施している箇所がぽろぽろ見つかったため、まずはこれらの計算結果を変数に格納後、ほかの処理でも共有できるようにします。
(A-2) 回転を別処理に置き換える
180個のパーツを個別に処理しているといっても、シーンによっては別パーツからy軸がずれているだけだったり、計算済みの2地点間の割合から求められたりするケースが多くありました。このように、単純な加算やLerp, Slerpを代わりに用いるだけで、回転処理をせずに済ませることができないかを検討します。
(もっとも、当初コーディングしたときとは異なる組み立て方を考えるようなものなので、適用できそうな個所を探すのはなかなか大変です。)
例えば上図のシーンでは、真ん中で四角錐パーツが円柱状に並びながら回転・上昇しています。このとき、横1段分の各パーツについては回転処理を用いた位置・回転の計算が必要ですが、その他の段にあるパーツの位置は、最初の1段にあるパーツに対してローカルのy座標がずれただけです。回転に至っては1段目のパーツと完全に同じであるため、最初の1段の計算結果を流用すればほかの段はすべて回転処理を回避することができます。
このように、あるサブアイテムと他のサブアイテムの位置関係・回転関係がシンプルである場合、計算結果を流用できないか検討してみるのがいいかもしれません。
(ちなみに実際のVCIでは、各パーツの初期位置データからlerp, slerpを用いて位置・回転を求めているため、1段分の処理自体もさらに軽量化しています。)
(B) 回転処理にも、SetPositon, SetRotationにも効く対処
(B-1) 動かさなくていいサブアイテムは動かさない
シンプルですが、まずはこれが一番だと思います。プログラムを組み始めた時点では、(動作確認の観点としても)全サブアイテムに対してSetPositon, SetRotationを実施するかたちで実装するケースも多いかと思います。この段階から、各場面ごとに、「ほぼ毎フレーム動くパーツ」と「ほぼ動かないパーツ」に処理を分け、後者に対しては例外時以外は計算も位置・回転指定もしないようにすることで、1フレームあたりの処理数を減らすことができます。
(後者でも処理をしなければいけない例外は、実装にもよりますが、例えば全体のスケールが変更されることで、全パーツの相対位置を一から再計算するようなケースです。)
(B-2) 処理するフレームを間引く
こちらは、2019年版アドベントカレンダーの重いVCIアイテムがちょっと軽くなると良いなで、にゃふさんが記事で述べられているとおりです。毎フレームで計算・位置回転指定するのではなく、必要なだけのフレームを処理し、その他のフレーム処理を間引く方法です。離散的(飛び飛び)なアニメーションを実現したい場合には、動かさない間のフレーム処理は不要なので、この対処がよく効きます。しかし逆に、本 VCI のように連続的なアニメーションを行いたい場合、フレームの間引きはアニメーションのカクつきとして知覚されてしまうため、どの程度まで間引くかは判断が必要になります(以下の章も参照ください)。
(B-3) 一度に全パーツを処理せず、複数のフレームに分割して全体を更新する
今回ご紹介する対処法の中では、一番トリッキーなものかもしれません。
基本的な考え方は、本来毎フレームで全パーツを更新していくところを、あるフレームでは一部のパーツを更新し、次のフレームでは別のパーツ群を更新し・・というように、順繰りに対象パーツ群を変えながら更新していこう、というものです。
当然、更新のタイミングがパーツ間でバラバラになってしまうため、ある瞬間の姿をじっくり観察すれば粗が見えてしまうのですが、時間幅をもって眺める分には破綻が少なく、それでいて、どのフレーム間においても動いているパーツは存在するため、アニメーションのカクつき感を低減することができます。
具体的な実装例は、以下の通りです。ここではパーツ群を16に分割して処理するため、パーツに振ったIDを16で割った余りを変数 l とし、同じ余りのパーツに対して(例えば、l = 1なら1, 17,33, 49, ..)処理を行い、その次のフレームでは、余りが異なるパーツ群に対して(l = 2 なので今度は 2, 18, 34, 50, ..)処理を行い・・というように、順繰りに更新を行っていきます。
function update()
for i=l,180,16 do -- time 時点での各パーツの位置を Lerp で求め指定
polymid[i].SetPosition(Vector3.Lerp(posSceneNow[i], posSceneNext[i], time))
polymid[i].SetRotation(Quaternion.Lerp(quartSceneNow[i], quartSceneNext[i], time))
end
l = l + 1
if l > 16 then
l = 1
end
end
この動きを可視化したのが、下に埋め込んだTwitterの動画です。解説用に、処理中のサブアイテムだけがサイズ2倍になるようにして可視化しました。1/8倍速のカットを見ていただくと、ある瞬間には一部のサブアイテムだけが処理され、その後対象が次々と切り替わっていくのが分かりやすいかと思います。最後のシーンは、リリース版を等倍速でそのまま撮ったものですが、それほどカクつき感や違和感は感じられないかと思います。(実際には、粗が目立たないような処理も別途行っていますが、それについてはまた別の機会に)
Qiita記事への動画埋め込み用ツイート。VCI作成時に試した負荷低減法の説明用です。
— OIE@遊楽団 (@yug04) December 1, 2020
明日公開のアドカレ用に、いまも素材仕込み中… pic.twitter.com/oQnLuaSUCt
どこまで負荷を減らせばいいの?
上記の対処法のうち、A-1, B-1は実行結果に変化がないため、コードを見直し、できる限り実施していくかたちでよいかと思います。しかし、悩ましいのはA-2, B-2, B-3ですね。B-2, B-3 では、当初想定していた動きのなめらかさ・一体性を損なうことになりますし、A-2 においても、軽い処理に置き換える過程で、一部の動作が変化してしまう場合もあるでしょう。負荷が高いが理想の動作に近い状態と、負荷は低いが動作の多くに妥協した状態、両者間でどのようにバランスを取ればよいのでしょうか。
個人的には、自分が想定する利用ケースにきちんとマッチするかどうかを、アイテムごとに判断していくしかないのかな、と考えています。
本VCIの場合、利用者が自身のスタジオ(将来的にはルーム)でBGMのように流しっぱなしにしながら、一人 or 少人数で別作業をしていることをまず想定しているため、(多人数・インタラクション多めのような)軽量重視でなくてもいいかなと考えました。一方で、配信時に背景の賑やかしとして設置することも想定すると、配信にあまりにも影響が出てしまうようなアップロード帯域は使いたくありませんでした。
具体的に決めたゴールは以下の3点です。
- 自分の環境で、アップロード速度をだいたい800kb/s(≒100kB/s)以下に抑えること
- 自分の環境で、ハコスタジオにロードした状態で、だいたい 89.5 fps をキープすること
- 上記2項目を守ったうえで、なるべく動作を理想の状態に近づけること
これらを達成するために、上記B-3のフレーム分割数を調整したり、リッチだけど重い処理を泣く泣くあきらめたり、パーツ群ごとに更新頻度を変えたり・・と、かなりのシェイプアップを図りました。結果的に、最初に想定していたアニメーションよりはかなりカクついた感じにはなってしまいましたが、逆に全体のミニマル感とは意外にマッチしてるかもとも思ったり。。いかがだったでしょうか?
おわりに
以上、私自身の悪戦苦闘と、それをもとに得られたものを書かせていただきました。多数のサブアイテムを扱うケースはそれほど多くはないと思いますが、自由度が高くて「遊べる」VCIにおいて、性能面の問題を避けるために表現の幅が狭まってしまうのはもったいないなぁ・・とも思っています。本記事が、開発者の方も利用者の方も楽しめるようなVCIつくりの一助になれば幸いです。
今年2020年4月に初めてバーチャルキャストとVCIに触れ、このタイミングで大量のサブアイテムを扱うのは結構無謀でした(笑)。ただ、検討と開発用の時間をある程度しっかり確保できたこともあり、いろいろと学びの多い VCI 作成経験でした。まだまだ初心者の身ですが、今後もまたこのような機会でお会いできればと思います。