知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】Cascade Shadow Maps【2】 #113

はじめに

前回で、CSMsについて解説したので、今回は実装していきます。

soramamenatan.hatenablog.com

実装

Unity上

Scene上に以下のオブジェクトを配置します。

  • カメラ
  • ライト
  • カメラから近いオブジェクト(Sphere)
  • カメラから遠いオブジェクト(Cube)
  • Plane

f:id:soramamenatan:20210829145200p:plain

カメラのInspector

カメラに今回制作するスクリプトを刺します。
DirLightとMainCameraは先程のステップで配置したもの、ShadowCasterは影を描画するシェーダーを刺します。

f:id:soramamenatan:20210829145545p:plain

PlaneのInspector

Planeには影を受け取るシェーダーのマテリアルを刺します。

f:id:soramamenatan:20210829145933p:plain

ソースコード

スクリプト

using System.Collections.Generic;
using UnityEngine;

public class CascadedShadowMapping : MonoBehaviour
{
    [SerializeField] private Light _dirLight;
    [SerializeField] private Shader _shadowCaster;
    [SerializeField] private Camera _mainCamera;

    // 分割数
    private const int SplitsNum = 4;
    private Camera _dirLightCamera;
    // 影行列
    private readonly List<Matrix4x4> _world2ShadowMats = new List<Matrix4x4>(SplitsNum);
    // 分割したDirectionalLightカメラ
    private readonly GameObject[] _dirLightCameraSplits = new GameObject[SplitsNum];
    private readonly RenderTexture[] _depthTextures = new RenderTexture[SplitsNum];

    // ------------------視錐台関連------------------
    private float[] _lightSplitsNear;
    private float[] _lightSplitsFar;

    // 視錐台構造体
    private struct FrustumCorners
    {
        public Vector3[] NearCorners;
        public Vector3[] FarCorners;
    }

    private FrustumCorners[] _mainCameraSplitsFcs;
    private FrustumCorners[] _lightCameraSplitsFcs;
    // ------------------視錐台関連------------------

    private void Awake()
    {
        InitFrustumCorners();
        _dirLightCamera = CreateDirLightCamera();
        CreateRenderTexture();
    }

    /// <summary>
    /// 視錐台の初期化
    /// </summary>
    private void InitFrustumCorners()
    {
        _mainCameraSplitsFcs = new FrustumCorners[SplitsNum];
        _lightCameraSplitsFcs = new FrustumCorners[SplitsNum];
        for (var i = 0; i < SplitsNum; i++)
        {
            _mainCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum];
            _mainCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum];

