34.7K Views
January 22, 19
スライド概要
UnityのUIとはどういったシステムなのか、そして最適化する上で確認すべきポイントとTipsを紹介します。
---
このスライドは、「TECHxGAME COLLEGE#10 Unityでパフォーマンスの良いUIを作る為のTips」で紹介したスライドを少し手直ししたものとなります。
https://techxgamecollege.connpass.com/event/99824/
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
Unityでパフォーマンスの 良いUIを作るためのTips ver 1.1 Tatsuhiko Yamamura @ Unity
お品書き • UnityのUIシステムおさらい • パフォーマンスの良いUIの為に注意すべきこと • バッチング • フィルレート • リビルド • その他
Unity UI
Unity UI • Unity UI、通称UGUI • Unity 4.6のタイミングで追加 • オープンソースで開発 https://bitbucket.org/Unity-Technologies/ui/ • 当時はNGUIというアセット(有料)を殆どの人が使用していた • NGUI開発者が中の人になって一緒に開発
uGUIの機能 ビジュアル表現 • • Image • Raw Image Text uGUIには結構いろいろな機能がある インタラクション • Button • Toggle • Slider • etc レイアウト • Basic Layout • Layout Group • etc
UGUIの機能 • 基本的なレイアウト • 自動レイアウト • ビジュアル表現 • インタラクション
UGUIの機能 • 基本的なレイアウト • 自動レイアウト • ビジュアル表現 • インタラクション
UGUIの機能 • 基本的なレイアウト • 自動レイアウト • ビジュアル表現 • インタラクション https://github.com/mob-sakai/UIEffect
UGUIの機能 • 基本的なレイアウト • 自動レイアウト • ビジュアル表現 • インタラクション
全自動 UI最適化ボタン 結構、なんとなくで作れてしまうUIシステム、 ただしパフォーマンスも担保してくれるかというと、微妙にしてくれない。 全自動最適化ボタンは無い。
柔軟性 柔軟性とパフォーマンスはトレードオフ 実際に標準の機能を使用せず、自作する必要も出てくる パフォーマンス
バッチング
描画処理 サンプル キャラクター サンプル キャラクター サンプル キャラクター 描画は奥から順にスタンプのように貼り付けていくイメージ
描画する流れ Material (+Shader) Texture Polygon 描画は基本のポリゴンと同じく、マテリアルでポリゴンを塗る方式 描画
複数繰り返す 描画処理をたくさん繰り返すのは効率が悪い
Canvasに一旦まとめる マテリアル テクスチャ Canvas UIのレイアウト 平面上の要素なので、多少楽をしている(らしい) 結合したポリゴン ※まとめるのはHierarchyではなく実際の描画ベース
まとめて一回で描画(バッチング) まとめたポリゴンを一気に描画すれば、大量に描画しても負荷はソコソコ
一括描画できるのは 同じテクスチャのみ 異なるマテリアルの場合、異なるポリゴンを用意して描画(バッチが解ける)
入れ子のせいで バッチングできない スプライトが入れ子になるとバッチング出来なくなる ※Hierarchyではなく描画ベースの入れ子
入れ子のせいで バッチングできない テクスチャが同じでもマテリアルが異なるとバッチング出来ない
バッチが可能な条件 • 同じCanvasに所属している • 同じMaterialを使用している • 同じTextureを参照している • 座標のZ値が同じ位置にあること • 同じマスクでクリップされていること(RectMask2Dの場合) バッチングの為には全ての条件を満たす必要がある
UI Profilerで観察 バッチングが解けてるかどうかはUI Profilerで確認 (Unity 2017.1から) 確認 方法
バッチで まとめた UI バッチ一覧 描画一覧と、バッチでまとめたUIの一覧を確認できる
バッチ出来ない理由 テクスチャが違う バッチが解ける理由も表示される
Sprite Atlas 対策 バッチをまとめたい場合はSpriteAtlasにまとめる
Sprite Atlasで 一つのテクスチャにまとめる 参照するテクスチャが 同じなので、バッチングが出来る 一枚のテクスチャにまとめれば、同じマテリアルで複数のUIを表示出来る UVの異なる矩形ポリゴン
違うタイプのキャラクターが 混ざっても1回で描画できている 実際の描画の様子
なにか違う… 元画像 右はオリジナル、左はSpriteAtlasで取得したスプライト。妙な模様が追加されている
スプライトをミッチリと詰めた為に発生。UIは基本的に矩形なので、ミッチリ詰めると発生
余裕を持って 配置する 対策 対策1:Tight Packingを止めて、余裕を持って配置
Imageを矩形ではなく ポリゴンで描画する (Unity 2018.3から) 対策 対策2:矩形ではなくポリゴンを使用して描画
バッチング対策 • バッチの条件を満たす • テクスチャをパックする • マスクの使用は抑える 対策
フィルレート
ポリゴンに色を塗る際は 1ピクセルずつ塗っていく ポリゴンを表示し、色を画面(スクリーン)の1ピクセルずつ塗っていく。これをマテリアル単位でGPUで一括で行う
沢山のUIがある場合 重なって表示している場合に問題になる。
沢山のUIがある場合 複数回塗られている 透明は「描画しない」ではなく「透明を塗る」なので、重なっている部分が複数回塗られる(オーバードロー)
UI Profiler (Composite Overdraw) UI ProfilerのCompsiteOverdrawで確認できる。色が赤に濃いほど負荷が高い 確認 方法
UI Profilerで確認しているの図
オーバードロー対策 • 不要なUIを排除 sprite mesh使用 通常のquad • 重なる部分を出来るだけ削る • Sprite Meshを使用 • Fill Centerを外して、中央をくり抜く • オーバードローしても問題ないシェーダーを使う 対策
• シェーダーの負荷は 各デバイスのプロファイラーで 確認する • XCodeはいいぞ
UIのリビルド (ジオメトリ)
Canvasに一旦まとめる マテリアル テクスチャ Canvas UIのレイアウト バッファ 前述の通り、Canvasは一旦描画するUIのポリゴンをまとめて、バッファを生成。 バッファは基本使い回される
2300個のImage 殆ど負荷がかからない 単一描画になるので効率よく描画できる。バッチング出来ているなら殆どCPUに負荷がかからない(iPadPro1stで検証)
変化があれば再構築が必要 マテリアル 更新 テクスチャ 更新 Canvas 更新 UIのレイアウト UIを動かすと以前のバッファを使い回せなくなるので、バッファを作り直す必要がある バッファ
バッファの再構築は即時ではない Canvas 変化した内容に応じて Dirtyフラグを付ける UIを再構築 SetDirty Update SendWillRenderCanvases LateUpdate バッファは即時反映ではなく、変化した物にDirtyフラグをセット、溜めて一括処理する Post Rate Update
再構築の条件 • UIをenable/disable • マテリアルが変化 • RectTransformのサイズが変化 • Transformの親が変化 UI再構築の条件は結構緩く、簡単に再構築が走る
一つ変わると、同じCanvasの全ての UIを再構築する Set Dirty 同じCanvasが全て 更新対象に ジオメトリはCanvas単位で管理しているので、一つでも変更があれば同Canvas全てに影響する
このUIのどれかが動けば、背景のUIにリビルドが走る
メインスレッドの 処理は少ない 配置により ソートに時間がかかる Canvas内のサイズによってジオメトリの再構築に時間がかかる ジオメトリの再構築はメインスレッドではない為、即座にフレームが落ちる訳ではない(テキスト…フォントの取得は除く)。ただし ジオメトリ生成完了までレンダースレッドが遅延するので、量によっては固まる。また、他のジョブ進行にも影響する。
確認方法 UI Profilerが動いている 動いていなければ静か 確認 方法 UI Profilerで確認出来る
Profilerでも分かりやすい 確認 方法 通常のプロファイラーでもUGUIRenderingが伸びるので確認できる
確認 方法 CPU Profiler(Hierarchy)で OnDidApplyAnimationPropertiesや OnRectTransformDimensionsChangeで検索 追加表示項目を変更 Show Related Objects(関連オブジェクトを表示) UI再構築してそうなの一覧が表示される UIのEventSystemでUIが動いている場合、誰がUIを動かしたのかを確認できる
Canvasを分割 対策 対策1:SubCanvasを作りジオメトリを分割する
Canvasを分割 Set Dirty 違うCanvasなのでリビルド対象外 Dynamic Canvas Static Canvas Canvasを分割することで、UIを動かしてもジオメトリの再構築範囲を限定できる
(Canvasのジョブ完了待ちが 消えた効果もある) Canvas分割の効果。UIのソートやジオメトリの生成コストが殆ど無くなる
動くUIはImageのSprite Meshを避ける use sprite mesh : enable 3.22ms 対策 use sprite mesh : disable 0.4ms SpriteMeshはポリゴンが多く、ジオメトリ生成のコストが上がる。動かすUIには使わない方が無難
親UIを動かす root element element element element element element element 親Transformを動かした場合もUIの再構築が走る。 ScreenSpaceCameraを使用している場合、よく起こる
動かしたタイミングで負荷 確認 方法 UIProfilerでRenderの山ができている場合はコレ
• 要素数を減らす • 要素をプーリングして 最小限のUIでどうにかする 対策 コレといった対策は無い。 動くCanvasには要素はあまり突っ込まないのが良い
UIのリビルド (Layout Group)
RectTransform 子の rm o f s n a r T t c Re Re c 子の Rec tTra nsf o rm 子 の tT ra ns fo rm 子の RectTransform LayoutGroupは、例えばRectTransformの中に大量のUIがある時、
RectTransform ILayoutGroup 子のRectTransform 子のRectTransform 子のRectTransform 子のRectTransform 子オブジェクトを自動的に並べてくれる(レイアウトする)機能 ILayoutGroupで 子RectTransformの情報を収集し
RectTransform ILayoutGroup ILayoutSelfController 子のRectTransform 子のRectTransform 子のRectTransform 子のRectTransform 子オブジェクトの情報を元に、親(自身)のサイズを調整することも可能 ILayoutSelfControllerで 子RectTransformの情報を元に 自分のオブジェクトのサイズを変更する
RectTransform ILayoutGroup ILayoutSelfController 子のRectTransform 子のRectTransform 子のRectTransform 子のRectTransform 子のRectTransform 子のRectTransform 要素が増えたりサイズが変わっても対応できる、柔軟性を持つ 要素が増えても 自動的に対応
うまく組み合わせれば、横に伸びて縦に要素を加算するUI…みたいなものも作れる
特にScroll Viewとかでよく使われる機能
Dirty System Layout Group Element Element Element Element UI LayoutもDirtySystemを使用して実現している
Dirty System Layout Group Element Element Element Child Child UIの変更があればDirty (この例では緑のElementのサイズを変更) Dirty
Dirty System Layout Group Element Element リビルド申請 Element Child DirtyしたUIから親LayoutGroupへ、レイアウトの再構築を申請
Dirty System Layout Group Element Element Element 再レイアウト あとで一斉にUIを再レイアウトする Element
Dirty System Layout Group Layout Group Element Element Layout Group Element Element リビルド申請 Element Element 複数のLayoutGroupがある場合は
Dirty System Layout Group Layout Group Element Element Layout Group Element Element Element Element LayoutGroupの親まで到達して、そこから再レイアウト (なので、途中で自身のサイズを変えるContentSizeFitterはLayoutGroupの間に挟むと具合が悪い)
Groupの更新 Layout Group Layout Group Element Element rect.GetComponents() Element Layout parent.GetComponents() Group Element parent.GetComponents() Element この親LayoutGroupへの通知は、毎回GetComponentを使用。子のレイアウト変更にもGetComponentを使用。 Element 地味に負荷になる
DirtyするUIが紛れ込むだけで 毎フレーム、UIのリビルドが走る 一つでもDirtyするUIが含まれると、毎回Layoutの再構築。 これはメインスレッドで実行され、妙に高い負荷になる
確認方法 UI Profilerが動いている 動いていなければ静か 確認 方法 再レイアウトが起こっている場合、UI ProfilerのLayoutが大きく変化する
再レイアウト処理 メインスレッドで処理されるため、大きな負荷と認識される事が多いです。
• RectTransformに関連するものを操作しない (結果は変化しなくとも、セットするだけでリビルドが走る) • Layout Propertyに関連するものを操作しない • UI Element関連を操作しない • Mecanimのステートマシンに、上記の要素にアクセスする AnimationClipを入れない (Simple Animationなら多分OK) 対策
対策 もしUIのインタラクティブな機能(ScrollViewとかButtonとか)から呼び出している場合、ProfilerのUI Detailで 誰が呼び出しているのか確認できる
対策 LayoutGroupを 可能な限り使わない
その他
その他 500個のUI.Textコンポーネントのtextを変更 Font.CacheFontForText TextコンポーネントのTextを変更する際にメインスレッドでフォント取得処理が発生 Textコンポーネント1つなら問題無いが、大量にあると負荷になる (※変更しなければ発生しない)
その他 • Raycast Targetは可能な限り外す • World Space CameraのEvent Cameraにはカメラを登録する (無い場合はFindTag(“MainCamera”)を呼ばれる • Pixel Perfect設定は動かないカメラのみ設定する • テクスチャは画面解像度に合ったものを使う、 可能な限り圧縮する
その他 • UI Profilerはエディターのみ動作 • XCode等のデバイス専用プロファイラーは 詳しい情報を収集できて便利 • 負荷を見るならビルドする Windows向けでも、多くのノイズを取り除いてくれる (実機で確認するのがベターではある)
まとめ
ポイント • 動かさなければ負荷は低い • 動かすなら最低限の影響に抑える • ドローコールとフィルレート、バッチング。 良いバランスを取る •気を抜くな!誰も信用するな! プロファイラーを手放すな!
UI Element (Runtime) Coming Soon
おしまい