Slide 1

Slide 1 text

【Unity/Burst】 CPUだけでペンギン一万体 アニメさせてみた

Slide 2

Slide 2 text

誰 村上 善紀 仙台高専(11年~) DeNA(16年4月~) アカツキ(19年4月~) IL仙台(21年12月~)

Slide 3

Slide 3 text

視聴者の想定 難易度:Advanced ・コンシューマーの3Dゲーム開発の経験がある ・Unityでの3Dゲーム開発に詳しい ・Burstが何か知っている

Slide 4

Slide 4 text

ペンギンのハドル 皇帝ペンギンは寒さに対抗するために、ハドルと呼ばれる集まりを作る。ハドル には数百ほどの数が集まるらしい

Slide 5

Slide 5 text

それはともかくとして、

Slide 6

Slide 6 text

ペンギンは一万体くらい 60 30FPSで 元気に動いていてほしい

Slide 7

Slide 7 text

一万体動かしたいペンギン 頂点1076 ボーン6

Slide 8

Slide 8 text

今回の実行環境 CPU:Ryzen 5950x (Stock) GPU:Radeon 6800XT (Stock) RAM:DDR4-3200 (CL14) 64GB Unity:2022.2b3 Development Build(Faster Runtime) / IL2CPP Release DX12 / Graphics Job Enabled / HDRP 3820x2160

Slide 9

Slide 9 text

そもそも標準のUnityではペンギン一万体動かないの?

Slide 10

Slide 10 text

Unityの標準実装ではどうなるか? CPUスキニングとGPUスキニングについて 一万体全員が映るケースで検証してみる 1mおきにx軸とz軸について100x100で配置。カメラはこれらを収める位置に。 最良の結果を得るため、AnimatorのボーンOptimizeも利用する。

Slide 11

Slide 11 text

Unityの標準実装ではどうなるか? ・Animator+CPUスキニング 1フレームに掛かる時間 140-160ms Animator関連 35ms程度 Skinning関連 105ms程度 (Render+Mainスレッド の重ならない部分の合計)

Slide 12

Slide 12 text

Unityの標準実装ではどうなるか? ・Animator+GPUスキニング 1フレームに掛かる時間 55ms~60ms程度 Animator関連 35ms程度 Skinning関連 40ms程度 (Render+Mainスレッド の重ならない部分の合計)

Slide 13

Slide 13 text

30FPSは、1フレームが33msに収まる必要がある

Slide 14

Slide 14 text

アニメーション~スキニングを 前述した問題を解決できるよう、Burstで実装してみよう。 ということで

Slide 15

Slide 15 text

そもそも。

Slide 16

Slide 16 text

アニメーションとスキニングとは ポリゴンで表現される生物について、本来生物が骨の動作によって行う筋肉等 の移動を、簡易的にリアルタイムCGで行うためのもの。 アニメーションでは、ボーンという骨を表す表現の、回転値と位置を決定する。 スキニングでは、ボーンの位置に合わせて、ポリゴンの頂点を移動する。

Slide 17

Slide 17 text

Unityでのアニメーションとスキニングの具体的な仕様 ・ボーン Transformで表される。スキニングのロジック内では座標変換行列として扱わ れる。 ・アニメーション Animatorによってボーンが挙動することで実現される。AnimationClipが、具体 的なアニメーション内容を持つ。 ・スキニング Skinned Meshという種類のポリゴン描画で実行する。

Slide 18

Slide 18 text

Unityのアニメーションとスキニングについて 動作が重い理由について分析する

Slide 19

Slide 19 text

動作が重い理由:アニメーション

Slide 20

Slide 20 text

アニメーションの問題は主に二つある 1.Transformが邪魔 2.Animatorが単純に重い

Slide 21

Slide 21 text

アニメーションの問題1. Transformが邪魔

Slide 22

Slide 22 text

Transformはパフォーマンス的には邪魔者 単純にオブジェクト指向的メモリ配置なため、重い。(連続していない) また、階層構造で処理の並列化の可否が決定する。 https://forum.unity.com/threads/ijobparallelfortransform-15000-transforms-executed-on-single-job-thread-a ny-hints.537723/ https://blog.unity.com/technology/best-practices-from-the-spotlight-team-optimizing-the-hierarchy