            _lightCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum];
            _lightCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum];
        }
    }

    /// <summary>
    /// DirectionalLightカメラを生成
    /// </summary>
    /// <returns>生成したライト</returns>
    private Camera CreateDirLightCamera()
    {
        var goLightCamera = new GameObject("Directional Light Camera");
        var lightCamera = goLightCamera.AddComponent<Camera>();

        lightCamera.cullingMask = 1 << LayerMask.NameToLayer("Caster");
        lightCamera.backgroundColor = Color.white;
        lightCamera.clearFlags = CameraClearFlags.SolidColor;
        lightCamera.orthographic = true;
        lightCamera.enabled = false;

        for (var i = 0; i < SplitsNum; i++)
        {
            _dirLightCameraSplits[i] = new GameObject("dirLightCameraSplits" + i);
        }

        return lightCamera;
    }

     /// <summary>
     /// RenderTextureの生成
     /// </summary>
    private void CreateRenderTexture()
    {
        var rtFormat = RenderTextureFormat.ARGB32;
        if (!SystemInfo.SupportsRenderTextureFormat(rtFormat))
        {
            rtFormat = RenderTextureFormat.Default;
        }

        for (var i = 0; i < SplitsNum; i++)
        {
            // Stencilに書き込めるようにするので,Depthに24を指定
            _depthTextures[i] = new RenderTexture(1024, 1024, 24, rtFormat);
            Shader.SetGlobalTexture("_gShadowMapTexture" + i, _depthTextures[i]);
        }
    }

     private void Update()
    {
        // mainCameraを視錐台用に分割する
        CalcMainCameraSplitsFrustumCorners();
        // Light用Cameraの計算
        CalcLightCamera();

        if (!_dirLight || !_dirLightCamera)
        {
            return;
        }

        Shader.SetGlobalFloat("_gShadowBias", 0.005f);
        Shader.SetGlobalFloat("_gShadowStrength", 0.5f);

        // 影行列の計算
        CalcShadowMats();

        Shader.SetGlobalMatrixArray("_gWorld2Shadow", _world2ShadowMats);
    }



    /// <summary>
    /// mainCameraを視錐台用に分割する
    /// </summary>
    private void CalcMainCameraSplitsFrustumCorners()
    {
        var near = _mainCamera.nearClipPlane;
        var far = _mainCamera.farClipPlane;

        // UnityのCascadeSplitsの値
        // 0 : 6.7%, 1 : 13.3%, 2 : 26.7%, 4 : 53.5%,
        float[] nears =
        {
            near,
            far * 0.067f + near,
            far * 0.133f + far * 0.067f + near,
            far * 0.267f + far * 0.133f + far * 0.067f + near
        };
        float[] fars =
        {
            far * 0.067f + near,
            far * 0.133f + far * 0.067f + near,
            far * 0.267f + far * 0.133f + far * 0.067f + near,
            far
        };

        _lightSplitsNear = nears;
        _lightSplitsFar = fars;

        Shader.SetGlobalVector("_gLightSplitsNear", new Vector4(_lightSplitsNear[0], _lightSplitsNear[1], _lightSplitsNear[2], _lightSplitsNear[3]));
        Shader.SetGlobalVector("_gLightSplitsFar", new Vector4(_lightSplitsFar[0], _lightSplitsFar[1], _lightSplitsFar[2], _lightSplitsFar[3]));

        // 視錐台Cameraの頂点を計算
        for (var k = 0; k < SplitsNum; k++)
        {
            // near
            // ビューポート座標を指定して、指定したカメラ深度の4つの視錐台の頂点を指すビュー空間ベクトルを計算する。
            // 第4引数に視錐台ベクトルの配列が出力される
            _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners);
            for (var i = 0; i < SplitsNum; i++)
            {
                // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入
                _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
            }

            // far
            _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners);
            for (var i = 0; i < SplitsNum; i++)
            {
                _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
            }
        }
    }



    /// <summary>
    /// Light用Cameraの計算
    /// </summary>
    private void CalcLightCamera()
    {
        if (_dirLightCamera == null)
        {
            return;
        }

        for (var k = 0; k < SplitsNum; k++)
        {
            for (var i = 0; i < SplitsNum; i++)
            {
                // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている
                _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
                _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
            }

            // 分割した視錐台の各座標で一番小さいもの、一番大きいものを計算
            float[] xs = { _lightCameraSplitsFcs[k].NearCorners[0].x, _lightCameraSplitsFcs[k].NearCorners[1].x, _lightCameraSplitsFcs[k].NearCorners[2].x, _lightCameraSplitsFcs[k].NearCorners[3].x,
                       _lightCameraSplitsFcs[k].FarCorners[0].x, _lightCameraSplitsFcs[k].FarCorners[1].x, _lightCameraSplitsFcs[k].FarCorners[2].x, _lightCameraSplitsFcs[k].FarCorners[3].x };

            float[] ys = { _lightCameraSplitsFcs[k].NearCorners[0].y, _lightCameraSplitsFcs[k].NearCorners[1].y, _lightCameraSplitsFcs[k].NearCorners[2].y, _lightCameraSplitsFcs[k].NearCorners[3].y,
                       _lightCameraSplitsFcs[k].FarCorners[0].y, _lightCameraSplitsFcs[k].FarCorners[1].y, _lightCameraSplitsFcs[k].FarCorners[2].y, _lightCameraSplitsFcs[k].FarCorners[3].y };

            float[] zs = { _lightCameraSplitsFcs[k].NearCorners[0].z, _lightCameraSplitsFcs[k].NearCorners[1].z, _lightCameraSplitsFcs[k].NearCorners[2].z, _lightCameraSplitsFcs[k].NearCorners[3].z,
                       _lightCameraSplitsFcs[k].FarCorners[0].z, _lightCameraSplitsFcs[k].FarCorners[1].z, _lightCameraSplitsFcs[k].FarCorners[2].z, _lightCameraSplitsFcs[k].FarCorners[3].z };

            var minX = Mathf.Min(xs);
            var maxX = Mathf.Max(xs);

            var minY = Mathf.Min(ys);
            var maxY = Mathf.Max(ys);

            var minZ = Mathf.Min(zs);
            var maxZ = Mathf.Max(zs);

            // 視錐台Lightカメラに視錐台の情報を入れる
            _lightCameraSplitsFcs[k].NearCorners[0] = new Vector3(minX, minY, minZ);
            _lightCameraSplitsFcs[k].NearCorners[1] = new Vector3(maxX, minY, minZ);
            _lightCameraSplitsFcs[k].NearCorners[2] = new Vector3(maxX, maxY, minZ);
            _lightCameraSplitsFcs[k].NearCorners[3] = new Vector3(minX, maxY, minZ);

            _lightCameraSplitsFcs[k].FarCorners[0] = new Vector3(minX, minY, maxZ);
            _lightCameraSplitsFcs[k].FarCorners[1] = new Vector3(maxX, minY, maxZ);
            _lightCameraSplitsFcs[k].FarCorners[2] = new Vector3(maxX, maxY, maxZ);
            _lightCameraSplitsFcs[k].FarCorners[3] = new Vector3(minX, maxY, maxZ);

            // near平面の中心点
            var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f;

            // ローカルからワールドに変換
            _dirLightCameraSplits[k].transform.TransformPoint(pos);
            _dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation;
        }
    }

    /// <summary>
    /// 影行列の計算
    /// </summary>
    private void CalcShadowMats()
    {
        _world2ShadowMats.Clear();
        for (var i = 0; i < SplitsNum; i++)
        {
            // DirectionalLightカメラの設定
            ConstructLightCameraSplits(i);
            _dirLightCamera.targetTexture = _depthTextures[i];

            // shaderの設定でカメラのレンダリングを行う
            _dirLightCamera.RenderWithShader(_shadowCaster, "");

            // カメラでレンダリングを行うので、false
            var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false);
            // VP行列
            _world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix);
        }
    }

    /// <summary>
    /// DirectionalLightカメラの設定
    /// </summary>
    /// <param name="index">分割したDirectionalLightカメラのindex</param>
    private void ConstructLightCameraSplits(int index)
    {
        var cameraTransform = _dirLightCamera.transform;
        cameraTransform.position = _dirLightCameraSplits[index].transform.position;
        cameraTransform.rotation = _dirLightCameraSplits[index].transform.rotation;

        _dirLightCamera.nearClipPlane = _lightCameraSplitsFcs[index].NearCorners[0].z;
        _dirLightCamera.farClipPlane = _lightCameraSplitsFcs[index].FarCorners[0].z;

        _dirLightCamera.aspect = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[0] - _lightCameraSplitsFcs[index].NearCorners[1]) / Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]);
        _dirLightCamera.orthographicSize = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]) * 0.5f;
    }

    /// <summary>
    /// Gizmoの描画
    /// </summary>
    private void OnDrawGizmos()
    {
        if (_dirLightCamera == null)
        {
            return;
        }

        var fcs = new FrustumCorners[SplitsNum];
        for (var k = 0; k < SplitsNum; k++)
        {
            // mainCameraから見た、視錐台の分割線
            Gizmos.color = Color.white;
            Gizmos.DrawLine(_mainCameraSplitsFcs[k].NearCorners[1], _mainCameraSplitsFcs[k].NearCorners[2]);

            fcs[k].NearCorners = new Vector3[SplitsNum];
            fcs[k].FarCorners = new Vector3[SplitsNum];

            for (var i = 0; i < SplitsNum; i++)
            {
                fcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.TransformPoint(_lightCameraSplitsFcs[k].NearCorners[i]);
                fcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.TransformPoint(_lightCameraSplitsFcs[k].FarCorners[i]);
            }

            // 分割したDirectionalLightカメラの視錐台を描画
            // ライトのrotateを0にすれば、上のものと一致する
            Gizmos.color = Color.red;
            Gizmos.DrawLine(fcs[k].NearCorners[0], fcs[k].NearCorners[1]);
            Gizmos.DrawLine(fcs[k].NearCorners[1], fcs[k].NearCorners[2]);
            Gizmos.DrawLine(fcs[k].NearCorners[2], fcs[k].NearCorners[3]);
            Gizmos.DrawLine(fcs[k].NearCorners[3], fcs[k].NearCorners[0]);

            Gizmos.color = Color.green;
            Gizmos.DrawLine(fcs[k].FarCorners[0], fcs[k].FarCorners[1]);
            Gizmos.DrawLine(fcs[k].FarCorners[1], fcs[k].FarCorners[2]);
            Gizmos.DrawLine(fcs[k].FarCorners[2], fcs[k].FarCorners[3]);
            Gizmos.DrawLine(fcs[k].FarCorners[3], fcs[k].FarCorners[0]);

            Gizmos.DrawLine(fcs[k].NearCorners[0], fcs[k].FarCorners[0]);
            Gizmos.DrawLine(fcs[k].NearCorners[1], fcs[k].FarCorners[1]);
            Gizmos.DrawLine(fcs[k].NearCorners[2], fcs[k].FarCorners[2]);
            Gizmos.DrawLine(fcs[k].NearCorners[3], fcs[k].FarCorners[3]);
        }
    }

    private void OnDestroy()
    {
        _dirLightCamera = null;

        for (var i = 0; i < SplitsNum; i++)
        {
            if (_depthTextures[i])
            {
                DestroyImmediate(_depthTextures[i]);
            }
        }
    }
}

