インフィニットループ 技術ブログ

2023年07月26日 (水)

著者 : y-murakami

【Unity】AnimatorやPlayableGraphからも卒業!?スキニング・スケルタルアニメーションの独自実装をして全局面で勝つ・前編【中級者向け】

 仙台の3Dゲームエンジニア、にしきんです。

 BRGによる描画についてと、AnimationCurveのBurst化についてをこれまでお話してきました。今回はそれらを踏まえて、独自でスケルタルアニメーション・スキニングをUnity内で実装していくお話をいたします。ちなみにただ動くことを目的にしているわけではありません。実用的に、パフォーマンスや取り扱いやすさなどでビルトインソリューションに勝利できる(!!)ことを示していきたいと思います。

 なお本トピックは少々複雑なため、前編・後編の二部構成とさせていただきます。前編にあたるこちらの記事では、概要・実装戦略と動作パフォーマンスの確認を行います。次回予定している後編では、実装方法の詳細や、ボーンに対する処理の拡張に開いたAPIの設計についての詳解を行おうと考えております。

 では本題に入ります。

Unityにおけるアニメーション・スキニングの問題

 ゲームエンジンにおいてビルトインで用意されているソリューションを置き換える場合はそれなりの理由が必要になるでしょう。 Unityのアニメーションの、私のユースケースにおいての問題点(2023.2.0a20時点)を挙げると以下のようなものがあります。

  • (超重要)アニメーションとスキニングがいずれもBRGとかみ合わず利用できない
  • Animatorが設計や挙動的に受け入れがたい
    PlayableGraphはいくらか良いが洗練されていないように感じる
  • AnimatorもPlayableGraphも別スレッドから直接のパラメータ供給が不可
  • AnimatorとPlayableGraphはその評価について 独自で実装を置き換えた場合明らかにパフォーマンスで勝てるような速度で動作している(ように見える)
  • アニメーションリターゲティングが任意の一致する構造で出来ない
  • スキニングの発動タイミングがコントロールできず毎フレーム行われる

 あくまでも私のユースケースでの問題認識ですが、納得していただける部分もあるのではないでしょうか。独自で実装出来ればすべて解決できそうですね。

ポリゴンモデルにおけるスケルタルアニメーション・スキニングとは

 先に進む前に。ここまで当たり前のようにスキニングだのスケルタルアニメーションだのと言葉を使っておりますが、そもそもこれらはなんでしょうか?何が達成されれば、スキニングやスケルタルアニメーションは実現されたことになるのでしょうか?

 まあ、明確な規格はありません。閉廷!……とは、しないでおきましょうか。適当にでっちあげておきましょう。

スキニング

 あるメッシュについて任意数の行列からそれぞれの影響度を加味して、行列によって頂点位置を変形する処理。この処理で登場する行列をそれぞれボーンという。

スケルタルアニメーション

 スキニングで利用されるボーンを「アニメーション」という時間軸上の挙動情報や、リアルタイム計算(IK等)によって示される通りに動かすこと。

 これら定義をまとめると「スケルタルアニメーションが行われると頂点位置の変換行列(ボーン)が出力され、その行列を利用することでスキニングが行われる。」というような言い方ができるでしょうか。3Dキャラクターをアニメさせるという曖昧な文言で示されるような要件は、これを実現すれば達成できたことになると思います。本当に?……話が進まないので、とりあえずそう思っておきましょう。

実装戦略

 アニメーションを独自実装するとは言っても、Unityのシステムと互換性を完全に失う必要はないでしょう。経験的に、この手の話はオーサリング周りまで抜け出すと厄介になりがちです。

 あくまでAnimation ClipやUnityのMeshをそのまま独自システムで利用できるデータに変換し、それを活用してBRGで描画しつつ(SkinnedMeshRendererに該当)、アニメーションを制御(Animator・PlayableGraphに該当)できるようにします。(尚、Animation ClipではAnimation Curveがふんだんに利用されています。前回AnimationCurveのBurst化について説明した理由ですね。独自でハイパフォーマンスでAnimation Clipを解釈するために必須だったということです。)

 ところで、私のケースではAnimationClipからの過剰な制御を省きたいという気持ちがあり、アニメーションはあくまでボーン行列のみを制御するべきだと考えておりました。ですから、マテリアルプロパティや、MonoBehaviourコンポーネント制御、Event等には対応しないことにしました。それらのようなコンセプトに対応する必要がある場合はTimelineのような、より上位の階層で行うべきだと思っております。
 もちろん、Timelineはパフォーマンス上の難があるため雑に利用すれば折角のアニメーション独自実装によるパフォーマンスの利点をつぶしてしまいかねません。つまり、Timelineも独自実装するべきでしょう。この話はまた、どこかで行うかもしれません。

 それからアニメーションの制御については、ユーザー側でInertial BlendingやIKといった拡張に対応できているとよいでしょうから、そのようなAPIにしてみます。PlayableGraphを、順当に使いやすく仕上げたようなAPIにすると、妥当な形になるかもしれません。

 最後にスキニングについてですが、GPUを利用します。これについては工夫できる余地はあまりないです。Unity公式ソリューションの処理速度に負けないようにだけ気を付けたいと思います。