Slide 23

Slide 23 text

Transformはパフォーマンス的には邪魔者 一方で、作業上重要な役割も担っている。 AnimationClipを介したアニメーション再生で、Transformは使われることが前 提。

Slide 24

Slide 24 text

アニメーションの問題2. Animatorが単純に重い

Slide 25

Slide 25 text

2.Animatorが単純に重い 見た感じ、やってることに対して重すぎる。 (Unityの内部実装は確認できないため、Burstで一から実装したらもう少しパ フォーマンス出るだろうという推測です。)

Slide 26

Slide 26 text

動作が重い理由:スキニング編

Slide 27

Slide 27 text

動作が重い理由:スキニング編 ・そもそも、スキニングがロジックとして重い! Unityは別に何か悪いことをしているわけではない。 強いて言えば、Skinned Meshでは16bitのメッシュをスキニングできない。今回 は精度的に16bitで十分なため、これはUnityの問題と言える?

Slide 28

Slide 28 text

これらをふまえて

Slide 29

Slide 29 text

独自実装の指針

Slide 30

Slide 30 text

独自実装の指針 ・アニメーション AnimationClipを利用できるようにしながら、Burstで一から PlayableGraph(Animator)に相当する物をBurstで実装する。 ・スキニング 16bitスキニングに対応して、Burstで実装する。

Slide 31

Slide 31 text

独自実装する:アニメーション編

Slide 32

Slide 32 text

独自実装する:アニメーション編 ・AnimationClipの利用 ・PlayableGraph(Animator)相当のもの これらが必要

Slide 33

Slide 33 text

AnimationClip

Slide 34

Slide 34 text

AnimationClipの詳細 内部表現をエディタースクリプティングで確認することが出来る。

Slide 35

Slide 35 text

AnimationClipの詳細 AnimationClipの内部表現は、文字列とAnimationCurveで出来ている。 AnimationCurveとして時間軸上におけるアニメーションの値が表現され、文 字列で、そのCurveがどのプロパティに関連するか示されている。 ※今回はボーンに対する挙動なので移動回転スケールができればいい

Slide 36

Slide 36 text

AnimationClipの評価をBurst化 ・だがAnimationCurveはメインスレッドでしか評価できない。 https://forum.unity.com/threads/animationcurve-evaluate-can-only-be-calle d-from-main-thread.531614/ 移植するためには、内部ロジックが必要。

Slide 37

Slide 37 text

AnimationClipの評価をBurst化 UnityのCsReference内にAnimationCurveの図的表示に使われている部分の ロジックがあったので、これをベースにスレッドセーフなものを実装する。 https://github.com/Unity-Technologies/UnityCsReference/blob/master/Edit or/Mono/Animation/AnimationWindow/CurveEditor.cs

Slide 38

Slide 38 text

PlayableGraph相当の表現

Slide 39

Slide 39 text

PlayableGraph相当の表現 Clipをどのように評価し、最終的なボーンの出力にするかは、決して素朴なも のではない。ブレンディングやレイヤーマスクなど、何をどのようにどの程度実 装するかはゲームに応じて決めていい。 その為、詳細は割愛します。 (自由に実装できるということ自体が 重要な個所)

Slide 40

Slide 40 text

スキニングの実装指針詳細

Slide 41

Slide 41 text

スキニング in 16bit 頂点バッファを16bit化して、それに合わせたロジックを書く。 今回はランタイムでUnityの通常のメッシュアセットを変換し、16bitになるように 調整する。 ※UnityではBurstをつかって、VertexBufferのレイアウト等に対して細かく指示 しながらランタイムでメッシュを構築できる。 https://github.com/Unity-Technologies/MeshApiExamples

Slide 42

Slide 42 text

Unityでのアセット

Slide 43

Slide 43 text

Burstで利用するランタイム表現

Slide 44

Slide 44 text

実際に動かした

Slide 45

Slide 45 text

Unityエディターに移動します

Slide 46

Slide 46 text

パフォーマンス確認・改善

Slide 47

Slide 47 text

ボーンアニメ+スキニング ペンギン1000体のパフォーマンス ・Burst実装     ・Animator+Unity CPU ・Animator+Unity GPU

