iOS や Android などのモバイルプラットフォーム向けに Unity でゲームを開発する際、メニュー画面やスキル選択、ショップ画面などの「UI が密集するシーン」を開くと、端末が急激に発熱し、数分でフレームレートが低下(熱スロットリングが発生)する現象に直面することがあります。

プロファイラで頂点数(Vertices)を確認しても大した数ではなく、C# スクリプトの CPU 負荷も正常である場合、ボトルネックは GPU の 「フィルレート(Fillrate:ピクセル描画能力)の限界」 にあります。その最大の原因が、uGUI(Unity UI)における Overdraw(重複描画) です。

本記事では、uGUI で Overdraw が発生する仕組みと、透明な Image がもたらす深刻なパフォーマンス影響をコードレベルで解明し、描画負荷を劇的に削減する具体的な最適化テクニックを解説します。

1. 根本原因:透明(Alpha = 0)な Image がピクセルを消費する仕組み

多くの開発者が「画面に見えていない(透明な)UI 要素は描画コストがかからない」と誤解しがちです。しかし、uGUI の Image コンポーネントは、マテリアルのカラー設定で Alpha を 0(完全透明)に設定しても、依然として GPU のレンダリングパイプライン上では描画処理(Draw Call)が実行されます

具体的には以下の処理が毎フレーム走ります:

  1. Canvas が UI の頂点データ(メッシュ)をビルドし、GPU へ送信する。
  2. GPU は頂点シェーダーを実行し、画面上のピクセル位置を決定する。
  3. フラグメントシェーダー(Fragment Shader)が実行され、たとえ透明度(Alpha)が 0 であっても、各ピクセルのカラー計算が行われ、フレームバッファに書き込まれる。

この結果、画面上では何も見えない透明な板であるにもかかわらず、ピクセルバッファへの書き込み(フィルレート)が消費されます。これが何重にも重なると、GPU は 画面の解像度 × 重なり数 分のピクセル計算を強いられ、簡単に限界値を超えてしまいます。

【通常表示】              【Overdraw(重複描画)の視覚化】
┌──────────────┐          ┌──────────────┐
│  [ ボタンA ]  │          │  ██████████  │ ← 2重 (透明背景 + ボタン)
│              │ ───>     │              │
│  [ ボタンB ]  │          │  ██████████  │
└──────────────┘          └──────────────┘
※透明な全画面パネルがある場合、画面全体が「明るい青〜赤」になり、全ピクセルが何重にも無駄に描画されている状態を示します。
  

図:見えない透明Imageやパネルが重なることで、同一ピクセルへの書き込みが何重にも発生する様子

2. Overdrawの確認方法:Unity Editorでの視覚化

ゲーム内でどこが Overdraw のボトルネックになっているかは、Unity エディタ上で視覚的に確認できます。

  1. Scene ビューの左上にある描画モードのドロップダウン(デフォルトは「Shaded」)をクリックします。
  2. リストから 「Overdraw」 を選択します。
  3. 画面が暗くなり、オブジェクトが重なっている部分が明るく(青から赤、さらに白へ)表示されます。

この Overdraw ビューにおいて、「UI要素がないはずの背景領域が青く光っている」、あるいは「文字やアイコンの周辺が真っ白(最悪のオーバーラップ状態)になっている」場合は、そこが最適化のターゲットです。

3. 解決策1:判定専用の透明Imageを『Empty Graphic』に置き換える

ボタンのクリック可能範囲を広げるため、あるいはジェスチャー(スワイプ等)を検出するために、透明な Image(Source ImageがNoneでColor Alphaが0)を配置することはよくあります。
これらはクリック判定(Raycast)のためだけに存在し、描画される必要はありません。

この場合、描画処理を完全にバイパスしつつ、クリック判定だけを通すカスタムコンポーネント EmptyGraphic を作成して使用するのが最も効果的です。

