Unityで3Dアクションゲームやオープンワールドゲームを制作する際、描画負荷軽減のために不可欠な機能がLOD Group(Level of Detail)です。遠くにあるモデルのポリゴン数を下げることでレンダリング負荷を大幅に削減できますが、モデルの切り替えがパッと発生すると視覚的に不自然(ポップノイズ)に見えてしまいます。

これを解消するためにLOD GroupのFade Modeを「Animate Cross-fading」に設定し、モデルが滑らかにディゾルブ(クロスフェード)しながら切り替わるように設定するのが一般的です。しかし、自作したShader Graph(シェーダーグラフ)マテリアルを適用したオブジェクトでは、クロスフェードが機能せず瞬時に切り替わってしまったり、遷移中に影が完全に消えてガタついたりする問題が頻繁に発生します。

本記事では、URP環境においてカスタムShader Graph製マテリアルがLODクロスフェードに対応しない根本原因を解説し、Shader Graphを用いた正しいクロスフェードと影のフェードの実装手順を詳しく紹介します。

1. 根本原因①:グローバルキーワード「LOD_FADE_CROSSFADE」の未処理

UnityのLOD Groupで「Animate Cross-fading」を有効にすると、UnityエンジンはLOD切り替え中の2つのモデル(例:LOD0とLOD1)を同時に描画し、それぞれのブレンド比率(0〜1)をシェーダーへ送ります。

このブレンド制御は、Unity内部で定義されているグローバルキーワード「LOD_FADE_CROSSFADE」によって有効化されます。標準のLitシェーダーやUnlitシェーダーはこのキーワードにデフォルトで対応しており、自動的にディザリング処理を行ってフェードアウト・フェードインを合成します。

しかし、Shader Graphで新規に作成したカスタムシェーダーは、デフォルトのままではこのLODキーワードに対する処理ロジックを含んでいません。 そのため、エンジンがいくらフェード比率を送ってもシェーダー側でそれを無視してしまい、結果としてフェードせずに「パッと瞬間切り替え」が起きてしまうのです。

2. 根本原因②:Alpha Clipping(アルファクリッピング)の無効化

LODのクロスフェードでは、描画負荷の観点から、不透明オブジェクト(Opaque)用のレンダーキューで動くように設計されています。通常の半透明ブレンド(Transparent)でクロスフェードを行うと、描画順(ソート)の破綻や、ピクセル描画が何重にも重なる「オーバードロー」による負荷が激増するためです。

そのため、Unityはディザリング(Dithering:ピクセルを網点状に間引く処理。別名スクリーンドア透過)を用いて疑似的に半透明を表現します。このディザリング処理を実行するためには、シェーダー側でピクセルを破棄する「Alpha Clipping(アルファクリッピング / clip関数)」が有効になっている必要があります。

Shader Graphの設定(Graph Settings)でAlpha Clippingがオフ(無効)になっていると、ディザリングによるピクセルのくり抜きが行えず、不透明のまま描画されてしまいます。

3. 根本原因③:Shadow Caster(シャドウキャスター)パスでのフェード非対応

オブジェクト本体の描画がディザリングによって滑らかに消えていっても、地面に落とす影(Shadow Casterパス)がディザリングに対応していないと、影だけが遷移中に一瞬で消滅します。

結果として、本体は滑らかに切り替わっているのに、影だけがパッと消えたり現れたりするため、プレイヤーからは画面が一瞬チラついたように見え、クオリティを著しく損ねるバグとなってしまいます。影も本体と同じアルファマスク計算を共有し、Shadow Casterパス上でもクロスフェード処理を評価する必要があります。

4. Shader GraphにおけるLOD Cross-fadeの正しい実装手順

Shader GraphでLODのクロスフェードに対応させるための設定手順は以下の通りです。