Slide 48

Slide 48 text

ボーンアニメ+スキニング ペンギン1000体のパフォーマンス ※スキニングとAnimation関連部分のみ ・Burst実装 約4.6ms ・Animator+Unity CPU 約13ms ・Animator+Unity GPU 約5ms

Slide 49

Slide 49 text

1000体でこれじゃあ、10000体むり

Slide 50

Slide 50 text

改善1 Burst Jobのスケジュールパフォーマンス

Slide 51

Slide 51 text

改善1 UnityのJobスケジュールパフォーマンス スケジュールに掛かってる時間が長い(2ms以上)

Slide 52

Slide 52 text

改善1 UnityのJobスケジュールパフォーマンス 例えばスキニングの処理は、1メッシュの頂点バッファが一つのNativeArrayで 表される。 安全システムによってコレクションのコレクションは許可されておらず、通常では 複数の頂点バッファを一つのJobに対してまとめることができない。 つまりメッシュ数分のJobがスケジュールされる。これが重い。

Slide 53

Slide 53 text

UnityのJobスケジュールパフォーマンス 安全システムの回避方法として、ポインタのコレクションを扱うことができる。 NativeArrayはGetUnsafePtr()で要素先頭へのポインタを取得することができ る。つまり「配列の先頭ポインタ」の「配列」は作れる。 これによって、複数の頂点バッファへの処理を単一のIJobParallelForで行うこ とが出来る。

Slide 54

Slide 54 text

UnityのJobスケジュールパフォーマンス 2msほど要していたものが、0.3ms程度まで落ちた

Slide 55

Slide 55 text

改善2 ボーンアニメーションの評価速度

Slide 56

Slide 56 text

改善2 ボーンアニメーションの評価速度 ボーンAnimationの評価自体にも時間が掛かっている。(1msが31スレッド)

Slide 57

Slide 57 text

改善2 ボーンアニメーションの評価速度 直接の原因としてはAnimationCurve相当の処理評価がそれなりに重いこと。

Slide 58

Slide 58 text

改善2 ボーンアニメーションの評価速度 そもそもクリップがオーサリングされている以上の分解能で評価される必要が あるのか? ない。指定された分解能以上の評価が行われないようにしちゃおう! モーションの評価が決まった回数ならば、ボーンアニメーション程度の情報量 はキャッシュできるのでは? あるモーションのある特定のフレームは、初回の評価の結果をキャッシュして、 一度だけしか評価されないように実装してみる。

Slide 59

Slide 59 text

UnityのJobスケジュールパフォーマンス 1msほどの処理が0.1msほどに

Slide 60

Slide 60 text

ボーンアニメ+スキニング ペンギン1000体のパフォーマンス 計測環境:Ryzen 9 5950x / Radeon 6800XT / DX12 / IL2CPP DEBUG ・Burst All実装改善後      ・Burst実装(さっきの) 2.3ms 4.6ms

Slide 61

Slide 61 text

改善3 スキニングをキャッシュ

Slide 62

Slide 62 text

改善3 スキニングをキャッシュ AnimationClipが固定の分解能を持てるなら、スキニングもキャッシュを持て る。 1フレーム当たりの情報量は 16bit x 4 x 3 = 192bit (24byte) 頂点数1076の場合約25KB …まあ対象や条件を限定するなら。

Slide 63

Slide 63 text

ボーンアニメ+スキニング 約1000頂点ペンギン1001体のパフォーマンス ・Burst All実装(改善3あり)     ・Burst実装(改善3なし) 2.3ms 2.0ms

Slide 64

Slide 64 text

計算0なのに思ったより早くなってないのね スキニングの処理自体は1.8ms -> 1.1ms 帯域の方が問題か。コア数が少ないCPUなら有意な差が出るかも。(ちゃんと 調べていない) Jobスケジュールの複雑度が上がったオーバーヘッドもある。(0.4ms増) 追記:ポインタをもっと駆使していればもうちょい何とかなった。 尚、今回は各Mesh Rendererの利用するMeshの頂点バッファが、毎回CPU 側の処理としてキャッシュから値を受け取っていて、それぞれがGPUへのアッ プロードをしている。 単体のアニメーションを再生するだけにしては無駄な処理で、重い。

