UnityのURP(Universal Render Pipeline)を使用した3Dゲーム開発において、ガラス、水面、キャラクターのシースルー衣装、フェードアウトするエフェクトなどのマテリアルを「Transparent(半透明)」に設定した際、「オブジェクト自体の影が地面や壁に全く落ちなくなってしまった」「建物やキャラクターの影の中に半透明オブジェクトを置いても、影にならずにそこだけ不自然に明るく浮き上がって見える」という問題に直面したことはないでしょうか。

これらは、従来のBuilt-inパイプラインとURPの描画処理の違い、およびフォワードレンダリングにおける深度バッファ(Depth Buffer)の扱い方に起因する、極めて一般的かつ遭遇しやすいグラフィックスの仕様(および設定ミス)です。本記事では、半透明オブジェクトが影を「落とさない」「受け取らない」根本原因を物理・数学的な描画パイプラインの観点から解説し、Shader Graphを用いた正しい解決設定手順を詳しく紹介します。

1. 根本原因①:半透明オブジェクトが「影を落とさない」理由

半透明オブジェクトが影を落とさない最大の原因は、URPのデフォルト設定において「Shadow Caster」パスが半透明マテリアルに対して無効化されていることです。

Unityが影を生成する際、シーン内のすべての「影を落とすオブジェクト」をライトから見た視点でレンダリングし、「シャドウマップ(深度テクスチャ)」と呼ばれる画像に距離を書き込みます。この描画プロセスを実行するシェーダー内のコードを「Shadow Casterパス」と呼びます。

しかし、半透明オブジェクトは「向こう側が透けて見える」性質を持っています。もし半透明オブジェクトが完全に不透明な影を落としてしまうと、ガラスのコップの影が真っ黒な岩のようになってしまい、不自然極まりない見た目になります。そのため、URPの多くの標準半透明シェーダー(UnlitやLitのTransparentモード初期状態)では、処理負荷の削減を優先してShadow Casterパス自体が実行されないか、最初からパスが除外されています。これが、半透明に設定した瞬間に影が消えるカラクリです。

2. 根本原因②:半透明オブジェクトが「影を受け取らない」理由

一方で、半透明オブジェクト自身が他の物体の影(例:建物の影や他のキャラクターの影)に入った際、影に染まらず明るいまま描画されてしまう(=受影しない)原因は、フォワードレンダリングにおけるレンダーキュー(描画順序)と深度書き込み(Depth Write)の仕様にあります。

一般的なゲームエンジンにおける描画順序は以下の通りです:

  1. 不透明(Opaque)オブジェクトの描画: 手前から奥の順に描画し、深度バッファに距離を記録します。この時点で不透明部分に対するシャドウマップがサンプリングされ、影が適用されます。
  2. シャドウマップの描画完了: 不透明なオブジェクトに対する影の判定が確定します。
  3. 半透明(Transparent)オブジェクトの描画: 不透明オブジェクトの上に、奥から手前へ順番に重ね合わせて描画します。

このプロセス中、多くの半透明オブジェクトは深度バッファへの書き込み(Depth Write)をオフにして描画されます(※オンにすると後ろにある半透明オブジェクトが非描画になってしまうため)。深度が書き込まれないと、グラフィックスパイプラインは「そこにある半透明ピクセルがどの深さに位置しているか」を正確に判定できず、シャドウマップとの距離比較が行えなくなります。結果として、半透明オブジェクトは影の計算から除外され、ライトの光を100%受けて明るく浮き上がって描画されるバグが発生します。

3. Shader Graphにおける「影を落とす(Cast Shadow)」正しい実装手順

URPのShader Graphを使用して半透明オブジェクトから影を落とすためには、以下のステップを踏んでShader Graphを設定します。

重要なポイント: Shader Graphの「Graph Settings」で、適切なマテリアル設定を行う必要があります。
  1. 対象の Shader Graph ファイルを開きます。
  2. Graph Settings タブ(右側の歯車マーク)を選択します。
  3. 「Surface Type」を Transparent に設定します。
  4. その下に表示される Cast Shadows のチェックボックスをオンにします(※これによりシェーダーにShadow Casterパスが追加されます)。
  5. 半透明オブジェクトのアルファ値(透明度)に応じて影の濃さを変えたい場合、Alpha Clipping(アルファクリッピング)を有効にします。
    • Graph Settingsで Alpha Clipping にチェックを入れます。
    • Blackboard(黒板)から AlphaThreshold(または _Cutoff)プロパティを作成し、Master Stackの Alpha Clip Threshold に接続します。
    • テクスチャのアルファ情報をMaster Stackの Alpha に接続します。

これにより、指定した閾値(Threshold)より不透明な部分のみがシャドウマップに書き込まれ、綺麗な切り抜きシャドウ(例:半透明なフェンスや木の葉、網目状のオブジェクトの影)が落とせるようになります。

