影 (シャドウ)効果を実装する方法

前書き

Unityエンジンに付属するシャドウ機能は、より効果的なShadowMapです。この記事では、プロジェクターを使用してシャドウを生成するシャドウの別の実装を紹介します。以下は、実行中のデモのビデオです。 HDビデオを表示するには、ここをクリックしてください。

 

一、機能の実現

1.主光源のシャドウ投影をオフにする

上図に示すように、影を投影する場合は、主光源をオフにして影を作る必要があります。

2.プロジェクターを設定する

下図のようにProjectorコンポーネントを追加してから、ProjectorのGameObjectの向きを調整します。

3.コアコードを記述する

上記のようにProjectorShadowスクリプトを作成します。

3.1最初にRenderTextureを作成する  

// render textureを作成する
        mShadowRT = new RenderTexture(mRenderTexSize, mRenderTexSize, 0, RenderTextureFormat.R8);
        mShadowRT.name = "ShadowRT";
        mShadowRT.antiAliasing = 1;   // アンチエイリアシング(antialiasing)をオフにする
        mShadowRT.filterMode = FilterMode.Bilinear;
        mShadowRT.wrapMode = TextureWrapMode.Clamp;     // wrapmodeをClampに設定する必要がある

まず、このRenderTextureの形式はR8であり、この形式によって作成されるテクスチャメモリの使用量が最も少ないことに注意してください。

実行時にテクスチャを表示します。

2048×2048のテクスチャを作成する場合、メモリは4MBのみです。次に、AntiAliasingが1に設定します。つまり、アンチエイリアシングはオンになりません。WrapModeはClampに設定します。

 

最終的な実行パラメータを次の図に示します。図のDepthBufferに対し、コードは設定されていませんが、デフォルトでオフになっています。この投影されたシャドウによって作成されたRenderTextureは、DepthBufferを使用する必要がないため、オフにする必要があります。

 

3.2 Projectorの設定

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

ここでの主なことは、プロジェクターを正射影に設定することです。同時に、次の図に示すように、プロジェクターのサイズを設定し、プロジェクターの無視レイヤーを設定します。

 

プロジェクターのサイズは23に設定し、無視レイヤーはUnitです。即ちゲームで作成されたすべてのユニットということです。

3.3 投影Cameraを作する   

//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のnearClipPlaneおよびnearClipPlaneは、プロジェクターの設定と同じです。

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

3.4レンダリング方法の選択

このセクションはポイントです。いくつかの記事を参照して、最後に2つの方法を要約しました。そのうち、CommandBufferの方法は実際のプロジェクトに適していて、レンダリング効率を向上させることができると思います。

最初にコードの実装を見てください。

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行になります。

mShadowCam.cullingMask = mLayerCaster;

mShadowCam.SetReplacementShader(replaceshader, "RenderType");

CameraがどのレイヤーのGameObjectをレンダリングするか、同時にCameraのレンダリングがどのShaderによって置き換えるかを設定します。

次の図に示すように、Cameraは作成されたすべてのUnitのみをレンダリングします。

Cameraで使用されるShaderに対し、通常の頂点/フラグメントShaderで処理できます。

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
        }
    }
}

このシェーダーは、書き込み深度を閉じながら、クリッピングせずに白色を出力します。

3.4.2 CommandBufferを使用する場合、コードは次のとおりです。

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」です。

フレームを更新する場合:

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);
            }           
        }
    }

ゲームで作成されたすべてのユニットをトラバースします。最初に視錐台をカリングし、投影カメラで表示できないUnitをカリングします。主に次の2行のコードです。

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

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

投影カメラの視錐台を計算し、関数を使用して、ユニットのColliderが視錐台内にあるかどうかを判断します。このようにして、現在のフレームでカメラが見ることができるUnitを除外できます。

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

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がOKかどうかを判断し、このUnitに一つのRenderが可視な場合にのみ、このユニットをレンダリングします。(Renderの可視性によって個別に各Renderをレンダリングしないのは、レンダリングされたUnitは部分的ではなく、全体的にレンダリングされることからです。要するに、全体的にレンダリングされたか、レンダリングしないかのことになっています。)

したがって、問題は、Unitがいつ可視的になり、いつ非可視になるかをどのように知るかということです。以下のコードスニペットを参照してください。

private bool mIsVisible = false;

    public bool IsVisible
    {
        get { return mIsVisible; }
    }

    void OnBecameVisible()
    {
        mIsVisible = true;
    }

    void OnBecameInvisible()
    {
        mIsVisible = false;
    }

このスクリプトは各Renderの下にハングアップします。Renderがカメラに表示されると、UnityエンジンはOnBecameVisible関数をコールします。レンダーカメラが表示されていない場合は、OnBecameInvisible関数がコールされます。

現在のこのDemoでは、投影CameraがCommandBufferを使用している場合、Cameraはオブジェクトをレンダリングせず、Main CameraのみがすべてのRenderをレンダリングするため、Visibleが表示されている場合、Renderが画面に表示されます。Visibleが表示されていない場合、このRenderは画面に表示されません。

要約すると、各フレームが更新されるときに、最初に投影CameraがレンダリングできるUnitを選び出し、次にこのオブジェクトがMain Cameraにも同時に表示されるかどうかを判断します。すべてが満たされたら、mCommandBuf.DrawRenderer(render、mReplaceMat);関数を使用して、オブジェクトを作成されたRenderTextureにレンダリングします。

3.5プロジェクターShaderはどのように実装されていますか?

投影Shaderは実際にはシャドウ受信Shaderであり、具体的な実装は次のとおりです。

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)によって計算されます。次に、最終的な色をブレンドします。

以下に示すように:

Mask画像が追加され、シャドウエッジをより適切に処理できるようになり、シャドウエッジはフェードインおよびフェードアウトの効果があります。

4.ゲームを実行する

以下に示したように、同じ表示角度で、レンダリングにCommandBufferを使用するかどうかを切り替えます。同じ効果の下で、CommandBufferによって使用されるBatchが優れており、それに応じてパフォーマンスも向上します。 (上の画像はCommandBufなしで、下の画像はCommandBufを使用しています)

 

CommandBufなしでのレンダリング

CommandBufを使用したレンダリング

 

二、プロジェクトDemoアドレス

https://github.com/xieliujian/UnityDemo_ProjectorShadow

 

三、参考ドキュメント

主に以下の2つの記事(中国語注意)を参照しました。

1.Unity3D手游开发日记(1) – 移动平台实时阴影方案

この記事では、シャドウスキームの技術的なポイントと注意点が非常にうまくまとめられていました。本記事で明確でない疑問点については、この記事を参照してください。

 

2.利用Projector实现动态阴影

この記事では、主に、このシャドウ効果を実現するためのDynamic Shadow Projectorプラグインの変更の経験を紹介しています。それはとても参考になります。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com