UnityのURP(Universal Render Pipeline)を用いた3Dゲーム開発において、Directional Light(ディレクショナルライト)やPoint Light(ポイントライト)を追加した際、「モデルの表面や床に謎の黒いしま模様(横縞ノイズ)が這い回るように表示される」「影の輪郭がモザイクのようにギザギザになる」「縞ノイズを消すためにライトの設定を変えたら、今度は影が足元から離れて宙に浮いて見える」といったシャドウ関連の描画バグに直面したことはないでしょうか。

これらは「シャドウアクネ(Shadow Acne)」および「ピーターパニング(Peter Panning)」と呼ばれる、3Dグラフィックスにおける影生成(シャドウマップ法)の極めて古典的かつ代表的な不具合です。本記事では、この描画バグが発生する内部的な数学の仕組みと、URPで美しく自然な影を描画するためのBias(バイアス)およびNormal Bias(法線バイアス)の正しい調整手順を分かりやすく解説します。

1. 根本原因①:シャドウアクネが発生する仕組み

モデルの表面に等高線のような黒い横縞模様が浮き出る「シャドウアクネ」の正体は、モデル自身の影が自分自身に投影されてしまう「セルフシャドウ(自己影)の判定ミス」です。

Unityのシャドウ生成プロセス(シャドウマップ法)では、まず「ライトから見た視点」でシーンをレンダリングし、各オブジェクトの距離情報を記録したテクスチャ(シャドウマップ)を作成します。その後、カメラからシーンを描画する際、描画対象のピクセルのライトからの距離と、シャドウマップに記録された最短距離を比較し、「ピクセルの距離 > シャドウマップの距離」であれば「光が遮られている(=影になる)」と判定します。

しかし、シャドウマップは有限のピクセル数(解像度)で構成されているため、モデルの斜面や平面上において、複数のFragment(ピクセル)が一つのシャドウマップピクセルを共有することになります。さらに、浮動小数点の計算精度の限界から、「本来なら光が当たるべき平面であるにもかかわらず、計算の端数処理によって、わずかにシャドウマップに記録された値よりもオブジェクト表面のほうが奥にある」と判定されてしまい、1ピクセル置きに「日向」と「影」の判定が繰り返されます。これが黒い横縞ノイズ(アクネ)となって画面に現れます。

2. 根本原因②:ピーターパニング(影の宙浮き)が発生する仕組み

このシャドウアクネを防ぐ最も手軽な方法は、ライトの設定にある「Bias(Depth Bias)」の数値を上げることです。Biasは、シャドウマップの深度値を計算する際に、意図的に一定のオフセット値を加えて「影を少し奥側(ライトから遠い方向)へずらす」処理を行います。これにより、平面でのセルフシャドウの誤判定を完全に避けることができます。

しかし、このBiasの値を大きくしすぎると、今度は別のバグ「ピーターパニング」が発生します。

深度をライトから遠ざけすぎた結果、「オブジェクトの実際の底面(接地部分)と、影が描き始められる位置」の間に隙間ができてしまい、キャラクターが地面から浮いているように見える描画破綻が発生します(※童話ピーターパンのように影が本体から切り離されてしまうことからこの名がついています)。したがって、美しい影を描画するには、シャドウアクネを完全に消しつつ、ピーターパニングを起こさない限界の「絶妙なバランス」を見極める必要があります。

3. URPライトインスペクターにおける「正しいバイアス調整手順」

URPの各ライト(Lightコンポーネント)の「Shadows」パラメータには、この問題を解決するための2種類のバイアス値が用意されています。

プロパティ名 役割と特徴 調整時のガイドライン
Bias (Depth Bias) シャドウマップ全体を「ライトの照射方向」の奥側へ平行にずらす。上げすぎるとピーターパニングに直結する。 極力小さく設定する(例: 0.02~0.05)。0にしてしまうとアクネが発生しやすいため、極小の値を維持します。
Normal Bias 影を落とすレシーバーの「法線方向(面が向いている向き)」に沿って、シャドウマップの境界位置を少し押し縮める。ピーターパニングを起こさずにシャドウアクネだけを強力に狙い撃ちできる。 やや大きめに設定する(例: 0.5~1.5)。特に斜面や球体のしま模様は、この値を引き上げることで綺麗に消すことができます。

具体的な調整ワークフロー

  1. シーン上にバグ(縞模様)が確認できるキャラクターや傾斜したアセットを配置し、カメラを近づけます。
  2. ライトの Bias0 に、Normal Bias0 に設定します(この時点では強烈なシャドウアクネが発生します)。
  3. まず Normal Bias0.1 刻みでゆっくりと上げていきます(多くの場合、0.5~1.2 の間で表面の縞模様がサーッと消えていきます)。
  4. Normal Biasだけで消えなかった微小なアクネやノイズに対して、Bias0.01 刻みで少しずつ増やします(多くの場合、0.02~0.04 程度で完全にノイズが消え、影がぴったり足元にくっついた状態を作れます)。
  5. モデルの足元をアップで確認し、影と足の間に不自然な隙間(ピーターパニング)ができていないか確認します。隙間がある場合は、Biasの値を少し下げ、Normal Biasでカバーするように調整します。

