知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】ライティングの基礎シェーダー #115

はじめに

今回は複数ライトに対応した基本的なシェーダーについて勉強します。

シェーダー

Shader "Day3/Practice1"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpecularPow ("Speclar Pow", float) = 5
    }

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

        CGINCLUDE

        sampler2D _MainTex;
        float4 _MainTex_ST;
        // ライトの色を取得する
        half4 _LightColor0;

        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
            half3 normal : NORMAL;
        };

        ENDCG

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

            CGPROGRAM

            struct v2f
            {
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
                float3 viewDir : TEXCOORD1;
                float3 lightDir : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            half _SpecularPow;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float isDirectional = step(1, _WorldSpaceLightPos0.w);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));

                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texCol = tex2D(_MainTex, i.uv);

                // 拡散反射
                float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

                // 鏡面反射
                float3 reflectVec = reflect(-i.lightDir, i.normal);
                float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow);

                // 環境光
                float3 ambient = ShadeSH9(float4(i.normal, 1));

                return fixed4(ambient, 1);

                fixed4 col = fixed4(texCol.rgb * (ambient + diffuse) + specular, texCol.a);

                return col;
            }
            ENDCG
        }

        Pass
        {
            Tags { "LightMode"="ForwardAdd" }
            Blend One One

            CGPROGRAM

            struct v2f
            {
                float2 uv : TEXCOORD0;
                half3 normal : NORMAL;
                float3 lightDir : TEXCOORD1;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                float isDirectional = step(1, _WorldSpaceLightPos0.w);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 texCol = tex2D(_MainTex, i.uv);
                float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

                return fixed4(texCol.rgb * lightCol, 1);
            }
            ENDCG
        }
    }
}

拡散反射光

非金属表面付近で起きる光の反射のうち、界面で発生する鏡面反射を除いた成分のことである

拡散反射 - Wikipedia:より引用

つまり、光があたっている部分を明るく、あたっていない部分は暗くなるものになります。

以下の画像のdiffuse reflectionがそれにあたります。

https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Lambert2.gif/440px-Lambert2.gif

拡散反射 - Wikipedia:より引用

求め方

光が一番当たる部分は、光の向きとオブジェクトの法線が逆ベクトルに
当たらない部分は垂直、もしくはそれ以上と考えれば求められそうです。

以下の画像のようにイメージして頂ければわかりやすいかと思います。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/nn_hokuson/20161101/20161101195523.jpg

【Unityシェーダ入門】ランバート拡散照明モデルを試す - おもちゃラボ:より引用

つまり、光の向きと法線の内積を求めることで光の強さを計算することができます。
それをコードに起こしたものが以下になります。

// vertex
float isDirectional = step(1, _WorldSpaceLightPos0.w);
o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional));


// fragment
float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

_WorldSpaceLightPos0

ディレクショナルライトとその他のライトで取得できるものが異なります。

ライト
ディレクショナルライト float4(ワールド空間の向き, 0)
その他のライト float4(ワールド空間の位置, 1)

その他のライトの場合は、位置が取得できるので向きベクトルへと変換しています。

鏡面反射光

二物質の界面において発生する反射である。鏡面反射では反射の法則が成り立ち、入射角と反射角を等しくする。

鏡面反射 - Wikipedia:より引用

つまり、どの程度光を反射させるかを示すものになります。

求め方

まずは、光が入ってくるベクトルの反射ベクトルを求めます。
そのベクトルと視線のベクトルの内積を求めることで、反射の強さを求めることができそうです。

以下の画像のようにイメージして頂ければわかりやすいかと思います。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/nn_hokuson/20161102/20161102205834.png

【Unityシェーダ入門】フォン鏡面反射で金属っぽくしてみる - おもちゃラボ:より引用

コードに起こしたものが以下になります。

// vertex
o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz);

// fragment
float3 reflectVec = reflect(-i.lightDir, i.normal);
float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow);

