【Unity】【シェーダ】投影テクスチャシャドウを実装する

Unityのシェーダで投影テクスチャシャドウを実装する方法をまとめました。

Unity2019.2.18

投影テクスチャシャドウ?

以前、投影テクスチャマッピングについてまとめた記事を書きました。

light11.hatenadiary.com

投影テクスチャシャドウはこれを影の描画に応用したテクニックです。
よって本記事の内容は上記の記事を前提としますので必要に応じて参照してください。

さて手法としては、まずディレクショナルライトと同じ位置に置いたプロジェクターから、
影をキャストするモデル(キャラクターなど)を影の色で描画します。

f:id:halya_11:20200204143359p:plain

次に影を受け取るモデル(床など)を描画します。
この時、投影テクスチャマッピングのときと同様にプロジェクターの
プロジェクション空間に変換した頂点座標を用いて影テクスチャをサンプリングします。

f:id:halya_11:20200204143616p:plain

サンプリングした色を元の色と乗算することにより、影の色が描画されます。

スクリプト

それではまずプロジェクターのスクリプトから書いていきます。

using UnityEngine;

[ExecuteAlways]
[RequireComponent(typeof(Camera))]
public class ShadowProjector : MonoBehaviour
{
    [SerializeField]
    private int _renderTextureSize = 512;

    private Camera _camera;
    [SerializeField]
    private RenderTexture _renderTexture;

    private void OnEnable()
    {
        _camera = GetComponent<Camera>();
        _renderTexture = new RenderTexture(_renderTextureSize, _renderTextureSize, 0, RenderTextureFormat.RGB565, 0);
        _camera.targetTexture = _renderTexture;
        _camera.SetReplacementShader(Shader.Find("ShadowProjector"), null);
        _camera.depth = -10000; // 先にレンダリングしたいので小さくしておく
        _camera.clearFlags = CameraClearFlags.Color;
        _camera.backgroundColor = Color.white;
    }

    private void OnDisable()
    {
        if (_renderTexture != null) {
            _camera.targetTexture = null;
            DestroyImmediate(_renderTexture);
        }
    }

    private void OnPostRender()
    {
        var viewMatrix = _camera.worldToCameraMatrix;
        var projectionMatrix = GL.GetGPUProjectionMatrix(_camera.projectionMatrix, true);
        Shader.SetGlobalMatrix("_ShadowProjectorMatrixVP", projectionMatrix * viewMatrix);
        Shader.SetGlobalTexture("_ShadowProjectorTexture", _renderTexture);
        // プロジェクターの位置を渡す
        // _ObjectSpaceLightPosのような感じでwに0が入っていたらOrthographicの前方方向とみなす
        var projectorPos = Vector4.zero;
        projectorPos = _camera.orthographic ? transform.forward : transform.position;
        projectorPos.w = _camera.orthographic ? 0 : 1;
        Shader.SetGlobalVector("_ShadowProjectorPos", projectorPos);
    }
}

投影テクスチャマッピングの記事では視錐台の情報を自前で用意していましたが、
今回はレンダリング処理も行うためCameraコンポーネントを使用しています。

このカメラはRenderTextureをレンダリングターゲットとし、ShadowProjectorというシェーダで対象モデルを描画します。
また、影を描画するカメラは一番最初に走らせたいのでCamera.depthを小さい値にしています。

さらにレンダリング前のOnPostRender()でVP行列やテクスチャをシェーダのグローバル変数にセットしています。

シェーダ

次に影を受けるためのシェーダを書きます。

Shader "ShadowReceiver"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 projectorSpacePos : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };
            
            sampler2D _ShadowProjectorTexture;
            float4x4 _ShadowProjectorMatrixVP;
            float4 _ShadowProjectorPos;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.projectorSpacePos = mul(mul(_ShadowProjectorMatrixVP, unity_ObjectToWorld), v.vertex);
                o.projectorSpacePos = ComputeScreenPos(o.projectorSpacePos);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.projectorSpacePos.xyz /= i.projectorSpacePos.w;
                float2 uv = i.projectorSpacePos.xy;
                float4 projectorTex = tex2D(_ShadowProjectorTexture, uv);
                // カメラの範囲外には適用しない
                fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5);
                float alpha = isOut.x * isOut.y * isOut.z;
                // プロジェクターから見て裏側の面には適用しない
                alpha *= step(-dot(lerp(-_ShadowProjectorPos.xyz, _ShadowProjectorPos.xyz - i.worldPos, _ShadowProjectorPos.w), i.worldNormal), 0);
                return lerp(1, projectorTex, alpha);
            }
            ENDCG
        }
    }
}

これはほぼ投影テクスチャマッピングのものと同様です。
今回は渡されたレンダーテクスチャの色をそのまま出しているだけですが、
このモデル自体の色と乗算すれば影の部分だけが黒く適用できます。

レンダリング結果

このスクリプトをアタッチしてディレクショナルライトと同じ姿勢に設定し、
影を受ける側のモデルにシェーダを適用すると以下のようになります。

f:id:halya_11:20200204144656p:plain

正常に影が描画されていることが確認できました。

関連

light11.hatenadiary.com