フィルレート、Canvas、および入力

確認済のバージョン: 5.3

-

難易度: Advanced

この章では、Unity UIの構築に関わる問題を幅広く解説しています。

フィルレートに関する問題の解決方法

GPU のフラグメント・パイプラインへの負荷を軽減するための方法は、次の2通りあります。

  • フラグメントシェーダーの複雑性を軽減する
    • 詳細は『UIシェーダーと低性能デバイス』の項をご参照ください。
  • 抽出が必要なピクセルの数を削減する。

UI シェーダーは基本的に標準化されているため、最も起こりやすい問題は、単純なフィルレートの過剰使用です。原因はほとんどの場合、重なり合ったUI要素が多いことや、画面中で大きな部分を占めるUI要素が複数あることです。両方とも、極めて高レベルのオーバードローを引き起こす可能性があります。

フィルレートの過剰使用を軽減してオーバードローを減らすために、以下の対策をご検討ください。

非表示UIを削除する

既存のUI要素をデザインし直す必要性を最小限にとどめられる方法は、プレイヤーから見えない要素を無効にすることです。この方法が適用できる最も代表的なケースは、不透明の背景を持つ全画面UIを使用している場合です。この場合、その全画面UIの下にある全ての要素を無効にすることができます。

最も簡単にこれを行うには、ルートGameObjectや、非表示のUI要素を含むGameObjectを無効化します。他の方法については『[Canvas Rendererの無効化]](/learn/tutorials/topics/best-practices/other-ui-optimization-techniques-and-tips#disabling-canvas-renderers)』の項をご参照ください。

非表示のカメラ出力の無効化

Unity UI で全画面UIが開かれていて背景が「不透明」だったとしても、ワールド空間カメラはUIの背後に標準3Dシーンをレンダーします。レンダラーは、3Dシーン全体が全画面Unity UIで隠れてしまうことを認識しません。

したがって、完全全画面のUIが開かれる時は、その下に隠れる全てのワールド空間カメラを無効化して3Dワールドの無駄なレンダリングを省けば、GPUの負荷を軽減することができます。

(注) “Screen Space – Overlay”に設定されているCanvasは、シーン内のアクティブなカメラの数に関係なく描画されます。

大部分が隠れるカメラの場合

「全画面」UIでも、3Dワールドを完全には覆わずに一部が表示されたままになる場合も多数あります。そのような場合には、表示される部分だけをレンダーテクスチャに含める方法が適しています。ワールドの中で表示される部分がレンダーテクスチャに「キャッシュ」されていれば、ワールド空間カメラを無効化しても問題ありません。キャッシュされたレンダーテクスチャをUI画面の背後に描画して3Dワールドを擬似的に表示することができます。

合成によって作成されたUI

合成によるUIの作成は、多くのデザイナーによく使われる手法です。標準の背景と要素を組み合わせたり重ね合わせたりしてUIを作成する手法です。これは比較的簡単に行うことができ反復型開発にも適していますが、Unity UIが透明レンダリングキューを使用しているためパフォーマンス的には効率が良いとは言えません。

例えば、背景を1つ、テキストの書かれたボタンを1つ持つ単純なUIがあるとしましょう。テキストグリフ内でピクセルが落ちた場合GPUは、背景のテクスチャ、次にボタンのテクスチャ、その次にテキスト・アトラスのテクスチャの合計3つを抽出しなければなりません。UIがより複雑になって背景にも装飾的要素が重ねられると、抽出が必要なサンプルの数も著しく増加します。

サイズの大きなUIがフィルレートの制約を受けている場合の最善策は、特別なUIスプライトを作成し、それを使用してUIに含まれる装飾要素や不変要素をできるだけ多く背景テクスチャに統合させることです。望み通りのデザインを作るためには要素同士を重ね合わせなければならないこともありますが、その場合負荷が高くなりプロジェクトのテクスチャ・アトラスのサイズも大きくなります。この方法を用いれば、重なり合う要素の数を減らすことができます。

この手法(UIを専用のUIスプライトで作成してレイヤリングする要素の数を抑える)は、二次的要素にも応用することができます。例えば、スクロール式ペインに商品一覧が表示される、ストアのUIがあるとしましょう。各商品のUI要素には枠線と背景、価格表示アイコン、商品名などの情報が含まれるとします。

このストアUIには背景が必要ですが、商品一覧は背景の上をスクロールするため、商品の要素をストアUIの背景テクスチャに統合することはできません。しかし、商品のUI要素のうち枠線・価格・商品名などの要素を商品の背景に統合することは可能です。アイコンのサイズと数によっては、フィルレートに対してかなりの処理量が節約できます。

レイヤリングする要素の数を減らすことによる不都合もあります。専用化された要素は再利用できなくなり、この作成のためにアーティストの作業量が増えます。また大きなテクスチャが新しく追加されることで、UIテクスチャに必要なメモリの量が著しく増加することも考えられます。特に、UIテクスチャの読み込みとアンロードがオンデマンドで行われる設定になっていない場合は、その可能性が高くなります。

UI シェーダーと低性能デバイス

Unity UI が使用するビルトインのシェーダーは、マスキングやクリッピングその他、多岐にわたる複雑な操作に対応しています。この複雑性のため、例えばiPhone 4などの低性能デバイスにおいては、より単純なUnity 2Dシェーダーと比べてパフォーマンス性が低くなります。

マスキングやクリッピングをはじめとする「手の込んだ」機能は、低性能デバイス向けのアプリケーションには不要です。使用されない操作を削除するためのカスタムシェーダーを作成することも可能です。例えば以下のように最小限のUIシェーダーを作成できます

Shader "UI/Fast-Default"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"
            
            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                half2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
            };
            
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.worldPosition = IN.vertex;
                OUT.vertex = mul(UNITY_MATRIX_MVP, OUT.worldPosition);

                OUT.texcoord = IN.texcoord;
                
                #ifdef UNITY_HALF_TEXEL_OFFSET
                OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
                #endif
                
                OUT.color = IN.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;
            fixed4 frag(v2f IN) : SV_Target
            {
                return (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
            }
        ENDCG
        }
    }
}