reflectはhlslに定義済の反射ベクトルを返すものになります。
また、計算結果にpowをしているのは物体によって反射率が異なるからになります。

環境光

物体間や物体内部における光の反射が生み出す間接光を簡易的に表現したい場合に用いる。

アンビエント | CG用語辞典 | CGWORLD Entry.jp:より引用

とのこと。

求め方

0.3f辺りを加算する手法もありますが、今回はShadeSH9を使用します。

コードに起こしたものが以下になります。

float3 ambient = ShadeSH9(float4(i.normal, 1));

ShadeSH9

引数に法線を入れることで、環境光を取得するものになります。
wは1.0とコメントに記載してあります。

定義は以下となります。

// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
    // Linear + constant polynomial terms
    half3 res = SHEvalLinearL0L1 (normal);

    // Quadratic polynomials
    res += SHEvalLinearL2 (normal);

#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif

    return res;
}

深堀り

ShadeSH9の中身を深堀りしてみましたが、結論から述べると理解できませんでした。

// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1 (half4 normal)
{
    half3 x;

    // Linear (L1) + constant (L0) polynomial terms
    x.r = dot(unity_SHAr,normal);
    x.g = dot(unity_SHAg,normal);
    x.b = dot(unity_SHAb,normal);

    return x;
}

// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
    half3 x1, x2;
    // 4 of the quadratic (L2) polynomials
    half4 vB = normal.xyzz * normal.yzzx;
    x1.r = dot(unity_SHBr,vB);
    x1.g = dot(unity_SHBg,vB);
    x1.b = dot(unity_SHBb,vB);

    // Final (5th) quadratic (L2) polynomial
    half vC = normal.x*normal.x - normal.y*normal.y;
    x2 = unity_SHC.rgb * vC;

    return x1 + x2;
}

unity_SH系は、球面調和関数によって求められたGIのようで、それと法線情報をごにょごにょして環境光を求めているっぽいです。

zhuanlan.zhihu.com

使わなかったもの

UNITY_LIGHTMODEL_AMBIENTは公式にLegacy variableとあるので、使用しませんでした。

UNITY_LIGHTMODEL_AMBIENTをそのまま返したものは以下になります。

f:id:soramamenatan:20220324182413p:plain

各光の描画結果

拡散反射光

f:id:soramamenatan:20220324185641p:plain

鏡面反射光

f:id:soramamenatan:20220324185657p:plain

環境光

f:id:soramamenatan:20220324185713p:plain

上記3つを合わせたもの

f:id:soramamenatan:20220324185352p:plain

他のライトの実装

基本的には、拡散反射光と同様に法線とライトの向きの内積によって求めます。

float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0;

また、Unityにライトのパスを追加していることを伝えるためにForwardAddをTagsに追加します。
今回は、色を追加するのでBlendはOne Oneにしています。

Tags { "LightMode"="ForwardAdd" }
Blend One One

他のライトを追加した描画結果

ポイントライトを追加しました。

f:id:soramamenatan:20220324190931p:plain

参考サイト様

qiita.com

qiita.com

【UnityShader】Deferred Shading #114

はじめに

Deffered Shadingについて勉強していきます。

Forward Rendering

各オブジェクトを1つ、または複数のパスで描画するものになります。
パスの数は、オブジェクトに作用するライトによって決まります。

docs.unity3d.com

ライティング

Forward Rendertingには以下の3つのライティングが存在します。

ライティング名 意味
ピクセルライティング(Per-pixel) ピクセルごとに計算される高価なもの
頂点ライティング(Per-vertex) 頂点ごとに計算される安価なもの
球面調和ライティング(SH) 頂点で複数のライトの球面調和による計算コストが実質0だが、あくまで近似なので精度は低い

これらをUnityが自動で割り振ってくれます。

パス

パスは以下の2種類があります。