影を描画するシェーダー

Shader "CustomShadow/Caster"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        CGINCLUDE
        #include "UnityCG.cginc"
        struct v2f
        {
            float4 pos : SV_POSITION;
            float2 depth:TEXCOORD0;
        };

        uniform float _gShadowBias;
        v2f vert (appdata_full v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.pos.z += _gShadowBias;
            o.depth = o.pos.zw;
            return o;
        }

        fixed4 frag (v2f i) : COLOR
        {
            float depth = i.depth.x / i.depth.y;

            // プラットフォーム毎の違いを吸収
            // shaderの言語がGLSLならtrue
        #if defined (SHADER_TARGET_GLSL)
            // (-1, 1)-->(0, 1)
            depth = depth * 0.5 + 0.5;
            // 深度値が反転しているか
            // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向
        #elif defined (UNITY_REVERSED_Z)
            //(1, 0)-->(0, 1)
            depth = 1 - depth;
        #endif
            return depth;
        }
        ENDCG

        Pass {
            Cull front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

影を受け取るシェーダー

Shader "CustomShadow/Receiver" {

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

        Pass {
            Tags{ "LightMode" = "ForwardBase" }

            CGPROGRAM
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD3;
                float eyeZ : TEXCOORD2;
            };

            uniform float4x4 _gWorldToShadow;
            uniform sampler2D _gShadowMapTexture;
            uniform float4 _gShadowMapTexture_TexelSize;

            uniform float4 _gLightSplitsNear;
            uniform float4 _gLightSplitsFar;
            uniform float4x4 _gWorld2Shadow[4];

            uniform sampler2D _gShadowMapTexture0;
            uniform sampler2D _gShadowMapTexture1;
            uniform sampler2D _gShadowMapTexture2;
            uniform sampler2D _gShadowMapTexture3;

            uniform float _gShadowStrength;

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.eyeZ = o.pos.w;
                return o;
            }

            // Cascadeの重みを色として表示
            fixed4 GetCascadeWeights(float z)
            {
                fixed4 zNear = float4(z >= _gLightSplitsNear);
                fixed4 zFar = float4(z < _gLightSplitsFar);
                // 仮に2番目の視錐台の場合、(0,1,0,0)
                fixed4 weights = zNear * zFar;
                return weights;
            }

            // 影の計算
            float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex)
            {
                // WVP行列に変換し、0~1の範囲に
                float4 shadowCoord = mul(w2Shadow, wPos);
                shadowCoord.xy /= shadowCoord.w;
                shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5;

                float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy);

                // 深度
                float depth = shadowCoord.z / shadowCoord.w;
                #if defined (SHADER_TARGET_GLSL)
                    depth = depth * 0.5 + 0.5;
                #elif defined (UNITY_REVERSED_Z)
                    depth = 1 - depth;
                #endif

                float shadow = sampleDepth < depth ? _gShadowStrength : 1;
                // どの分割した視錐台のものの影かを返す
                return shadow * cascadeWeight;
            }

            // ShadowTextureのサンプリング
            float4 SampleShadowTexture(float4 wPos, fixed4 cascadeWeights)
            {
                float cascadeShadow =
                    CulcShadow(_gWorld2Shadow[0], wPos, cascadeWeights[0], _gShadowMapTexture0) +
                    CulcShadow(_gWorld2Shadow[1], wPos, cascadeWeights[1], _gShadowMapTexture1) +
                    CulcShadow(_gWorld2Shadow[2], wPos, cascadeWeights[2], _gShadowMapTexture2) +
                    CulcShadow(_gWorld2Shadow[3], wPos, cascadeWeights[3], _gShadowMapTexture3);

                return cascadeShadow * cascadeWeights;
            }

            fixed4 frag (v2f i) : COLOR0
            {
                fixed4 weights = GetCascadeWeights(i.eyeZ);
                fixed4 col = SampleShadowTexture(i.worldPos, weights);
                return col;
            }
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            ENDCG
        }
    }
}


