Unity投影シャドウ効果をどのように実現するか

##序文
Unityエンジンに付属するシャドウ機能は優れたShadowMapです。この記事では、プロジェクターを使用してシャドウを生成する、シャドウの別の実現法を紹介します。

---
##一.機能の実現
###1.メイン光源のシャドウ投影をオフにします
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e1685b12-c912-e89c-7841-2bf21f0c68f9.png)
上の図に示すように、シャドウ投影を使用する場合は、メイン光源をオフにしてシャドウを投影する必要があります。

###2.Projectorのセットアップ
図に示すように、Projectorコンポーネントを追加してから、ProjectorのGameObjectの方向を調整します。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/8384aeb7-4f18-844f-df68-7cd47a6f0dfe.png)
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2bf8d6d7-bd72-a060-035b-c6a7b61ffa81.png)

###3.コアコードを書く
上の図に示すように、ProjectorShadowスクリプトを書きます。

####3.1最初にRenderTextureを作成します

```ruby:RenderTexture
        mShadowRT = new RenderTexture(mRenderTexSize, mRenderTexSize, 0, RenderTextureFormat.R8);
        mShadowRT.name = "ShadowRT";
        mShadowRT.antiAliasing = 1;   // アンチエイリアスをオフにする
        mShadowRT.filterMode = FilterMode.Bilinear;
        mShadowRT.wrapMode = TextureWrapMode.Clamp;     // wrapmodeをClampに設定する必要がある
```
まず、このRenderTextureの形式はR8であり、この形式で作成されたテクスチャのメモリ占用が最小であることに注意してください。

実行時にテクスチャをチェックします。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/88b9ec04-df14-c4ff-58f7-771a972e18e3.png)
2048×2048のテクスチャを作成する場合、メモリは4MBしかありません。次に、AntiAliasingを1に設置します。つまり、アンチエイリアスを無効になさせます。WrapModeをClampに設置します。

最後に実行する時のパラメータを次の図に示します。図中のDepthBufferに対して、コードは設定されていませんが、デフォルトでオフになっています。この種のプロジェクションシャドウによって作成されたRenderTextureは、DepthBufferを使用する必要がないためです。オフにする必要があります。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/1db0805f-2f49-e17b-20e2-103f893ef186.png)

####3.2 Projectorの設置

```ruby:projector初期化
        mProjector = GetComponent<Projector>();
        mProjector.orthographic = true;
        mProjector.orthographicSize = mProjectorSize;
        mProjector.ignoreLayers = mLayerIgnoreReceiver;
        mProjector.material.SetTexture("_ShadowTex", mShadowRT);
```

ここでは、主にプロジェクターを正投影に設定することです。同時に、次の図に示すように、プロジェクターのサイズを設定し、プロジェクターの無視レイヤーを設定します。
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2333fa17-b14d-3afa-692d-58c56e7eceb9.png)
プロジェクターのサイズを23に設定します。無視レイヤーをUnitに設定して、つまり、ゲームで作成されるすべての単位です。
####3.3投影Cameraの作成

```ruby:camera初期化
        mShadowCam = gameObject.AddComponent<Camera>();
        mShadowCam.clearFlags = CameraClearFlags.Color;
        mShadowCam.backgroundColor = Color.black;
        mShadowCam.orthographic = true;
        mShadowCam.orthographicSize = mProjectorSize;
        mShadowCam.depth = -100.0f;
        mShadowCam.nearClipPlane = mProjector.nearClipPlane;
        mShadowCam.farClipPlane = mProjector.farClipPlane;
        mShadowCam.targetTexture = mShadowRT;
```
作成したCameraのClearFlagsをクリアカラーに設定します。

CameraのクリーニングカラーBackgroundColorを黒に設定します。

Cameraも正投影であるはずです、正投影のサイズもProjectorのサイズと同じである必要があります。

CameraのDepthを-100に設定します。つまり、メインCameraよりも早くレンダリングされることを意味します。

Cameraの近裁断面と遠裁断面の設置は、プロジェクターの近裁断面と遠裁断面の設置と同じであります。

CameraのTargetTextureは、作成されたRenderTextureに設定されます。つまり、CameraはすべてのオブジェクトをこのRenderTextureにレンダリングします。

####3.4レンダリング方法の選択
これは、この記事の鍵と感じます。いくつかの記事を参考にして、最後に2つの方法をまとめました。個人的には、その中のCommandBufferを使用する方法は、実際のプロジェクトに適していて、レンダリング効率を上げることができると思います。

まずはコート実現をご覧ください。