パス名 意味
ベースパス 1つのディレクショナルライトと全ての頂点ライティング、球面調和ライティングでオブジェクトをレンダリングする
加算パス 追加されたピクセルライティングでオブジェクトをレンダリングする

問題点

ディレクショナルライト以外のピクセルライトは、加算パスでレンダリングされます。
これはひとつのライトごとにひとつのパスを使用するものになります。
つまり、1つのオブジェクトに対して複数のライトが当たる場面等で非常に負荷がかかってしまいます。

また、ピクセルライティングから頂点や球面調和ライティングに変更させても、これらは精度が低いものとなるので、不自然な描画結果になってしまう可能性があります。

Differd Rendering

こちらは2パスで描画する手法になります。

docs.unity3d.com

パス

https://docs.unity3d.com/550/Documentation/uploads/SL/CameraRenderFlowCmdBuffers.svg

パスは以下の2種類があります。

パス名 意味
G-Bufferパス 各オブジェクトを1回レンダリングするもの
描画に必要なデータをG-Bufferとしてテクスチャに書き込む
ライティングパス G-Bufferと深度に基づいてライティングを計算するパス

G-Buffer

G-Bufferはテクスチャとして各データを保持しているものになります。
1パス目で、描画に必要なデータをG-Bufferに書き込きます。
2パス目以降で、このテクスチャを使用して描画していきます。
これらのテクスチャはグローバルシェーダーのプロパティとして、設定されます。
詳しくは以下となります。

プロパティ名 意味 備考
_CameraGBufferTexture0 Diffuse Color(RBG), occlusion(A)
_CameraGBufferTexture1 Specular Color(RBG), roughness(A) roughness = 1.0 - smoothness
_CameraGBufferTexture2 法線ベクトル(RBG), 未使用(A)
_CameraGBufferTexture3 Emission + lighting + lightmaps + reflection probes カメラがHDRレンダリングを使用している場合、Emission + lightingのRenderTargetsは生成されない
代わりに、カメラがレンダリングするターゲットがRT3として使用される
_CameraGBufferTexture4 Light occlusion(RGBA) ShadowmaskかDistanceShadowmaskのライトモードを使用している場合に使える
_CameraDepthTexture Depth + Stencil

メリット

ライティングを2パス目でまとめて行うため、ライトを増やしても負荷がかかりにくいことが挙げられます。
また、オブジェクトに影響を与えられるライトの数に制限が無く、ピクセル毎に評価冴えるため正しく描画されます。

デメリット

半透明のオブジェクトを正しく描画できない

これはG-Bufferパスで情報を2次元のテクスチャに書き込んでしまうからになります。
このパス段階ではブレンドができず、G-Bufferには1ピクセルにつき1つのデータしか保存できないため不透明と半透明の情報を同時に保持できません。
これにより、オブジェクト同士の前後関係が無くなってしまい、半透明が正しく描画されなくなってしまいます。

MSAAが使えない

G-Bufferを全てMSAA用に変更する必要があります。
また、各サブピクセルでライティングを行う必要があり結果的にSSAAと負荷がそこまで変わらなくなってしまうからになります。

MSAA

1ピクセルをより小さなサブピクセルと呼ばれるもの二分割します。
この分割数はMSAAの手前に付いている値に応じて変化し、4xなら4分割、8xなら8分割になります。
このサブピクセルの深度を求め、深度の差が大きい箇所をエッジとして色を求める手法になります。

実装

適当にScene上にオブジェクトを配置します。
CameraのRendering PathをDeferredに設定して終了になります。

f:id:soramamenatan:20220213164106p:plain

結果

フレームデバッグ上に各Textureが出ています。

f:id:soramamenatan:20220213164121p:plain

CameraDepthTexture

f:id:soramamenatan:20220213164438p:plain

ShadowMapTexture

f:id:soramamenatan:20220213164455p:plain

GBufferTexture0

f:id:soramamenatan:20220213164510p:plain

GBufferTexture1