解説

初期化

視錐台

private void InitFrustumCorners()
{
    _mainCameraSplitsFcs = new FrustumCorners[SplitsNum];
    _lightCameraSplitsFcs = new FrustumCorners[SplitsNum];
    for (var i = 0; i < SplitsNum; i++)
    {
        _mainCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum];
        _mainCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum];

        _lightCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum];
        _lightCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum];
    }
}

複数の視錐台に分割するための配列の初期化を行っています。

DirectionalLightカメラ

private Camera CreateDirLightCamera()
{
    var goLightCamera = new GameObject("Directional Light Camera");
    var lightCamera = goLightCamera.AddComponent<Camera>();

    lightCamera.cullingMask = 1 << LayerMask.NameToLayer("Caster");
    lightCamera.backgroundColor = Color.white;
    lightCamera.clearFlags = CameraClearFlags.SolidColor;
    lightCamera.orthographic = true;
    lightCamera.enabled = false;

    for (var i = 0; i < SplitsNum; i++)
    {
        _dirLightCameraSplits[i] = new GameObject("dirLightCameraSplits" + i);
    }

    return lightCamera;
}

DirectionalLight用のカメラと、視錐台用のDirectionalLightカメラを生成しています。

Update

MainCameraを4分割した、視錐台の計算

private void CalcMainCameraSplitsFrustumCorners()
{
    var near = _mainCamera.nearClipPlane;
    var far = _mainCamera.farClipPlane;

    // UnityのCascadeSplitsの値
    // 0 : 6.7%, 1 : 13.3%, 2 : 26.7%, 4 : 53.5%,
    float[] nears =
    {
        near,
        far * 0.067f + near,
        far * 0.133f + far * 0.067f + near,
        far * 0.267f + far * 0.133f + far * 0.067f + near
    };
    float[] fars =
    {
        far * 0.067f + near,
        far * 0.133f + far * 0.067f + near,
        far * 0.267f + far * 0.133f + far * 0.067f + near,
        far
    };

    _lightSplitsNear = nears;
    _lightSplitsFar = fars;

    Shader.SetGlobalVector("_gLightSplitsNear", new Vector4(_lightSplitsNear[0], _lightSplitsNear[1], _lightSplitsNear[2], _lightSplitsNear[3]));
    Shader.SetGlobalVector("_gLightSplitsFar", new Vector4(_lightSplitsFar[0], _lightSplitsFar[1], _lightSplitsFar[2], _lightSplitsFar[3]));

    // 視錐台Cameraの頂点を計算
    for (var k = 0; k < SplitsNum; k++)
    {
        // near
        // ビューポート座標を指定して、指定したカメラ深度の4つの錐台コーナーを指すビュー空間ベクトルを計算する。
        // 第4引数に視錐台ベクトルの配列が出力される
        _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners);
        for (var i = 0; i < SplitsNum; i++)
        {
            // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入
            _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
        }

        // far
        _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners);
        for (var i = 0; i < SplitsNum; i++)
        {
            _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
        }
    }
}
各視錐台のnearとfar
float[] nears =
{
    near,
    far * 0.067f + near,
    far * 0.133f + far * 0.067f + near,
    far * 0.267f + far * 0.133f + far * 0.067f + near
};
float[] fars =
{
    far * 0.067f + near,
    far * 0.133f + far * 0.067f + near,
    far * 0.267f + far * 0.133f + far * 0.067f + near,
    far
};