重要な前提条件: マテリアルのレンダリングモードは「Opaque(不透明)」のままで実装します。Transparentに変更する必要はありません。
  1. 対象の Shader Graph ファイルを開きます。
  2. Graph Settings タブ(右側の歯車マーク)を選択します。
  3. 「Surface Type」が Opaque になっていることを確認し、Alpha Clipping のチェックボックスをオンにします。
  4. グラフ内に LOD Cross Fade ノードを追加します。このノードは、Unityエンジンから送られてくる現在のLOD遷移比率(0〜1)を自動的に出力します。
  5. グラフ内に Dither ノードを追加します。
  6. LOD Cross Fade ノードの出力を、Dither ノードの In(または Screen Position / アルファ入力)に接続します。
    ※Ditherノードは画面のピクセル座標に基づいて網点パターンを生成します。
  7. Dither ノードの出力を、Master Stackの Alpha Clip Threshold に接続します。
  8. マテリアル本来のアルファマスク(テクスチャのアルファ値など)がある場合は、それとDitherノードの出力を Multiply(乗算)した上で Alpha Clip Threshold に接続します。

これで、LOD遷移時に自動的にディザリングがかかり、美しくディゾルブ遷移するようになります。

Shadow Caster(影)のクロスフェードを有効化する設定

影も同様にディザリングフェードさせるためには、Shader GraphのMaster Stack内の設定を確認します。 通常、上記の通り Alpha Clip Threshold にディザ値が接続されていれば、URPは本体描画だけでなく Shadow Caster パスに対しても自動的にその値を反映して影をクリップ(ディザリング)します。 マテリアルのインスペクターで Cast Shadows が有効になっており、影が正しくディザリングされていることをシーンビューで確認してください。

5. LOD Cross-fadeの実装アプローチ比較

アプローチ 設定内容 メリットとデメリット
1. Ditherノードによるクリップ処理(推奨) LOD Cross Fade ノードと Dither ノードを組み合わせ、Alpha Clip Threshold に接続する。 メリット: 描画順破綻がなく極めて軽量。モバイルやVR環境でも快適に動作する。
デメリット: 網点パターンが見えるため、カメラが極端に近づいた際にドット感が目立つ場合がある。
2. カスタムHLSL(Transparent化) Surface Typeを Transparent に変更し、カスタムHLSLでアルファブレンドを直接操作する。 メリット: 完全な半透明ブレンドになり、網点ノイズが一切ない滑らかなブレンドが可能。
デメリット: レンダーキューがTransparentになるため、Zバッファによる前後関係が崩れ(ソートバグ)、オーバードローにより著しく高負荷になる。実務では非推奨。

6. LOD切り替え時のC#コードによる動的監視・デバッグ

開発中に、特定のモデルでLODフェードが正常に動作しているかをデバッグするため、LODGroupの現在の遷移状態や適用されているLODレベルをゲーム再生中にテキストとして出力する実用的なデバッグスクリプトを紹介します。

using UnityEngine;

[RequireComponent(typeof(LODGroup))]
public class LODTransitionDebugger : MonoBehaviour
{
    private LODGroup lodGroup;
    private int lastLODIndex = -1;

    void Start()
    {
        lodGroup = GetComponent<LODGroup>();
    }

    void Update()
    {
        if (lodGroup == null) return;

        // 現在適用されているLODレベルのインデックスを取得(リフレクション等のオーバーヘッドを避けるための概算手法)
        // ※Unityの標準APIでは直接「現在のLODインデックス」を取得できないため、各LODのRendererの可視状態から判定します。
        int currentLODIndex = GetVisibleLODIndex();

        if (currentLODIndex != lastLODIndex)
        {
            Debug.Log($"[LODDebugger] {gameObject.name} のLODが変更されました: LOD{lastLODIndex} -> LOD{currentLODIndex}");
            lastLODIndex = currentLODIndex;
        }
    }

    private int GetVisibleLODIndex()
    {
        LOD[] lods = lodGroup.GetLODs();
        for (int i = 0; i < lods.Length; i++)
        {
            foreach (Renderer renderer in lods[i].renderers)
            {
                if (renderer != null && renderer.isVisible)
                {
                    return i;
                }
            }
        }
        return -1;
    }
}

このスクリプトをLOD Groupコンポーネントを持つゲームオブジェクトにアタッチすることで、モデルが実際にどのタイミングで切り替わっているかがConsoleウィンドウにリアルタイムで出力され、Shader Graph側のDither値と実際の切り替え挙動が一致しているかを検証するのに非常に役立ちます。