f:id:soramamenatan:20220213164520p:plain

GBufferTexture2

f:id:soramamenatan:20220213164530p:plain

参考サイト様

https://esprog.hatenablog.com/entry/2016/03/17/000143

https://light11.hatenadiary.com/entry/2019/09/05/212345

【UnityShader】Cascade Shadow Maps【3】 #114

はじめに

前回、CSMsのスクリプト部分について解説しました。
今回は、シェーダー部分について解説します。

深度を渡すシェーダー

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 : TEXCOORD0;
                float eyeZ : TEXCOORD1;
            };

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

解説

深度を渡すシェーダー

頂点シェーダー

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

スクリプトから渡してきた_gShadowBiasをクリップ座標のZに加算することでシャドウアクネを防いでいます。
また、このシェーダーは深度を渡すだけですので、ZWのみをdepthに入れています。

フラグメントシェーダー

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

Z値をWで除算することにより、正規化デバイス空間へと変換しています。
Z値はプラットフォームによって異なるのでその違いを吸収しています。

影を受け取るシェーダー

頂点シェーダー

特に何もしていないので、割愛させて頂きます。

フラグメントシェーダー

fixed4 frag (v2f i) : COLOR0
{
    fixed4 weights = GetCascadeWeights(i.eyeZ);
    fixed4 col = SampleShadowTexture(i.worldPos, weights);
    return col;
}
影の重み
// 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;
}

UnityObjectToClipPos()で取得したクリップ空間のw成分を引数としてどのシャドウマップを使用するかを決めています。

w成分はクリップ空間においてz成分の逆ベクトルとなっています。
クリップ空間は右手座標系ですので、カメラの前方にあるオブジェクトのz成分が負になっています。
そこでz成分の逆ベクトルであるw成分を使用することによって正の値を求めています。


例えば、カメラのNearが0.3でFarが100のときの各値は以下となります。

Near

f:id:soramamenatan:20220101090019p:plain

Far

f:id:soramamenatan:20220101090027p:plain


そして、w成分が仮に30だとすると

fiexd4 zNear = (1, 1, 1, 0);
fiexd4 zFar = (0, 0, 1, 1);

// (0, 0, 1, 0)
fixed4 weights = zNear * zFar;

となるので3番目のシャドウマップが使われることになります。

参考

https://answers.unity.com/questions/1364663/z-and-w-output-of-unityobjecttoclippos.html

影の計算
// 影の計算
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;
}

シャドウマップのサンプリングを行う関数となります。

スクリプト側から渡されたVP行列を元にシャドウマップのサンプリングを行い、重みに応じて描画するかの有無を決めています。

結果

影が描画されれば成功です。

f:id:soramamenatan:20220101092030p:plain

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

【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

【UnityShader】Cascade Shadow Maps【1】 #112

Cascade Shadow Mapsとは

カメラに近い位置の影はジャギーが目立って表示されています。
これを解決する手法の1つがCascade Shadow Maps(CSMs)になります。

ジャギーが目立ってしまっている例

奥の立方体は影のジャギーが特に目立たないのに対して、
手前の球は影のジャギーが目立っています。

f:id:soramamenatan:20210829132900p:plain

発生してしまう原因

このジャギーが発生してしまう原因は、シャドウマップの各領域がカメラの透視によって想定よりも拡大されているからになります。

以下の画像を見て頂けるとわかりやすいです。
視錐の遠い方の端は20ピクセルのシャドウマップに対し、
視錐の近い方の端は4ピクセルのシャドウマップになっています。
画面上では、どちらの端も同じサイズで表示されるので、近い方はシャドウマップの解像度が低くなってしまうのでジャギーが発生します。

https://docs.unity3d.com/ja/2019.4/uploads/Main/ShadMapFrustumDiagram.svg

シャドウカスケード - Unity マニュアルより引用

考え方