マジックナンバーは、CascadeSplitsの値となっています。

f:id:soramamenatan:20210908085704p:plain

これにより、各視錐台のnearとfarを計算しています。
実際の視錐台の値は以下の表となります。
※MainCameraのnearが0.3、farが100の場合

\ 1番目 2番目 3番目 4番目
near 0.3 7 20.3 47
far 7 20.3 47 100

表を見ても分かる通り、各視錐台にMainCameraのnearとfarを分割した値を割り当てられています。

各視錐台の各頂点の計算
// 視錐台Cameraの頂点を計算
for (var k = 0; k < SplitsNum; k++)
{
    // near
    // ビューポート座標を指定して、指定したカメラ深度の4つの錐台コーナーを指すビュー空間ベクトルを計算する。
    // 第4引数に視錐台ベクトルの配列が出力される
    _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners);
    for (var i = 0; i < SplitsNum; i++)
    {
        // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入
        _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
    }

    // far
    _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners);
    for (var i = 0; i < SplitsNum; i++)
    {
        _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
    }
}

CalculateFrustumCornersはビューポート座標を指定して、指定した深度の視錐台の4つの頂点を取得するものになります。
この座標はローカル座標で返ってくるので、TransformPointを使用してワールド座標へと変換します。

CalculateFrustumCorners
/// <summary>
/// ビューポート座標を指定して、指定したカメラ深度の4つの視錐台の頂点を指すビュー空間ベクトルを計算する。
/// </summary>
/// <param name="viewport">視錐台の計算に使用する正規化されたビューポート座標</param>
/// <param name="z">カメラの原点からの深度</param>
/// <param name="eye">カメラアイ</param>
/// <param name="outCorners">視錐台の頂点ベクトル出力配列, Lengthは4以上</param>
public void CalculateFrustumCorners(
    Rect viewport,
    float z,
    Camera.MonoOrStereoscopicEye eye,
    Vector3[] outCorners)