Slide 65

Slide 65 text

メッシュ共通化によるスキニングキャッシュ 今回は行わないが、キャッシュ戦略を使うなら、常時、全てのメッシュが固有の 頂点バッファを使う必要はあまりない。 Batch Renderer Group 2022を利用することで、メッシュをintのIDで指定する 描画発行が行える。特定のアニメーションのあるフレームをメッシュとして保存 し、それをIDによって指定することで最速のスキニングが可能。 (ただし、モーフやブレンディング等の処理が追加で行われることが頻繁なら、 この指針ではつど明示的なインスタンス化の解決が必要になるため、実装は複 雑になるかもしれない。)

Slide 66

Slide 66 text

いよいよ一万体で勝負

Slide 67

Slide 67 text

ボーンアニメ+スキニング ペンギン10000体のパフォーマンス ・Burst実装     ・Animator+Unity CPU ・Animator+Unity GPU

Slide 68

Slide 68 text

ボーンアニメ+スキニング ペンギン10000体のパフォーマンス フレーム時間トータル ・Burst実装 約65ms ・Animator+Unity CPU 約140-160ms ・Animator+Unity GPU 約55-60ms

Slide 69

Slide 69 text

標準のGPU実装に負けてるやん!

Slide 70

Slide 70 text

どういうこと? スキニング+アニメーションの時間だけならGPU実装より早い UnityのGPUスキニング Animator関連 35ms程度 Skinning関連 40ms程度 Burstアニメ+スキニング Animator相当 3ms程度 Skinning関連 25ms程度

Slide 71

Slide 71 text

どういうこと? GPUとの情報同期にやたらとオーバーヘッドがある UnityのAPIの構造上、GPU上のバッファに一度頂点相当情報を転送してから さらにGPU内でVertexBufferにコピーする必要があり、一度余計なコピーが 走っている。これによって11ms程度のロスが発生… https://forum.unity.com/threads/feature-request-vertex-buffer-with-lockbufferforwrite-when-po ssible.1294395/#post-8387988

Slide 72

Slide 72 text

回避できないオーバーヘッドは仕方がない フルコントロールしてる恩恵を使う

Slide 73

Slide 73 text

間引きをやる 登場しているキャラが多い場面などで、遠距離のキャラクターや動きの少ない キャラクターについてはより少ない頻度でスキニング更新する。割と古くからあ る最適化方法。 本来Unityではスキニングを一時停止させることは出来ず、この方法をパ フォーマンス改善で利用できない。独自実装ではスキニングを明示的にコント ロールしているため可能。

Slide 74

Slide 74 text

間引きをやる 動画 動画に移動します

Slide 75

Slide 75 text

間引きをやる 動画

Slide 76

Slide 76 text

間引きをやる 動画

Slide 77

Slide 77 text

間引きをやる 動画

Slide 78

Slide 78 text

間引きをやる 動画

Slide 79

Slide 79 text

間引きをやる 今回は高度な条件を入れず、1フレームごとに全体の半分ずつを更新してい る。29-30FPSを達成

Slide 80

Slide 80 text

今後の展望・まとめ

Slide 81

Slide 81 text

今後の展望 ・残念なオーバーヘッド含め、頂点アップロードの時間が掛かりすぎている。 Batch Renderer Group(BRG)によるMeshIDの指定によるスキニング結果の ルックアップ方式を、使える場面では使った方がよさそう。 余談だがBRGで沢山の異なるメッシュを使った時に、パフォーマンスが過剰に 低くなる特性を発見し、公式と連携してバグ提出した。BRGは2022で大幅にリ ニューアルされたホットなAPIなので注視必須。 https://forum.unity.com/threads/new-batchrenderergroup-api-for-2022-1.1230669/

Slide 82

Slide 82 text

まとめ ・Burstの登場でアセットの低レベルな表現を割と高速かつ自由にCPU側で扱 えるようになっている。   >CPUは最低物理8コアが常識な時代。酷使していこう。 ・ゲームにあったソリューション(間引き等)は汎用ゲームエンジン時代に取りづ らいようだが、今はBurstやコンピュートシェーダがあり、1から実装すればビル トインより高品質なものが割と作れる。検討すべきときはする。