ディザリングを用いた「半透明な薄い影」の表現

完全に切り抜くのではなく、ガラスやベールのように「半透明で薄い影」を落としたい場合は、Dither(ディザ)ノードを使用します。Shader Graph内で Dither ノードを追加し、その出力をMaster Stackの Alpha Clip Threshold に接続することで、ピクセル単位で細かく点描画するような形で擬似的な半透明影を表現することができます。

4. Shader Graphにおける「影を受け取る(Receive Shadow)」正しい実装手順

半透明オブジェクトが周囲の影を受け取って正しく暗くなるようにするには、マテリアルの深度書き込み設定と、シェーダー内のライトサンプリング設定を正しく組み合わせる必要があります。

アプローチ方法 設定内容と手順 適用場面と注意点
A. Lit Shaderでの標準Receive設定 Graph Settingsの「Receive Shadows」をオンにし、かつインスペクターのマテリアル設定で Depth WriteForce Enabled に指定する。 最も簡単な解決策。ただし、複雑に重なり合う半透明オブジェクトでは、深度順の描画破綻(ソート問題)が起きやすくなります。
B. Custom Functionによる影サンプリング(Unlit時) Shader Graph内で Custom Function ノードを使用し、HLSLコード内でメインライトのシャドウ減衰率(MainLightRealtimeShadow)を直接取得してカラーに乗算する(後述のコードを参照)。 Unlit(ライティング計算を行わない高速なシェーダー)を使用しつつ、影だけを受け取らせたいUIや半透明エフェクトに最適です。

カスタムHLSLスクリプトを用いた受影の実装(Unlit Master Stack用)

Shader GraphのUnlit Master Stackを使用している場合、標準のライティング機能が動作しないため、周囲の影を反映するにはHLSLで影の強度(シャドウアッテネーション)を取得し、最終カラーに乗算する必要があります。以下は、そのために使用するカスタム関数(Custom Functionノード用)の実装コードです。

// Custom Functionノードに入力するHLSLコード (インプロファイルまたは別ファイル)
// 入力(Inputs): WorldPos (Vector3)
// 出力(Outputs): ShadowAtten (Vector1)

void GetMainLightShadow_float(float3 WorldPos, out float ShadowAtten)
{
    ShadowAtten = 1.0;
    #if defined(UNIVERSAL_LIGHTING_INCLUDED)
        // ワールド座標からシャドウマップ用の座標(シャドウ座標)を計算
        float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
        
        // メインライトの影データをサンプリング
        #if VERSION_GREATER_EQUAL(10, 0)
            ShadowAtten = MainLightRealtimeShadow(shadowCoord);
        #else
            ShadowAtten = GetMainLight(shadowCoord).shadowAttenuation;
        #endif
    #endif
}

このカスタム関数をShader Graphに組み込み、ワールド座標(Position ノードのWorldスペース)を接続して、出力された ShadowAtten(影の強さ。0で完全な影、1で日向)をベースカラーやアルベドのRGBA値に掛け合わせることで、Unlitの半透明マテリアルでも完璧に影が乗るようになります。

5. 半透明オブジェクトの描画順(ソート)破綻を防ぐ「Depth Priming」設定

半透明オブジェクトで「影をきれいに受け取らせるために Depth Write(ZWrite) をオンにした」結果、今度は半透明オブジェクトの向こう側にある別の半透明オブジェクトや、パーティクル、モデルのパーツの一部が透けて見えなくなり、奥がくり抜かれたように不自然に描画が途切れるバグが発生することがあります。

これは前述の通り、深度バッファに半透明の距離が書き込まれたことで、グラフィックスGPUが「手前にある不透明オブジェクトによって奥のピクセルが遮蔽されている」と誤認して奥の描画をスキップしてしまう(Z-Culling)現象です。

このソート問題を回避しつつ美しく描画するためのプロジェクト全体の最適化設定として、以下の方法が推奨されます:

  1. Depth Priming Modeの調整:
    URPアセットの設定インスペクターを開き、「Rendering」カテゴリの Depth Priming ModeAuto または Forced に設定します。これにより、カメラの初期化フェーズで深度プリパスが実行され、不透明と半透明の深度判定がより正確になります。
  2. Render FeaturesによるTransparent Depth Prepassの作成:
    URPの Scriptable Renderer Features を使用して、半透明オブジェクトが描画される直前に「深度のみを書き込む特別なパス(Depth Prepass)」を挟み込みます。これにより、マテリアル側で無理やりDepth Writeをオンにせずとも、システム側で半透明の正しい重なり深度が算出され、ソートの破綻を防ぎつつ完璧な影投影(Shadow Receive)を両立することが可能になります。