Light用Cameraの計算

/// <summary>
/// Light用Cameraの計算
/// </summary>
private void CalcLightCamera()
{
    if (_dirLightCamera == null)
    {
        return;
    }

    for (var k = 0; k < SplitsNum; k++)
    {
        for (var i = 0; i < SplitsNum; i++)
        {
            // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている
            _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
            _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
        }

        // 分割した視錐台の各座標で一番小さいもの、一番大きいものを計算
        float[] xs = { _lightCameraSplitsFcs[k].NearCorners[0].x, _lightCameraSplitsFcs[k].NearCorners[1].x, _lightCameraSplitsFcs[k].NearCorners[2].x, _lightCameraSplitsFcs[k].NearCorners[3].x,
                   _lightCameraSplitsFcs[k].FarCorners[0].x, _lightCameraSplitsFcs[k].FarCorners[1].x, _lightCameraSplitsFcs[k].FarCorners[2].x, _lightCameraSplitsFcs[k].FarCorners[3].x };

        float[] ys = { _lightCameraSplitsFcs[k].NearCorners[0].y, _lightCameraSplitsFcs[k].NearCorners[1].y, _lightCameraSplitsFcs[k].NearCorners[2].y, _lightCameraSplitsFcs[k].NearCorners[3].y,
                   _lightCameraSplitsFcs[k].FarCorners[0].y, _lightCameraSplitsFcs[k].FarCorners[1].y, _lightCameraSplitsFcs[k].FarCorners[2].y, _lightCameraSplitsFcs[k].FarCorners[3].y };

        float[] zs = { _lightCameraSplitsFcs[k].NearCorners[0].z, _lightCameraSplitsFcs[k].NearCorners[1].z, _lightCameraSplitsFcs[k].NearCorners[2].z, _lightCameraSplitsFcs[k].NearCorners[3].z,
                   _lightCameraSplitsFcs[k].FarCorners[0].z, _lightCameraSplitsFcs[k].FarCorners[1].z, _lightCameraSplitsFcs[k].FarCorners[2].z, _lightCameraSplitsFcs[k].FarCorners[3].z };

        var minX = Mathf.Min(xs);
        var maxX = Mathf.Max(xs);

        var minY = Mathf.Min(ys);
        var maxY = Mathf.Max(ys);

        var minZ = Mathf.Min(zs);
        var maxZ = Mathf.Max(zs);

        // 視錐台Lightカメラに視錐台の情報を入れる
        _lightCameraSplitsFcs[k].NearCorners[0] = new Vector3(minX, minY, minZ);
        _lightCameraSplitsFcs[k].NearCorners[1] = new Vector3(maxX, minY, minZ);
        _lightCameraSplitsFcs[k].NearCorners[2] = new Vector3(maxX, maxY, minZ);
        _lightCameraSplitsFcs[k].NearCorners[3] = new Vector3(minX, maxY, minZ);

        _lightCameraSplitsFcs[k].FarCorners[0] = new Vector3(minX, minY, maxZ);
        _lightCameraSplitsFcs[k].FarCorners[1] = new Vector3(maxX, minY, maxZ);
        _lightCameraSplitsFcs[k].FarCorners[2] = new Vector3(maxX, maxY, maxZ);
        _lightCameraSplitsFcs[k].FarCorners[3] = new Vector3(minX, maxY, maxZ);

        // near平面の中心点
        var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f;

        // ローカルからワールドに変換
        _dirLightCameraSplits[k].transform.TransformPoint(pos);
        _dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation;
    }
}