UI Canvas のリビルド

UI を表示するためには、画面に表示されるUIコンポーネントそれぞれのジオメトリがUIシステムによって構築される必要があります。このプロセスには、動的レイアウトコードの実行、UIテキストの文字列に含まれる文字を表すポリゴンの生成、ドローコールを最小限に抑えるためにできるだけ多くのジオメトリを単一のメッシュに統合すること等が含まれます。このプロセスは何段階かに分かれています。詳細はUnity UIの基礎の章をご参照ください。

Canvas のリビルドはパフォーマンス面の問題を引き起こす可能性があります。主な理由は以下の2つです。

  • Canvas 上に多数の描画UI要素があると、バッチの算出自体の負荷が非常に高くなります。これは、要素のソートと解析にかかる負荷が、Canvas上の描画UI要素の数に対して線形増加を超える比率で増加するからです。
  • Canvas が頻繁にダーティになると、比較的変更の少ないCanvasの更新に過剰な時間が使われる可能性があります。

上記の問題はどちらも、Canvasの要素の数が増加するにしたがって深刻化する傾向にあります。

(重要な注意点)Canvas上の描画UI要素のどれかに変更が加わると、そのCanvasのバッチビルドの処理を再度行わなければなりません。この処理によって、そのCanvas上の全ての描画UI要素が、変更されたものもされていないものも含め、再解析されます。ここで言う「変更」には、スプライト・レンダラーに割り当てられたスプライトや、トランスフォームのポジションやスケール、テキストメッシュに含まれるテキストなど、UIオブジェクトの見た目に影響する全ての変更が含まれます。

子の順序付け

Unity UI は背面から前面に向かって構築され、ヒエラルキーに内におけるオブジェクトの順序によってソート順が決定されます。ヒエラルキー内で先に来るオブジェクトは後に来るオブジェクトの背面にあると見なされます。バッチはヒエラルキーの上から下へ順番にビルドされますが、この処理では、同じマテリアルおよび同じテクスチャ(1)を使用していてかつ間に挟まれたレイヤーのないオブジェクトが収集されます。間に挟まれたレイヤーがあるとバッチは強制的に破棄されます。

Unity Frame Debugger の項で説明されているとおり、間に挟まれたレイヤーの検査はフレームデバッガで行うことが可能です。「間に挟まれたレイヤー」とは、1つの描画オブジェクトが、他の2つの、本来はバッチ処理可能な描画オブジェクトの間に割り込んでいる状況を指します。

この問題が最もよく起こるのは、テキストとスプライトが近接して置かれている場合です。テキストのバウンディングボックスが、近接したスプライトに非表示の状態で重なることがあります。これは、テキストグリフのポリゴンが透明であることが理由です。解決方法は2つあります。

  • 描画オブジェクトの配置を変えて、バッチ処理可能なオブジェクトの間にバッチ処理不可能なオブジェクトが挟まれないようにする。つまり、バッチ処理不可能なオブジェクトをバッチ処理可能な2つのオブジェクト両方の上か下に移動する。
  • オブジェクトの位置を調整し、非表示の状態で重なっている部分を無くす。