```ruby:
private void SwitchCommandBuffer()
    {
        Shader replaceshader = Shader.Find("ProjectorShadow/ShadowCaster");

        if (!mUseCommandBuf)
        {
            mShadowCam.cullingMask = mLayerCaster;

            mShadowCam.SetReplacementShader(replaceshader, "RenderType");
        }
        else
        {
            mShadowCam.cullingMask = 0;

            mShadowCam.RemoveAllCommandBuffers();
            if (mCommandBuf != null)
            {
                mCommandBuf.Dispose();
                mCommandBuf = null;
            }
            
            mCommandBuf = new CommandBuffer();
            mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf);

            if (mReplaceMat == null)
            {
                mReplaceMat = new Material(replaceshader);
                mReplaceMat.hideFlags = HideFlags.HideAndDontSave;
            }
        }
    }
```

#####3.4.1 CommandBufferを使用しない場合
主には下記の2行のコード

```ruby:
mShadowCam.cullingMask = mLayerCaster;
mShadowCam.SetReplacementShader(replaceshader, "RenderType");
```

CameraがどのレイヤーのGameObjectをレンダリングしますか、またはCameraレンダリングがどのShaderを使用して取り替えますかを設置します。

次の図に示すように、Cameraは作成されたすべてのUnitのみをレンダリングします。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/12c62b52-08c3-5c9c-a1d2-386ea89620a4.png)
Cameraが使用したShaderには、一つの普通な頂点/フラグメントShaderを使用して処理できます。

```ruby:
Shader "ProjectorShadow/ShadowCaster"
{
    Properties
    {
        _ShadowColor("Main Color", COLOR) = (1, 1, 1, 1)
    }
    
    SubShader
    {
        Tags{ "RenderType" = "Opaque" "Queue" = "Geometry" }

        Pass
        {
            ZWrite Off
            Cull Off

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            
            struct v2f
            {
                float4 pos : POSITION;
            };
            
            v2f vert(float4 vertex:POSITION)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                return o;
            }

            float4 frag(v2f i) :SV_TARGET
            {
                return 1;
            }
            
            ENDCG
        }
    }
}
```

このShaderは白を出力するためのものであり、同時に書き込みの深度を閉じます。裁断は使用しません。

#####3.4.2 CommandBufferを使用する場合
主に次のコード

```ruby:
mShadowCam.cullingMask = 0;

            mShadowCam.RemoveAllCommandBuffers();
            if (mCommandBuf != null)
            {
                mCommandBuf.Dispose();
                mCommandBuf = null;
            }
            
            mCommandBuf = new CommandBuffer();
            mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf);

            if (mReplaceMat == null)
            {
                mReplaceMat = new Material(replaceshader);
                mReplaceMat.hideFlags = HideFlags.HideAndDontSave;
            }
```

CameraのCullingMaskを0に設定し、つまり、Cameraはオブジェクトを何もレンダリングせず、すべてのレンダリングはCommandBufferによって実行されます。次に、CommandBufferを作成し、それをCameraのCommandBufferリストに追加します。

CommandBufferレンダリングに必要なMaterialを作成します。Materialが使用するShaderは上記の「ProjectorShadow / ShadowCaster」です。

フレームごとに更新すると、

```ruby:
private void FillCommandBuffer()
    {
        mCommandBuf.Clear();

        Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam);

        List<GameObject> listgo = UnitManager.Instance.UnitList;
        foreach (var go in listgo)
        {
            if (go == null)
                continue;

            Collider collider = go.GetComponentInChildren<Collider>();
            if (collider == null)
                continue;

            bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds);
            if (!bound)
                continue;

            Renderer[] renderlist = go.GetComponentsInChildren<Renderer>();
            if (renderlist.Length <= 0)
                continue;

            // 目に見えるrenderはありますか
            // あれば、GameObject全体をレンダリングします
            bool hasvis = false;
            foreach (var render in renderlist)
            {
                if (render == null)
                    continue;

                RenderVis rendervis = render.GetComponent<RenderVis>();
                if (rendervis == null)
                    continue;

                if (rendervis.IsVisible)
                {
                    hasvis = true;
                    break;
                }
            }

            foreach(var render in renderlist)
            {
                if (render == null)
                    continue;

                mCommandBuf.DrawRenderer(render, mReplaceMat);
            }           
        }
    }
```

ゲームで作成されたすべての単位をトラバースします。最初に、視錐台を通して投影Cameraに表示されない単位を削除します。
主に次の2行のコードです。

```ruby:
Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam);
bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds);
```

投影Cameraの視錐台を計算して取得し、関数を介して、単位のColliderは視錐台範囲にあるかどうかを判断します。これて、現在のフレームカメラが見えるUnitを発見できます。