CSMsの考え方は、以下の手順になります。

  1. 視錐台を複数の視錐台に分割する
  2. 各サブ視錐台の正投影を計算
  3. 各サブ視錐台のシャドウマップをレンダリング
  4. シーンをレンダリング
    1. シャドウマップをバインドしてレンダリングする
    2. 頂点シェーダーで以下のことを行う
      1. 各ライト視錐台のテクスチャ座標を計算
      2. 頂点を変換する
    3. ピクセルシェーダーで以下のことを行う
      1. 適切なシャドウマップを決定する
      2. 必要ならテクスチャ座標を変換
      3. カスケードをサンプリングする

この手順を踏むことにより手前の影は詳細なものを、奥の影は広域にシャドウマッピングすることができます。
この時、シャドウマップの解像度はすべて同じで手前のものほどサイズが小さくなっています。

CSMsでのシャドウマップイメージ

https://docs.microsoft.com/ja-jp/windows/win32/dxtecharts/images/csm-shadow-quality.png

カスケードされたシャドウ マップ - Win32 apps | Microsoft Docs:より引用

ソースコード無しでの実装

オブジェクトの配置

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

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

f:id:soramamenatan:20210829135838p:plain

ライトの設定

ジャギーを目立たせやすくするために、ShadowTypeをHardShadowsにします。

f:id:soramamenatan:20210829135822p:plain

影をレンダリングするカメラの距離を伸ばす

Project Settingsを開き、Qualityを選びます。
Shadow Distanceの数値を250にします。
下のShadow CascadesをNo Cascadesにします。

f:id:soramamenatan:20210829135834p:plain

設定後

GameViewに戻ると、手前のオブジェクトの影が荒くなっています。
f:id:soramamenatan:20210829135826p:plain

Cascadeの追加

Project Settingsに戻り、Shadow CascadesをFour Cascadesにします。

f:id:soramamenatan:20210829135817p:plain

Cascade

GameViewに戻ると、手前のオブジェクトの影のジャギーが減っています。

f:id:soramamenatan:20210829135830p:plain


解説

Shadow CascadesをFour Cascadesにしたことにより、
Cascadeを4つにしているので影がジャギーなく描画できています。

各設定の詳細

Shadow Distance

カメラからどのくらい離れたところまで影を計算するかの値になります。

Shadow Cascades

Cascadeを用意する数を決めます。


問題点

Unityの公式に以下のように記載されています。

モバイルプラットホームでは、シャドウカスケードをサポートしていない

ディレクショナルライトのシャドウ - Unity マニュアル

ですので、モバイルの場合は自作する必要があります。


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


参考サイト様

docs.microsoft.com

docs.unity3d.com

【UnityShader】オブジェクトに軌跡をつける #111

はじめに

UnityのTrailRendererに似た、オブジェクトに追従する軌跡を制作していきます。

https://styly.cc/wp-content/uploads/2020/04/4_Texture.gif

【Unity】Trail Rendererを使って軌跡を光らせる方法 | STYLY:より引用


実装

Unity上

Scene上に適当なオブジェクトを配置します。

オブジェクト

x座標を少しズラしてください。
また、今回制作するマテリアルを刺してください。

f:id:soramamenatan:20210731141930p:plain

マテリアル

マテリアルには適当なノイズ用テクスチャを刺します。

f:id:soramamenatan:20210731142012p:plain

スクリプト

using UnityEngine;

public class Afterimage : MonoBehaviour
{
    [SerializeField]
    private Material _material;
    [SerializeField]
    private float _trailSpeed = 10f;
    
    private Vector3 _trailPos;
    private int _dirId;

    private void Awake()
    {
        _trailPos = transform.position;
        _dirId = Shader.PropertyToID("_TrailDir");
    }

    private void Update()
    {
        Trail();
        Rotate();
    }

    /// <summary>
    ///  原点を中心に回転させる
    /// </summary>
    private void Rotate()
    {
        var tr = transform;
        var angleAxis = Quaternion.AngleAxis(180 * Time.deltaTime, Vector3.forward);
        var pos = tr.position;
        tr.position = angleAxis * pos;
    }