MainCameraを分割し各視錐台を計算しました。
その視錐台をもとに、対応したLightカメラを分割した視錐台を計算します。

各視錐台のnearとfarを計算
for (var i = 0; i < SplitsNum; i++)
{
    // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている
    _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]);
    _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]);
}

MainCamera視錐台にはワールド座標に変換したnearとfarの値があるので、DirectionalLightカメラのtransformでローカル座標へと変換し、DirectionalLightカメラ視錐台のnearとfarとします。

計算結果をDirectionalLightカメラに渡す
// near平面の中心点
var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f;

// ローカルからワールドに変換
_dirLightCameraSplits[k].transform.TransformPoint(pos);
_dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation;

各Lightカメラで計算したnear座標の中心を元に、DirectionalLightカメラに渡しています。
最初の計算のときにローカル座標に変換しているので、ワールド座標に戻すために再び変換する必要があります。

影の行列の計算

/// <summary>
/// 影行列の計算
/// </summary>
private void CalcShadowMats()
{
    _world2ShadowMats.Clear();
    for (var i = 0; i < SplitsNum; i++)
    {
        // DirectionalLightカメラの設定
        ConstructLightCameraSplits(i);
        _dirLightCamera.targetTexture = _depthTextures[i];

        // shaderの設定でカメラのレンダリングを行う
        _dirLightCamera.RenderWithShader(_shadowCaster, "");

        // カメラでレンダリングを行うので、false
        var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false);
        // VP行列
        _world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix);
    }
}
DirectionalLightカメラの設定
/// <summary>
/// DirectionalLightカメラの設定
/// </summary>
/// <param name="index">分割したDirectionalLightカメラのindex</param>
private void ConstructLightCameraSplits(int index)
{
    var cameraTransform = _dirLightCamera.transform;
    cameraTransform.position = _dirLightCameraSplits[index].transform.position;
    cameraTransform.rotation = _dirLightCameraSplits[index].transform.rotation;

    _dirLightCamera.nearClipPlane = _lightCameraSplitsFcs[index].NearCorners[0].z;
    _dirLightCamera.farClipPlane = _lightCameraSplitsFcs[index].FarCorners[0].z;

    _dirLightCamera.aspect = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[0] - _lightCameraSplitsFcs[index].NearCorners[1]) / Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]);
    _dirLightCamera.orthographicSize = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]) * 0.5f;
}

CalcLightCamera()で計算した、分割DirectionalLightCameraを元に、大元のirectionalLightCameraに情報を渡しています。
アスペクト比は、nearの視錐台から縦横のベクトルの長さを取得することで計算しています。
正投影時のサイズも、同様にnearの視錐台から計算しています。

orthographic モードの場合、カメラの半分のサイズ。 https://docs.unity3d.com/ja/560/ScriptReference/Camera-orthographicSize.html

とあるので、半分にしています。

行列の計算
// shaderの設定でカメラのレンダリングを行う
_dirLightCamera.RenderWithShader(_shadowCaster, "");

// カメラでレンダリングを行うので、false
var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false);
// VP行列
_world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix);
RenderWithShader

レンダリングに使うシェーダーを差し替え、そのシェーダーでレンダリングを行う関数になります。
差し替えだけですと、SetReplacementShader()というメソッドもあります。

GL.GetGPUProjectionMatrix

Projection行列のプラットフォーム毎の違いを吸収してくれるメソッドとなります。
この変換を正規化デバイス座標と呼びます。


最後に、Projection行列とDirectionalLightCameraのView行列を計算して終了となります。


今回は以上となります。
ここまでご視聴ありがとうございました。

参考サイト様

light11.hatenadiary.com