処理のパイプライン

 戦略を踏まえると、実装はフレーム内で以下のような処理の流れをとります。実線の矢印で示される方向に処理が進みます。秩序の無い適当な図ですが、読み取ってください。

 UnityのAnimation ClipやMeshから戦略で必要となるような情報を独自に抽出する方法については、Unityのドキュメントとにらめっこしてください。メッシュについてはスレッドセーフなAPIがあるため、ランタイムでもある程度ハイパフォーマンスに情報を抽出することが可能です。

 スキニングやスケルタルアニメーション等、個々の要素の実装詳細は基本的な話ですから、ここでは解説しません。気になる方は、3Dゲームにおいて基本的な話なので調べれば情報は豊富にあると思います。無かったらごめんなさい。

動作、パフォーマンス確認

左側:Animator+SkinnedMeshRenderer 右側:独自アニメーション+BRG

 さて、実際に動作させてみました。違いは見た感じなさそうです。パフォーマンスの検証として、100体のペンギン(約1000頂点)を同時に動かしてみます。CPU時間はUnityプロファイラで、GPU時間はPIXを使って計測します。グラフィクスAPIはDX12、ビルドはDevelopment BuildでMasterです。ハードウェアはIntel Xe 96EU / 12700h@45Wを利用します。

まずUnity標準のAnimatorとスキニングですが、1フレーム当たり

  • GPU 約4.4ms(GPUはスキニングのみ)
  • CPU 約3.2ms(MeshSkinning+Animatorのメインスレッド占有時間)

ほど掛かりました。詳細は以下画像をご確認ください。

GPU 約4.4ms
CPU 約3.2ms

 そして今回作成した自作版については1フレーム当たり

  • GPU 約4.0ms(スキニングのみ)
  • CPU 約0.8msくらい(メインスレッドはほぼ占有しないので、適当に直列に測った感じ…)

を記録しました。Animatorの処理時間に相当するCPU側は4倍ほどの違いがあります。
(計測がちょっと雑すぎる所はあるんですが。実際に使い込んでいくと、スケジューリング中にメインスレッドは別の仕事ができますから更に差をつけられるはずです。また、まだ原因を調べてはないのですが下のプロファイリング画像から明らかなとおりスレッドへの負荷分配があまり上手くいってないように見えます。これはintelのPコアEコアだと思いますが、13900kではもう少し分配がうまく行ってた気がするので、要確認ですね。タスク粒度的にはEコアが処理を行って問題ないような内容なはずだったとも思います。)

GPU 約4.0ms
CPU 約0.8msくらい…

 GPU側についても、スキニング計算のやり方に大した幅はないため速度差が出ることはないはずなのですが10%程高速化できました。

 実は、2023あたりから公式のSkinnedMeshRendererによるスキニング処理がBatch化により高速化されていたらしく、計測してみてこれに勝っていたのは一安心でした。どんな感じなのかなと思い一応Unity 2023.2での公式スキニングシェーダについても中身をちゃんと見てみましたが、特に悪い記述はなかったので、Batch化に興味がある方は公式のコードを読んでみるとよいでしょう。僕も大体同じような指針でコードを書いておりました。ちなみに、何故パフォーマンスで勝てたのかは明確に心当たりがあるのですが、ないしょです。

 GPU観点としてもう一つ、当たり前といえば当たり前なのですがポイントがあり、描画にSkinnedMeshRendererを使うUnity側の処理と、描画にBRGを使っている私の処理でパフォーマンスに差がありません。アニメーションするメッシュはインスタンシングできないため、SkinnedMeshRendererもBRGも、1インスタンスを描画するDrawIndexedInstancedに変換されています。単一インスタンスの描画にBRGを使う場合は、パフォーマンス上の利点としてはCPU負荷やメモリ軽減が主になるということですね。

おわりに

 前編ではアニメーションとスキニングの概要や、Unityで実際にメッシュに対するオペレーションを動作させられること、さらにそれらについてパフォーマンスで優位に立てることを示しました。ただ、これではどう実現しているのかが判然としないと思いますから、後半の記事でそのあたりについては触れたいと思っております。

 今回、当たり前みたいにスキニングとAnimatorを高速なパフォーマンスで置き換えてしまっていますが、こんなことが出来ちゃうくらいには低レベルAPIが提供されていることにしっかり感動しておきましょう。これこそが今のUnityの強さです!(似たようなことを、いくつかの記事で言ってる気がします。)素晴らしいですね。

 ILでは3Dゲーム開発にも取り組んでおります。興味がある方は是非採用情報をご確認ください。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!