    /// <summary>
    /// 軌跡
    /// </summary>
    private void Trail()
    {
        var time = Mathf.Clamp01(Time.deltaTime * _trailSpeed);
        var tr = transform.position;
        _trailPos = Vector3.Lerp(_trailPos, tr, time);
        _material.SetVector(_dirId, transform.InverseTransformDirection(_trailPos - tr));
    }
}

シェーダー

Shader "Unlit/Afterimage"
{
    Properties
    {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _NoiseTex ("Noise", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _NoiseTex;
            float4 _NoiseTex_ST;
            fixed4 _TrailDir;

            v2f vert (appdata v)
            {
                v2f o;
                fixed weight = saturate((dot(v.normal, _TrailDir)));
                fixed noise = tex2Dlod(_NoiseTex, float4(v.uv.xy, 0, 0)).r;
                fixed4 trail = _TrailDir * weight * noise;
                v.vertex.xyz = float3(v.vertex.x + trail.x, v.vertex.y + trail.y, v.vertex.z + trail.z);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}


解説

Trail()

軌跡用に頂点をどのくらい伸ばすかをシェーダー側に渡しているメソッドになります。

private void Trail()
{
    var time = Mathf.Clamp01(Time.deltaTime * _trailSpeed);
    var tr = transform.position;
    _trailPos = Vector3.Lerp(_trailPos, tr, time);
    _material.SetVector(_dirId, transform.InverseTransformDirection(_trailPos - tr));
}

InverseTransformDirection

引数に入れた方向ベクトルを、ワールド空間からローカル空間へと変更してくれます。
方向ではなく、位置の場合にはTransform.InverseTransformPointを使用します。

バーテックスシェーダー

頂点を伸ばしている箇所になります。

fixed weight = saturate((dot(v.normal, _TrailDir)));
fixed noise = tex2Dlod(_NoiseTex, float4(v.uv.xy, 0, 0)).r;
fixed4 trail = _TrailDir * weight * noise;
v.vertex.xyz = float3(v.vertex.x + trail.x, v.vertex.y + trail.y, v.vertex.z + trail.z);

weight

自身の法線方向と、スクリプトから渡された方向ベクトルの内積を取得することで、重みを出しています。
今回は、角度差が小さいほど重みが大きくなります。

tex2Dlod

バーテックスシェーダー版のtex2D()になります。
定義は以下となります。

tex2Dlod(テクスチャ, float4(u値, v値, 0, lodの値の指定(0 ~ 7)))
tex2Dlodの詳しい解説

テクスチャのlodの値がDepth値(カメラからの距離)によって決定されます。
Depth値が計算されるのはバーテックスシェーダーの処理が終わり、フラグメントシェーダーに値を渡すときになります。
ですので、バーテックスシェーダー内ではlodの値が自動で割り当てられないので、tex2Dlod()を使用して明示的にlod値を指定して上げる必要があります。
MipMap等でも同様の処理が必要になります。

https://docs.unity3d.com/2018.3/Documentation/uploads/SL/PipelineCullDepth.png

Unity - Manual: ShaderLab: Culling & Depth Testing:より引用


結果

オブジェクトに軌跡が追従しています。

f:id:soramamenatan:20210731141425g:plain


参考サイト様

qiita.com

akinow.livedoor.blog

【UnityShader】投影テクスチャシャドウ #110

投影テクスチャシャドウとは

前回行った投影テクスチャマッピングの応用技術となります。

soramamenatan.hatenablog.com


影用のカメラを用意し、光源と同じ方向に設置します。
そして、ターゲットとなるオブジェクトの影だけを描画しカメラのtergetTextureとします。
そうすることにより、影のみ描画することができます。

https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F235561%2Fb99b289a-9d62-0e12-f5e6-720f2dfd1db2.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=20ab981b24df5796741542229c7b6107

投影テクスチャシャドウを実作してみた - Qiita:より引用


実装

Unity上

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

  • 影用のカメラ
  • 影を落とすオブジェクト
  • 影を受けるオブジェクト

影用のカメラのInspector

f:id:soramamenatan:20210717160626p:plain

オブジェクト

影を落とすオブジェクトには、何もしなくて良いです。
影を受けるオブジェクトには、今回制作したマテリアルを刺してください。

f:id:soramamenatan:20210717160616p:plain

Scene

f:id:soramamenatan:20210717160621p:plain

スクリプト

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class ProjectionTextureShadow : MonoBehaviour
{
    [Header ("Setting")]
    [SerializeField]
    private Camera _camera;
    [SerializeField]
    private int _renderTextureSize = 512;
    [SerializeField]
    private Material _material;
    [SerializeField] 
    private Transform _lightTransform;
    
    
    private int _matrixVpId;
    private int _textureId;
    private int _posId;
    private RenderTexture _renderTexture;

    private void Start()
    {
        SetPropertyId();
        CameraSettings();
    }
    
    /// <summary>
    /// PropertyIdの設定
    /// </summary>
    private void SetPropertyId()
    {
        _matrixVpId = Shader.PropertyToID("_ShadowProjectorMatrixVP1");
        _textureId = Shader.PropertyToID("_ShadowProjectorTexture1");
        _posId = Shader.PropertyToID("_ShadowProjectorPos1");
    }

    /// <summary>
    /// カメラの設定
    /// </summary>
    private void CameraSettings()
    {
        // 先にレンダリングさせるために、小さい値に
        _camera.depth = -10000;
        _camera.clearFlags = CameraClearFlags.Color;
        _camera.backgroundColor = Color.white;
        // 点滅を防ぐ
        _camera.allowHDR = false;
    }

    /// <summary>
    /// カメラがシーンのレンダリングを開始する前に呼ばれる
    /// </summary>
    private void OnPreRender()
    {
        if (_renderTexture == null)
        {
            UpdateSettings();
        }
        SetMaterialParam();
    }
    
    /// <summary>
    /// 設定の更新
    /// </summary>
    private void UpdateSettings()
    {
        ReleaseTexture();
        SetLightPosition();
        UpdateRenderTexture();
    }
    
    /// <summary>
    /// テクスチャの解放
    /// </summary>
    private void ReleaseTexture()
    {
        if (_renderTexture == null)
        {
            return;
        }
        _material.SetTexture(_textureId, null);
        RenderTexture.ReleaseTemporary(_renderTexture);
        _renderTexture = null;
        _camera.targetTexture = null;
    }
    
    /// <summary>
    /// 位置の設定
    /// </summary>
    private void SetLightPosition()
    {
        var objTransform = transform;
        _lightTransform.position = objTransform.position;
        _lightTransform.rotation = objTransform.rotation;
    }
    
    /// <summary>
    /// RenderTextureの更新
    /// </summary>
    private void UpdateRenderTexture()
    {
        _renderTexture = RenderTexture.GetTemporary(_renderTextureSize, _renderTextureSize, 16, RenderTextureFormat.ARGB32);
        _camera.targetTexture = _renderTexture;
    }
    
    /// <summary>
    /// マテリアルのパラメータ設定
    /// </summary>
    private void SetMaterialParam()
    {
        var viewMatrix = _camera.worldToCameraMatrix;
        var projectionMatrix = GL.GetGPUProjectionMatrix(_camera.projectionMatrix, true);
        _material.SetMatrix(_matrixVpId, projectionMatrix * viewMatrix);
        _material.SetTexture(_textureId, _renderTexture);
        _material.SetVector(_posId, GetProjectorPos());
    }
    
    /// <summary>
    /// プロジェクターの座標取得
    /// </summary>
    /// <returns></returns>
    private Vector4 GetProjectorPos()
    {
        Vector4 projectorPos;
        // Orthographic
        // _ObjectSpaceLightPosを参考に、wに0が入っていたらOrthographicの前方方向とみなす
        if (_camera.orthographic)
        {
            projectorPos = transform.forward;
            projectorPos.w = 0;
        }
        // Perspective
        else
        {
            projectorPos = transform.position;
            projectorPos.w = 1;
        }
        return projectorPos;
    }
    
    private void OnDestroy()
    {
        ReleaseTexture();
    }
}

シェーダー

Shader "Unlit/ProjectionTextureShadow"
{
    Properties 
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 projectorSpacePos : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                float2 uv : TEXCOORD3;
            };
            
            sampler2D _ShadowProjectorTexture1;
            float4x4 _ShadowProjectorMatrixVP1;
            float4 _ShadowProjectorPos1;
            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            fixed4 frag (v2f i) : SV_Target
            {
                // Projection座標に変換
                // tex2DProj(_ProjectorTexture, i.projectorSpacePos)と同義
                i.projectorSpacePos.xyz /= i.projectorSpacePos.w;
                float4 projectorTex = tex2D(_ShadowProjectorTexture1, i.projectorSpacePos.xy);
                // カメラの範囲外(0 ~ 1)は描画しない
                fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5);
                float alpha = isOut.x * isOut.y * isOut.z;
                // プロジェクターから見て、裏側の面には描画しない
                // Perspective  : _ShadowProjectorPos.xyz - i.worldPos
                // Orthographic : -_ShadowProjectorPos.xyz
                alpha *= step(-dot(lerp(-_ShadowProjectorPos1.xyz, _ShadowProjectorPos1.xyz - i.worldPos, _ShadowProjectorPos1.w), i.worldNormal), 0);
                fixed4 col = tex2D(_MainTex, i.uv);
                return lerp(1, 1 - projectorTex.a, alpha) * col;
            }
            ENDCG
        }
    }
}