上記は両方とも、Unityエディターで、Unity Frame Debuggerが開かれて有効になった状態で行えます。Unity Frame Debugger上に表示されるドローコールの数を確認しながら行えば、重なり合ったUI要素が原因で生じている不要なドローコールの数が最小に抑えられる順序と位置を特定できます。

Canvas の分割

通常、よほど単純なケース以外は、必ずCanvasを分割することが推奨されます。Canvasの分割は、要素をSub-canvasまたは兄弟Canvasに移動することで行います。

UI の特定の部分を常に他のレイヤーの上あるいは下に配置するために、その部分の描画深度を他とは別に制御しなければならない場合があります([例]チュートリアルの矢印など)。このような場合は特に兄弟 Canvas の使用が適しています。

それ以外のほとんどのケースでは、Subcanvasの方が便利です。Subcanvasは親Canvasの表示設定を受け継いでいるからです。

単純に考えると1つのUIを多数のSubcanvasに分割するのが最善策であるように思われるかもしれません。しかし、Canvasのシステム上、異なるCanvas間でバッチが組み合わされることはありません。パフォーマンス性の高いUIデザインを実現するには、リビルドの負荷と不要なドローコールの両方が最小限に抑えられるようにバランスを取ることが求められます。

全般的ガイドライン

Canvas は、その構成要素である描画コンポーネントのどれかに変更が加わるごとに再バッチされるので、基本的に、重要なCanvasは少なくとも2つに分割するのがお奨めです。さらに、同時に変更される要素はできるだけ同じCanvas上に配置するのがベストです。プログレスバーやカウントダウンタイマーなどもその例です。分割された2つの部分は両方とも同じデータを基盤にしているため、同時に更新される必要があります。したがって両方が同じCanvas上に配置されるのが理想的です。

背景やラベルなど、変更されることのない静的な要素全てを1つのCanvas上に配置しましょう。これらの要素はCanvasが最初に表示された時に一度バッチ処理され、その後は再バッチされる必要はありません。

2つ目のCanvasに、全ての「動的」要素(頻繁に変更される要素)を配置します。このCanvasは主にダーティな要素を再バッチするようになります。動的要素の数が非常に多くなる場合は、それらの要素をさらに、絶えず変更される要素(プログレスバー、タイマー表示、その他アニメーション要素)と、時々しか変更されない要素のグループに分割する必要があるかもしれません。

これは難易度の高い作業です。特にUI制御がプレハブにカプセル化されている場合はなおさらです。多くのUIでは、上記の方法の代わりに、負荷の高い制御をSub-canvasに配置することでCanvasを分割しています。

Unity 5.2 におけるバッチ処理の最適化

Unity 5.2 ではバッチ処理コードが大幅に書き換えられており、Unity 4.6、5.0、5.1に比べてパフォーマンスが大幅に向上しています。さらに、1コア以上のデバイスではUnity UIシステムは大部分の処理をワーカースレッドに移しています。Unity 5.2では、UIを何十ものSub-canvasに分割する必要性が基本的に低くなっています。多くのUIが、モバイルデバイス上で2つか3つのCanvasだけで高パフォーマンスを実現できるようになりました。

Unity 5.2 における最適化については、この記事の中でより詳しく触れています。

Unity UI における入力とレイキャスト

デフォルトではUnity UIは、タッチイベントやポインターのホバーイベントなどの入力イベントをGraphic Raycasterコンポーネントを使用して処理します。これは通常Standalone Input Managerコンポーネントで行われます。Standalone Input Managerはその名称とは裏腹に「ユニバーサルな」入力管理システムとして提供されており、ポインターとタッチの両方を扱うことができます。

モバイルにおけるマウスの誤入力判定(5.3)

Unity 5.4 より前のバージョンでは、Graphic Raycasterの付属したアクティブなCanvasのそれぞれが1フレームごとに1回レイキャストを実行し、その時点でのポインターの位置を確認します。(進行中のタッチ入力がある間は行われません。)これは、どのプラットフォームでも同じです。マウスのないiOSデバイスやAndroidデバイスの場合でもマウスの位置がクエリーされ、その位置にあるUI要素の特定が行われます(2)