4. 影の輪郭のギザギザ(ジャギー)を解消するURPアセット設定

バイアスを正しく設定しても、影のフチ自体がドット絵のように粗くギザギザに荒れている場合、それは「シャドウマップの解像度(Shadow Map Resolution)の不足」が原因です。これを解決するには、プロジェクト共通のURPアセットの設定を調整します。

設定箇所: Projectウィンドウ内の Universal Render Pipeline Asset(例: URP-HighFidelity 等)を選択し、インスペクターの「Lighting」および「Shadows」項目を開きます。
  1. Shadow Distance(シャドウ表示距離)を狭める:
    シャドウマップは「カメラからShadow Distanceに指定された距離」までの領域を一括でカバーします。ここが 150200 など過剰に広いと、シャドウマップのピクセルが遠くまで薄く引き伸ばされ、手元の影がぼやけてギザギザになります。シングルプレイの近接アクションなどでは 30~50 程度に縮めることで、シャドウマップの解像度が手前に集中し、一瞬でシャドウが極めて高精細になります。
  2. Shadow Cascades(シャドウキャスケード)を設定する:
    カメラからの距離に応じてシャドウマップを複数枚(2枚または4枚)に分割して割り当てる機能です。「手前」「中距離」「遠景」でシャドウ解像度を自動で切り替えるため、パフォーマンスへの悪影響を抑えつつ、手元カメラ前の影のギザギザを劇的に軽減できます。通常は 2 Cascades または 4 Cascades を有効に設定します。
  3. Shadow Atlas Resolutionを引き上げる:
    シャドウを描画するアトラスのピクセル解像度(例: 2048x2048 や 4096x4096)を上げます。ただし、この値を上げるとGPUのビデオメモリ(VRAM)消費と描画負荷が上がるため、ターゲット端末(モバイルかPCか)に合わせて慎重に選択してください。

5. 動的なシャドウバイアスのランタイム最適化C#スクリプト

プラットフォームやゲーム中のカメラの画角(FOV)やグラフィックス設定の変更に応じて、動的にライトのシャドウバイアスを調整するための実用的なヘルパースクリプトを以下に紹介します。特にAndroidやiOSなどのモバイル機器では、シャドウマップの解像度を下げざるを得ないため、バイアス値を高めに設定してアクネを回避しつつ、PC版ではタイトにしてクオリティを上げるといった制御が必須となります。

using UnityEngine;

[RequireComponent(typeof(Light))]
[DisallowMultipleComponent]
public class URPShadowBiasOptimizer : MonoBehaviour
{
    public enum TargetPlatformTier { MobileLow, MobileHigh, StandalonePC }
    
    [Header("グラフィックス品質設定")]
    public TargetPlatformTier platformTier = TargetPlatformTier.StandalonePC;

    private Light targetLight;

    void Start()
    {
        targetLight = GetComponent<Light>();
        ApplyOptimalSettings();
    }

    public void ApplyOptimalSettings()
    {\
        if (targetLight == null) return;
        if (targetLight.shadows == LightShadows.None) return;

        switch (platformTier)
        {
            case TargetPlatformTier.MobileLow:
                // 影解像度が極端に低い(512等)場合:アクネを消すためにバイアスを大きくし、
                // 隙間をごまかすためにニアプレーンを少し遠ざける
                targetLight.shadowBias = 0.08f;
                targetLight.shadowNormalBias = 1.5f;
                targetLight.shadowNearPlane = 0.5f;
                break;

            case TargetPlatformTier.MobileHigh:
                // 平均的なモバイル品質(1024シャドウマップ)
                targetLight.shadowBias = 0.04f;
                targetLight.shadowNormalBias = 1.0f;
                targetLight.shadowNearPlane = 0.2f;
                break;

            case TargetPlatformTier.StandalonePC:
                // PC/PS5向け高画質(2048シャドウマップ+ソフトシャドウ)
                // バイアスを極限まで小さくし、精密な接地感を出す
                targetLight.shadowBias = 0.015f;
                targetLight.shadowNormalBias = 0.4f;
                targetLight.shadowNearPlane = 0.1f;
                break;
        }
        
        Debug.Log($"[URPShadowBiasOptimizer] Light '{name}' のシャドウバイアスを {platformTier} 設定に最適化しました。");
    }
}

このスクリプトをDirectional Lightなどのライトオブジェクトにアタッチしておくことで、起動時のグラフィックス設定ロード時に ApplyOptimalSettings() を呼び出し、端末ごとの描画バグ発生リスクを未然に防ぐことができます。