EmptyGraphic(描画ゼロのクリック判定用スクリプト)

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 頂点やピクセルの描画処理を一切行わず、Raycastのクリック判定のみを提供する軽量コンポーネント。
/// 透明なImageコンポーネントの代わりにこれを使用することで、Overdrawをゼロにできます。
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
public class EmptyGraphic : Graphic
{
    protected override void Awake()
    {
        base.Awake();
        useLegacyMeshGeneration = false;
    }

    // 頂点データの生成(Geometry)を完全に空にする
    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();
    }
}

導入手順:

  1. 上記の EmptyGraphic.cs をプロジェクトに追加します。
  2. クリック判定用として置いていた透明な Image コンポーネントを取り除き(Remove Component)、代わりに EmptyGraphic をアタッチします。
  3. これだけで、そのオブジェクトのポリゴン描画負荷は 0 になり、かつボタン等の Button コンポーネントからのクリックイベントは従来通り検知されます。

4. 解決策2:非表示UIの正しい無効化手順

UIの一部(ポップアップや非アクティブなタブ)を一時的に非表示にする際、CanvasGroup の Alpha を 0 にしたり、色の Alpha を 0 にするだけでは、描画パイプラインから除外されず、Overdraw の原因であり続けます。
適切な非表示手法は以下の優先順位で行います。

  1. GameObject自体の非アクティブ化 (SetActive(false)):これが最もクリーンで、CPU・GPUともに負荷が完全にゼロになります。
  2. Canvasコンポーネントの無効化 (enabled = false):UIのアクティブ化に伴うAwake/Startの再走を避け、表示・非表示を高速に行いたい場合に非常に有効です。Canvasコンポーネント自体を無効にすると、子オブジェクトの描画処理が走らなくなります(ただし、C#スクリプトのUpdateなどは動作し続けます)。
  3. CanvasGroupblocksRaycasts = false かつ描画無効化:CanvasGroupを使用してアニメーションフェードする場合は、フェードアウト完了時に必ずCanvas自体を無効化するか、GameObjectを非アクティブに切り替えます。

5. 解決策3:SpriteのTight Meshによる余白カット

UIに円形やキャラクターイラストなどの「非矩形(四角形ではない)Sprite」を使用する場合、画像ファイルの四隅にある「透明な余白」も四角いポリゴンとして描画され、Overdraw を発生させます。

これを防ぐために、Sprite の形状に沿ったタイトなメッシュを生成させます。

  1. アセットフォルダ内の該当 Sprite を選択し、Inspector を開きます。
  2. Mesh TypeFull Rect から Tight に変更し、Apply します。
  3. UI上の Image コンポーネントの Use Sprite Mesh チェックボックスを オン (True) にします。

これにより、Unity は透明な余白をカットした多角形メッシュを自動で生成し、不必要な透明ピクセルの描画を大幅にスキップできます。※ただし、メッシュの頂点数がわずかに増加するため、極端にシンプルな画像や大量のパーティクル以外で有効な手法です。

6. 解決策4:Canvas分割による再ビルド(Rebuild)負荷の抑制

UI要素が重なり合い、かつ一部がアニメーション等で動く場合、Canvas全体の頂点データが毎フレーム再生成(Canvas Rebuild)されます。これも CPU 負荷の大きな要因です。

「動く要素(アニメーションするアイコン、ゲージ、ローディング表示)」「動かない要素(背景画像、静的な枠線、テキストラベル)」 を別々のサブ Canvas(親 Canvas の中に作成した Canvas コンポーネントを持つ GameObject)に分割することで、再描画の範囲をローカルに限定し、無駄な再描画と Overdraw を抑えることができます。

7. まとめ

  1. Alpha = 0 のImageは透明でも描画されている: GPU のピクセル書き込み能力(フィルレート)を無駄に消費します。
  2. クリック判定には EmptyGraphic を使う: 描画負荷ゼロで Raycast ターゲットを維持できるため、UI判定エリアの最適化に必須です。
  3. シーンの非表示UIは確実に無効化する: Alphaを0にするだけでなく、SetActive(false)Canvas.enabled = false で描画ツリーから完全に除外します。
  4. SceneのOverdrawビューで常時監視する: 開発段階から定期的にビューを確認し、重なりが多い領域を排除する設計を心がけましょう。