🔳

【Unity】SpriteRendererで3D空間上に影を投影する【URP】

2024/01/02に公開

概要

SpriteRendererは2D向けの機能なので、3D空間上に影を落とす機能が標準で搭載されていません。しかし、所謂HD2Dのような表現をしたいときなど、SpriteRendererでも影を描画したいケースがあります。
今回は、そういったケースのために、SpriteRendererで影を描画する方法を紹介します。

目標

このような形で、SpriteRendererから3D空間上に影を投影することを目標にします。
影を受けることはこの記事では取り扱いません。

SpriteRendererの設定を変更する

STEP1 InspectorウィンドウをDebugモードにする

まずは、SpriteRendererの設定を変更します。
SpriteRendererの3D用の設定は、通常の表示モードだと隠れているので、
Inspectorウィンドウ右上のその他 (⋮) メニューから、
表示モードをDebugモードにします。


https://docs.unity3d.com/ja/2018.4/Manual/InspectorOptions.html

STEP2 SpriteRendererのCastShadowsをOnにする

InspectorウィンドウをDebugモードにすると、詳細なオプションが確認できます。
影を投影したいので、ここのCast ShadowsをOnに設定してください。
変更後は、InspectorビューをNormalモードに戻して大丈夫です。

ここで、影はまだ描画されないことに注意してください。
これは、標準のシェーダーが影を投影するパスを含んでいないからです。

3D用のシェーダーのマテリアルを適用すれば影が描画されるようになりますが、描画順がDepthバッファ基準になるため、Zファイティングが発生してしまいます。1枚の画像ならそれでも大丈夫ですが、SpriteRendererの各種機能(Sprite Skin, flipなど)を利用することができません。
そこで、シェーダーを複製/改変して影を描画するようにします。

シェーダーを改変する

STEP3 シェーダーを複製する

まずはデフォルトのSpriteシェーダーをコピーします。
URPの場合は、Projectビューの
Packages/Universal RP/Shaders/2D/Sprite-Unlit-Defaultをコピーしてください。
Sprite-Lit-Defaultには2D光源の影響を受ける処理が含まれているので、3D空間で使う際はUnlitを使用するのがいいと思います。

実際のファイルは以下のパスにあります。(バージョンは適宜置き換えてください)
Library\PackageCache\com.unity.render-pipelines.universal@15.0.6\Shaders\2D\Sprite-Unlit-Default.shader

STEP4 シェーダーの名前を変更する

シェーダーはこのような構造になっています。
まずは最上部のShaderの名前を変更します。

Shader "Universal Render Pipeline/2D/Sprite-Lit-Default" // <-ここを変更する
{
    Properties
    {
        // ...
    }

    SubShader
    {
        // ...
	Pass
	{
            // ...
	}
        // ...
    }
    Fallback "Sprites/Default"
}

STEP5 影用のパスを追加する

次に、SubShaderブロックの末尾に、影用のパスを追加します。

Shader "任意の名前"
{
    Properties
    {
        // ...
    }

    SubShader
    {
        // ...
	Pass
	{
            // ...
	}
        // ...
	
	// <-ここに挿入
    }
    Fallback "Sprites/Default"
}

挿入する影用のパス(URP)は以下の通りです。

Pass
{
    Tags { "LightMode" = "ShadowCaster" }
    
    ZWrite On
    ZTest LEqual
    ColorMask 0

    HLSLPROGRAM
    #pragma target 2.0
    
    // -------------------------------------
    // Shader Stages
    #pragma vertex ShadowPassVertex
    #pragma fragment ShadowPassFragment

    //--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing
    #include_with_pragmas "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DOTS.hlsl"

    // -------------------------------------
    // Universal Pipeline keywords

    // -------------------------------------

    // This is used during shadow map generation to differentiate between directional and punctual light shadows, as they use different formulas to apply Normal Bias
    #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW

    // -------------------------------------
    // Includes
    
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl"
    
    // Shadow Casting Light geometric parameters. These variables are used when applying the shadow Normal Bias and are set by UnityEngine.Rendering.Universal.ShadowUtils.SetupShadowCasterConstantBuffer in com.unity.render-pipelines.universal/Runtime/ShadowUtils.cs
    // For Directional lights, _LightDirection is used when applying shadow Normal Bias.
    // For Spot lights and Point lights, _LightPosition is used to compute the actual light direction because it is different at each shadow caster geometry vertex.
    float3 _LightDirection;
    float3 _LightPosition;
    
    struct Attributes
    {
        float3 positionOS   : POSITION;
        float3 normalOS     : NORMAL;
        UNITY_SKINNED_VERTEX_INPUTS
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };

    struct Varyings
    {
        float4  positionCS  : SV_POSITION;
        UNITY_VERTEX_OUTPUT_STEREO
    };
    
    
    float4 GetShadowPositionHClip(Attributes input)
    {
        float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
        float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
    
    #if _CASTING_PUNCTUAL_LIGHT_SHADOW
        float3 lightDirectionWS = normalize(_LightPosition - positionWS);
    #else
        float3 lightDirectionWS = _LightDirection;
    #endif
    
        float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
    
    #if UNITY_REVERSED_Z
        positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
    #else
        positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
    #endif
    
        return positionCS;
    }
    
    Varyings ShadowPassVertex(Attributes attributes)
    {
        Varyings o = (Varyings)0;
        UNITY_SETUP_INSTANCE_ID(attributes);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
        UNITY_SKINNED_VERTEX_COMPUTE(attributes);

        attributes.positionOS = UnityFlipSprite(attributes.positionOS, unity_SpriteProps.xy);
        o.positionCS = GetShadowPositionHClip(attributes);
        return o;
    }
    
    half4 ShadowPassFragment(Varyings i) : SV_TARGET
    {
        return 0;
    }
    ENDHLSL
}

これはURPのLitシェーダー(3D用のシェーダー)のShadowCasterパスとSprite-Unlit-Defaultシェーダーを切り貼りしたものです。LODやAlphaClip関係の処理を削除し、VertexShaderをSpirte-Unlit-Defaultシェーダーのものに差し替えました。

しかし、これらの対応をせずそのままLitシェーダーのShadowCasterパスをコピペしても何故か正常に描画されたため(Transparentなため?)、そちらでも問題ないかもしれません。

正直よくわかっていない部分なので、もし何かご存じの方がいらっしゃればご教示いただけると嬉しいです。

一応、AlphaClip対応バージョンも用意したので、gistに上げておきます。
https://gist.github.com/gameshalico/27b09a2476a72737e6a8a1b090259f9c

他のレンダーパイプラインを使用する場合は、それぞれのレンダーパイプラインの標準シェーダーからコピーしてください。
Tags { "LightMode" = "ShadowCaster" }を目印に、ShadowCasterで検索を掛けると見つかりやすいと思います。

マテリアルを適用する

STEP6 マテリアルを作成する

あとはマテリアルを適用するだけです。
Projectビューで右クリック->Create/Materialからマテリアルを作成し、
Inspectorビュー上部の赤枠の部分をクリックし、先ほど変更した名前のシェーダーに変更してください。

STEP7 SpriteRendererにマテリアルを設定する

最後に、SpriteRendererのMaterialスロットに先ほど作成したマテリアルを設定します。

完成

以上で影が描画されるはずです。
これでも影が描画されない場合は、各種グラフィックス設定を確認してください。
3Dのプリミティブをいくつか作成して影が落ちるか確認するといいかもしれません。

参考

https://forum.unity.com/threads/why-cant-sprites-gameobjects-cast-shadows.215461/

Discussion