次に、次の判断を行います。

```ruby:
Renderer[] renderlist = go.GetComponentsInChildren<Renderer>();
            if (renderlist.Length <= 0)
                continue;

            // 目に見えるrenderはありますか
            // あれば、GameObject全体をレンダリングします
            bool hasvis = false;
            foreach (var render in renderlist)
            {
                if (render == null)
                    continue;

                RenderVis rendervis = render.GetComponent<RenderVis>();
                if (rendervis == null)
                    continue;

                if (rendervis.IsVisible)
                {
                    hasvis = true;
                    break;
                }
            }
```

視錐台内のUnitに対して、すべてのRenderをトラバースして、このRenderで行えるかどうかを判断します。このUnitのRenderは一つしか見えない場合、この単位をレンダリングします（ここでRenderは見えるかどうかを考えせず、各Renderを個別にレンダリングします。なぜなら、レンダリングされたUnitの完成性を確保したい、部分的にレンダリングしたくないということです。全体がレンダリングされるか、レンダリングされないかのどちらかです）。

したがって、問題は、Unitが見えるのはいつか、見えないのはいつか、どうやって知るのかということです。 次のコードを見ることができます。

```ruby:
private bool mIsVisible = false;

    public bool IsVisible
    {
        get { return mIsVisible; }
    }

    void OnBecameVisible()
    {
        mIsVisible = true;
    }

    void OnBecameInvisible()
    {
        mIsVisible = false;
    }
```

このスクリプトは各レンダーの下でハングします。このRenderがカメラに見られると、UnityエンジンはOnBecameVisible関数をコールします。見られないと、OnBecameInvisible関数をコールします。

現在、このDemoでは、投影CameraがCommandBufferを使用する場合、Cameraはオブジェクトを何もレンダリングしません。Main Cameraのみが全てのRenderをレンダリングします。ですから、Visibleが見える時に、このRenderは画面に表示されますが、Visibleが見えない時には表示されないと理解できます。

まとめをします。フレームごとに更新する時、最初に投影Cameraを介してレンダリングできるUnitを選出して、このオブジェクトは同時にMain Cameraに見られるかどうかを判断します。両方満たされている場合は、mCommandBuf.DrawRenderer（render、mReplaceMat）;関数を使用して、作成されたRenderTextureにオブジェクトをレンダリングします。

####3.5プロジェクターShaderはどのように実現されていますか？
投影Shaderは実際に、一つのシャドウを受けるShaderであります。具体的な実現方は下記のように、

```ruby:
ZWrite Off
            ColorMask RGB
            Blend DstColor Zero
            Offset -1, -1

            v2f vert(float4 vertex:POSITION)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.sproj = mul(unity_Projector, vertex);
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            float4 frag(v2f i):SV_TARGET
            {
                half4 shadowCol = tex2Dproj(_ShadowTex, UNITY_PROJ_COORD(i.sproj));
                half maskCol = tex2Dproj(_FalloffTex, UNITY_PROJ_COORD(i.sproj)).r;
                half a = shadowCol.r * maskCol;
                float c = 1.0 - _Intensity * a;

                UNITY_APPLY_FOG_COLOR(i.fogCoord, c, fixed4(1,1,1,1));

                return c;
            }
```

Vertで、o.sproj = mul(unity_Projector, vertex)；で投影位置を計算して取得します。Fragで、UNITY_PROJ_COORD(i.sproj)を介して投影テクスチャ座標を計算します。次に、最終的な色をブレンドします。

下図のように、
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/fc70dc03-85d2-9d87-71cf-08322742df81.png)
Maskマップを追加しました。このMaskマップを使用すると、シャドウ辺縁をより適切に処理でき、シャドウ辺縁はフェードインおよびフェードアウトの効果があります。

###4.ゲームを実行します
効果図を以下に示します。同じ観点で、CommandBufferを使用してレンダリングするかどうかを切り替えます。同じ効果の下で、CommandBufferを使用したバッチの方が優れており、それに応じてパフォーマンスも向上します。（上の画像はCommandBufを使用しておらず、下の画像はCommandBufを使用しています）
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6ea4506d-6b82-afe4-0d56-43eb8bb77597.png)
CommandBufレンダリングを使用していません場合
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/108053f2-0a1f-7ac6-2273-decab40bb59e.png)
CommandBufレンダリングを使用する場合

---
##二.プロジェクトDemoのアドレス

https://github.com/xieliujian/UnityDemo_ProjectorShadow

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

UWA公式サイト：https://jp.uwa4d.com
UWA公式ブログ：https://blog.jp.uwa4d.com