これによりCPU時間が無駄に消費されます。UnityアプリケーションのCPUフレーム時間を5%以上消費することが確認されています。

この問題はUnity 5.4で修正されています。 Unity 5.4以降では、マウスのないデバイスではマウス位置のクエリーが行われなくなっているため、不要なレイキャストは実行されません。

バージョン5.4より前のUnityでモバイル向け開発を行っている場合は、独自のInput Managerクラスの作成を強くお奨めします。これは、UnityのStandard Input ManagerをUnity UIソースからコピーしてProcessMouseEventメソッドとそれに対する呼び出しを全てコメントアウトするだけで、簡単に行えます。

レイキャストの最適化

Graphic Raycaster の仕組みは比較的単純で、「Raycast Target」がTrueに設定されている全てのGraphicコンポーネントに対して反復処理を行います。各Raycast Targetは、一連の条件を満たしているかどうかRaycasterによってテストされます。全ての条件を満たしていることが確認されたRaycast Targetは衝突のリストに追加されます。

レイキャストの実装に関する詳細

一連の条件とは具体的には以下の通りです。

  • Raycast Target がアクティブで有効になっており、描画されている([例]ジオメトリを持っている)。
  • 入力ポイントが、Raycast Targetの添付されたRectTransform内にある。
  • Raycast Target が、ICanvasRaycastFilterコンポーネントのどれかの(いずれかの深度の)子を持っている(または子である)。さらに、そのRaycast Filterコンポーネントが該当Raycastを許可している。

衝突されたRaycast Targetのリストは深度でソートされ、逆向きのターゲットがフィルターにかけられます。またカメラの背後でレンダーされた要素(つまり画面に表示されない要素)がフィルターにかけられ削除されます。

また Graphic Raycasterは、その"Blocking Objects"プロパティに該当フラグが設定されていれば、3D物理システムや2D物理システムにもレイキャストを行います。(スクリプトでは、このプロパティは blockingObjectsと呼ばれます。)

2D や3Dの障害オブジェクトが有効になっている場合、レイキャストをブロックしているPhysics Layer2D上の2Dあるいは3Dオブジェクトの下に描画されるRaycast Targetも全て、衝突のリストから削除されます

その後に最終的な衝突のリストが返されます。

レイキャストの最適化のヒント

Raycast Target はGraphic Raycasterによってテストされる必要があるため、"Raycast Target"設定は、ポインターイベントを受け取る必要のあるUIコンポーネントにのみ有効にすることをお奨めします。Raycast Targetのリストが小さいほど、トラバースされるヒエラルキーが浅くなり、毎回のRaycastのテストに掛かる時間が短縮されます。

ポインターイベントに反応する描画UIオブジェクト([例]背景とテキストそれぞれの色が変化するボタンなど)が複数含まれるような複合的UI制御の場合は、そのルートにRaycast Targetを1つ配置するのがお奨めです。そのRaycast Targetがポインターイベントを受け取り、それを複合的制御内の必要なコンポーネントに転送することができます。

ヒエラルキーの深度とレイキャストフィルター

各Graphic Raycastは、レイキャストフィルターを探す際、Transformヒエラルキーをルートに至るまで全てトラバースします。この処理にかかる負荷は、ヒエラルキーの深度に対して直線的に増加します。ヒエラルキー内で検知された、各Transformに付随したコンポーネントは全て、ICanvasRaycastFilterを実装しているかどうかテストされる必要があります。そのため、この処理には大きな負荷がかかります。

ICanvasRaycastFilter を使用する標準のUnity UIコンポーネントはいくつか存在します。例えば CanvasGroupImageMaskRectMask2Dなどです。したがって、このトラバーサルを単純に削除してしまうことはできません。

Sub-canvases と OverrideSorting プロパティー

Sub-canvas のoverrideSortingプロパティは、TransformヒエラルキーにおけるGraphic Raycastテストの進行を停止します。これを有効にすることでソートやレイキャスト検知の問題が生じることがなければ、レイキャスト・ヒエラルキーのトラバーサルによる負荷を軽減するために、これを使用することをお奨めします。

脚注

  1. 「間に挟まれたレイヤー」とは、異なるマテリアルを持つグラフィック・オブジェクトで、そのバウンディングボックスが、本来はバッチ処理可能な2つのオブジェクトと重なっており、ヒエラルキー内でその2つのオブジェクトの間に位置しているものを言います。
  2. これは、送信される必要のあるホバーイベントの有無を確認するために行われます。