解説

基本は前回のものと同じですので、違う箇所のみ解説します。

CameraSettings()

カメラの初期設定を行っているメソッドになります。

private void CameraSettings()
{
    // 先にレンダリングさせるために、小さい値に
    _camera.depth = -10000;
    _camera.clearFlags = CameraClearFlags.Color;
    _camera.backgroundColor = Color.white;
    // 点滅を防ぐ
    _camera.allowHDR = false;
}

depth

depthは深度という意味ですが、カメラですと描画順で使用されています。
今回複数のカメラがある可能性を考慮して低い値にしています。

allowHDR

HDRを無効にしています。
HDRを有効にしていると、Zファイティングのようなものが起きてしまいます。
原因は調べても不明でしたので、詳しい方がいらっしゃれば、教えてくださると幸いです。

f:id:soramamenatan:20210717162534g:plain

HDRに関しては以下で解説しています。

soramamenatan.hatenablog.com

UpdateRenderTexture()

RenderTextureの更新を行っているメソッドになります。

private void UpdateRenderTexture()
{
    _renderTexture = RenderTexture.GetTemporary(_renderTextureSize, _renderTextureSize, 16, RenderTextureFormat.ARGB32);
    _camera.targetTexture = _renderTexture;
}

今回、シャドウマップとしてRenderTextureを生成するので第三引数のdepthBufferに値を入れています。

フラグメントシェーダー

最後のreturn箇所になります。

return lerp(1, 1 - projectorTex.a, alpha) * col;

alphaはカメラの投影手法によって切り替わるため0or1になります。
ですのでlerpで何もなしかRenderTextureを描画するかを決めています。


結果

影を受けるオブジェクトに、影が落ちているのがわかります。 また、Camera Previewを見るとシャドウマップに書き込まれている内容がわかります。

f:id:soramamenatan:20210717164224p:plain


参考サイト様

light11.hatenadiary.com

qiita.com