【Unity】URP14のカスケードシャドウマッピングの実装を読んでみる

2024/06/05に公開

はじめに

UnityのURP14のカスケードシャドウマッピング(Cascaded Shadow Mapping ; CSM) の実装を読み、まとめてみようと思います。

環境

  • Unity2022.3.31f1
  • UniversalRP 14.0.11

シャドウの実装箇所

Unityのシャドウは、主に以下に実装されています。

ファイルパス
シャドウマップをレンダリング (C#) Library/PackageCache/
com.unity.render-pipelines.universal@14.0.11/
Runtime/Passes/MainLightShadowCasterPass.cs
影の描画 (シェーダー) Library/PackageCache/
com.unity.render-pipelines.universal@14.0.11/
ShaderLibrary/Shadows.hlsl

Chapter 1. シャドウマッピングの概要

まず最初に、Unityにおけるシャドウマッピングの仕組みについて簡単に解説します。

影の表示範囲

カメラのnearfar や Universal Render Pipeline Assetの Shadow Distance を元にして、
影を表示する領域を決定します。

Unity上での設定箇所


カメラのNearとFar


URPのShadow Distance

カメラNearShadow Distanceで挟まれた領域が影を表示する領域となります。

シャドウディスタンス - Unity マニュアル
Shadow Distance (シャドウディスタンス) プロパティを使用して、Unity がリアルタイムの影 (シャドウ) をレンダリングするカメラからの距離制限を決定します。

現在のカメラのファークリップ面がシャドウディスタンスよりも近い場合、Unity はシャドウディスタンスではなくカメラのファークリップ面を使用します。
https://docs.unity3d.com/ja/2019.4/Manual/shadow-distance.html

シャドウマップの生成

光源から最も近いオブジェクトまでの深度値 d をテクスチャに描画します。

シャドウマップは以下のような表示になります。

D3D11 OpenGLES3 Vulkan Metal
シャドウマップ
UNITY_REVERSED_Z 1 0 1 1

シャドウマッピング - Unity マニュアル
Unity は、光線がサーフェスをヒットするまでに移動する距離の情報をシャドウマップに入力します。それから、シャドウマップをサンプリングしてライトがヒットするゲームオブジェクトのリアルタイムの影を計算します。
https://docs.unity3d.com/ja/2019.4/Manual/shadow-mapping.html

影の描画

オブジェクトを描画時、光源からの距離Dを計算し、シャドウマップ上の深度 d と比較します。
D > d であれば、影に入っていると判定することができます。
D \leq d ならば影には入っていません。


影の表示

シャドウアクネとDepth Bias

影を描画する時、D \approx d ならば影には入っていないという判定を行いますが、
特定の状況下では深度値の精度が足りず深度値の正常な大小比較ができなくなります。

この時、シャドウアクネと呼ばれる現象が発生します。

参考 : シャドウマッピングと Bias プロパティー - Unityマニュアル

シャドウアクネを回避するため、URPには2つのBiasパラメータが用意されています。

パラメータ 効能
Depth Bias 頂点を光源方向にずらす量
Normal Bias 頂点を法線方向にずらす量

これらのBiasパラメータは、ShadowCasterパスで影を描画する時に頂点を動かすのに使用されます。

com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl
float4 _ShadowBias; // x: depth bias, y: normal bias

float3 ApplyShadowBias(float3 positionWS, float3 normalWS, float3 lightDirection)
{
    float invNdotL = 1.0 - saturate(dot(lightDirection, normalWS));
    float scale = invNdotL * _ShadowBias.y;

    // normal bias is negative since we want to apply an inset normal offset
    positionWS = lightDirection * _ShadowBias.xxx + positionWS;
    positionWS = normalWS * scale.xxx + positionWS;
    return positionWS;
}
その他の機能 ShadowFade

shadow distanceより離れたところに影が出た時、途切れてしまうことがあります。

これを回避するため、URPにはShadow Fadeという機能が用意されています。

ShadowFadeの実装は以下の形になっています。

com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl
half GetMainLightShadowFade(float3 positionWS)
{
    float3 camToPixel = positionWS - _WorldSpaceCameraPos;
    float distanceCamToPixel2 = dot(camToPixel, camToPixel);

    float fade = saturate(distanceCamToPixel2 * float(_MainLightShadowParams.z) + float(_MainLightShadowParams.w));
    return half(fade);
}

まとめ (Chapter 1)

  • シャドウマップテクスチャには、光源からオブジェクトまでの深度値が格納される
  • オブジェクトの影を描画する時、光源からの距離とシャドウマップテクスチャ上の深度値を比較する


Chapter 2. カスケードシャドウ

シャドウを描画する時、カメラに近いところはテクスチャが拡大されて表示されるため、
ジャギジャギ感が目立ってしまうことがあります

Unityに用意されている、カスケードシャドウを利用することでジャギジャギ感を解消できます。

参考1 : シャドウカスケード - Unity マニュアル
参考2 : ユニバーサルレンダーパイプラインアセット | Universal RP 14.0

カスケードシャドウの仕組み

1つのテクスチャアトラスの複数枚のシャドウマップを作成します。

  • シャドウマップ1 : 近景だけの深度を記録したシャドウマップ
  • シャドウマップ2 : 近景と遠景の両方の深度を記録したシャドウマップ

シャドウマップ1は、影を映す範囲が狭いため、影が滑らかになります
シャドウマップ2は、影を映す範囲が広いため、影が粗くなります

シャドウを描画する時、以下を行います。

  • 近景の影を描画する際は、シャドウマップ1を使う (影が滑らか)
  • 遠景の影を描画する際は、シャドウマップ2を使う (影が粗い)

カスケードシャドウマップは4つまで増やせる

URP Asset上では、カスケードの数は4つまで増やすことができます。


Chapter 3. カスケードシャドウの実装を読む (シェーダー側)

ここから、Unityのシャドウマッピングのシェーダーコードを追っていきます。

1. 使用するカスケードの決定

カスケードシャドウマップを使用して影を描画するためには、
オブジェクトを描画するときに 何番目のシャドウマップを使用するか を求める必要があります。


テクスチャアトラス

分割球 (Split Sphere)

ワールド空間に 分割球(Split Sphere) を設定し、
描画点のワールド座標がどの球に属しているかを求めることで、
カスケードを決定することができます。

分割球は、カメラのFOVによってワールド空間上での半径や位置が変わります。

カスケードの決定 (ComputeCascadeIndex)

ComputeCascadeIndexを使うと、あるワールド座標positionWSがどのカスケードに含まれるかを取得することができます。

half cascadeIndex = ComputeCascadeIndex(positionWS);

ComputeCascadeIndex の中身の実装は以下になります。

com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl # L265
half ComputeCascadeIndex(float3 positionWS)
{
    // 点P と 球0 の中心からの距離
    float3 fromCenter0 = positionWS - _CascadeShadowSplitSpheres0.xyz;
    // 点P と 球1 の中心からの距離
    float3 fromCenter1 = positionWS - _CascadeShadowSplitSpheres1.xyz;
    // 点P と 球2 の中心からの距離
    float3 fromCenter2 = positionWS - _CascadeShadowSplitSpheres2.xyz;
    // 点P と 球3 の中心からの距離
    float3 fromCenter3 = positionWS - _CascadeShadowSplitSpheres3.xyz;
    
    // 球0,1,2,3と点Pの距離を2乗して、float4にまとめる
    // x: 点P と 球0の中心からの距離の2乗
    // y: 点P と 球1の中心からの距離の2乗
    // z: 点P と 球2の中心からの距離の2乗
    // w: 点P と 球3の中心からの距離の2乗
    float4 distances2 = float4(dot(fromCenter0, fromCenter0), dot(fromCenter1, fromCenter1), dot(fromCenter2, fromCenter2), dot(fromCenter3, fromCenter3));

    // 点Pが球の中に入っているかを判定
    // x: 点P が 球0 の中に入っていたら1, 入っていなかったら0
    // y: 点P が 球1 の中に入っていたら1, 入っていなかったら0
    // z: 点P が 球2 の中に入っていたら1, 入っていなかったら0
    // w: 点P が 球3 の中に入っていたら1, 入っていなかったら0
    half4 weights = half4(distances2 < _CascadeShadowSplitSphereRadii);

    // 隣接する球との差分を取る
    // x : 点P が 球0の内側にいる場合は1、それ以外は0
    // y : 点P が 球0の外側にいて球1の内側にいる場合は1、それ以外は0
    // z : 点P が 球1の外側にいて球2の内側にいる場合は1、それ以外は0
    // w : 点P が 球2の外側にいて球3の内側にいる場合は1、それ以外は0
    weights.yzw = saturate(weights.yzw - weights.xyz);

    // 求めたweightからインデックスを決定
    // ケース1 : 点Pが球0の内側にいる場合    -> 0
    // ケース2 : 点Pが球0と球1の間にいる場合 -> 1
    // ケース3 : 点Pが球1と球2の間にいる場合 -> 2
    // ケース4 : 点Pが球2と球3の間にいる場合 -> 3
    return half(4.0) - dot(weights, half4(4, 3, 2, 1));
}

2. shadowcoord の計算

シャドウマップテクスチャから影の情報を取り出すためには、サンプリング点の座標が必要となります。
この座標は、以下の関数TransformWorldToShadowCoordで計算されます。

com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl # L319
float4 TransformWorldToShadowCoord(float3 positionWS)
{
#ifdef _MAIN_LIGHT_SHADOWS_CASCADE
    half cascadeIndex = ComputeCascadeIndex(positionWS);
#else
    half cascadeIndex = half(0.0);
#endif

    float4 shadowCoord = mul(_MainLightWorldToShadow[cascadeIndex], float4(positionWS, 1.0));

    return float4(shadowCoord.xyz, 0);
}

入力のpositionWS.xyzはワールド空間の3次元座標で、変換後のshadowcoord.xyはテクスチャ座標、shadowcoord.z は深度値となります。
_MainLightWorldToShadow[]には、これはカスケードごとの変換行列が格納されています。

3. テクスチャサンプリング

シャドウマップテクスチャから、シャドウの情報を取り出す処理は以下になります。

  1. シャドウマップのテクスチャサンプリング
    • ソフトシャドウを使用しない場合は、SAMPLE_TEXTURE2D_SHADOWマクロを使用して1点でテクスチャサンプリング
    • ソフトシャドウを使用する場合
      • Low品質 : SampleShadowmapFilteredLowQuality関数を使用して、4周辺サンプリング
      • Medium品質 : SampleShadowmapFilteredMediumQuality関数を使用して、9周辺サンプリング
      • High品質 : SampleShadowmapFilteredHighQuality関数を使用して、16周辺サンプリング
  2. LerpWhiteTo関数を使用して、影の強弱を調整
    - shadowStrengthが0に近づくほど、LerpWhiteToの出力は1に近づく
  3. BEYOND_SHADOW_FAR マクロを利用して、shadowCoord.z が0~1の範囲外だったら1に補正
com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl
real SampleShadowmap(TEXTURE2D_SHADOW_PARAM(ShadowMap, sampler_ShadowMap), float4 shadowCoord, ShadowSamplingData samplingData, half4 shadowParams, bool isPerspectiveProjection = true)
{
    // Compiler will optimize this branch away as long as isPerspectiveProjection is known at compile time
    if (isPerspectiveProjection)
        shadowCoord.xyz /= shadowCoord.w;

    real attenuation;
    real shadowStrength = shadowParams.x;

    // Quality levels are only for platforms requiring strict static branches
    #if defined(_SHADOWS_SOFT_LOW)
        attenuation = SampleShadowmapFilteredLowQuality(TEXTURE2D_SHADOW_ARGS(ShadowMap, sampler_ShadowMap), shadowCoord, samplingData);
    #elif defined(_SHADOWS_SOFT_MEDIUM)
        attenuation = SampleShadowmapFilteredMediumQuality(TEXTURE2D_SHADOW_ARGS(ShadowMap, sampler_ShadowMap), shadowCoord, samplingData);
    #elif defined(_SHADOWS_SOFT_HIGH)
        attenuation = SampleShadowmapFilteredHighQuality(TEXTURE2D_SHADOW_ARGS(ShadowMap, sampler_ShadowMap), shadowCoord, samplingData);
    #elif defined(_SHADOWS_SOFT)
        if (shadowParams.y > SOFT_SHADOW_QUALITY_OFF)
        {
            attenuation = SampleShadowmapFiltered(TEXTURE2D_SHADOW_ARGS(ShadowMap, sampler_ShadowMap), shadowCoord, samplingData);
        }
        else
        {
            attenuation = real(SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz));
        }
    #else
        attenuation = real(SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz));
    #endif

    attenuation = LerpWhiteTo(attenuation, shadowStrength);

    // Shadow coords that fall out of the light frustum volume must always return attenuation 1.0
    // TODO: We could use branch here to save some perf on some platforms.
    return BEYOND_SHADOW_FAR(shadowCoord) ? 1.0 : attenuation;
}

SAMPLE_TEXTURE2D_SHADOWマクロ

シャドウマッピングのサンプリングには SAMPLE_TEXTURE2D_SHADOW マクロを使用しています。
内部ではSampleCmpLevelZero関数を呼び出しています。

com.unity.render-pipelines.core@14.0.11/ShaderLibrary/API/D3D11.hlsl
#define SAMPLE_TEXTURE2D_SHADOW(textureName, samplerName, coord3) textureName.SampleCmpLevelZero(samplerName, (coord3).xy, (coord3).z)

シャドウマップを shadowCoord.xyでサンプリングし、結果を shadowCoord.z と比較しています。

テクスチャをサンプリングし、結果を比較値と比較します。 この関数は、mipmap レベル 0 でのみ SampleCmp を 呼び出すことと同じです。
https://learn.microsoft.com/ja-jp/windows/win32/direct3dhlsl/dx-graphics-hlsl-to-samplecmplevelzero

こちらは、記事の始めの方の図を思い出してみるとイメージがつかみやすいかと思います。
(以下の図の D = shadowCoord.z で、d = シャドウマップ上に保存された深度値 です)

まとめ (Chapter 3)

  • ComputeCascadeIndexを使うと、ワールド座標からカスケードシャドウマップを決定できる
  • TransformWorldToShadowCoord を使うと、ワールド座標からシャドウマップ座標shadowCoordを決定できる
    • shadowCoord.xyはテクスチャ座標
    • shadowCoord.zは深度値
  • SampleShadowmapを使うとシャドウマップからテクスチャサンプリングできる
    • Unity上の設定に応じて、ソフトシャドウやハードシャドウをサンプリングする
    • SAMPLE_TEXTURE2D_SHADOW マクロは、テクスチャサンプリングと深度比較を同時に行う

Chapter 4. カスケードシャドウの実装を読む (C#側)

次に、レンダーパス(C#)側のコードを読んでいきます。

シャドウマッピングの処理は、MainLightShadowCasterPass に実装されています。

com.unity.render-pipelines.universal@14.0.11/Runtime/Passes/MainLightShadowCasterPass.cs

1. 行列や分割球の作成

カスケードシャドウの行列作成処理は ExtractDirectionalLightMatrix にて実装されています。

  1. カスケードシャドウ用のデータ作成 (ComputeDirectionalShadowMatricesAndCullingPrimitives)
    • 行列 viewMatrixprojectionMatrix
    • 分割球cascadeSplitDistance
  2. シャドウマッピング用の行列作成 (GetShadowTransform)
  3. シャドウスライス用の行列演算 (ApplySliceTransform)
com.unity.render-pipelines.universal@14.0.11/Runtime/ShadowUtils.cs # L110
public static bool ExtractDirectionalLightMatrix(ref CullingResults cullResults, ref ShadowData shadowData, int shadowLightIndex, int cascadeIndex, int shadowmapWidth, int shadowmapHeight, int shadowResolution, float shadowNearPlane, out Vector4 cascadeSplitDistance, out ShadowSliceData shadowSliceData)
{
    bool success = cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(shadowLightIndex,
        cascadeIndex, shadowData.mainLightShadowCascadesCount, shadowData.mainLightShadowCascadesSplit, shadowResolution, shadowNearPlane, out shadowSliceData.viewMatrix, out shadowSliceData.projectionMatrix,
        out shadowSliceData.splitData);

    cascadeSplitDistance = shadowSliceData.splitData.cullingSphere;
    shadowSliceData.offsetX = (cascadeIndex % 2) * shadowResolution;
    shadowSliceData.offsetY = (cascadeIndex / 2) * shadowResolution;
    shadowSliceData.resolution = shadowResolution;
    shadowSliceData.shadowTransform = GetShadowTransform(shadowSliceData.projectionMatrix, shadowSliceData.viewMatrix);

    // It is the culling sphere radius multiplier for shadow cascade blending
    // If this is less than 1.0, then it will begin to cull castors across cascades
    shadowSliceData.splitData.shadowCascadeBlendCullingFactor = 1.0f;

    // If we have shadow cascades baked into the atlas we bake cascade transform
    // in each shadow matrix to save shader ALU and L/S
    if (shadowData.mainLightShadowCascadesCount > 1)
        ApplySliceTransform(ref shadowSliceData, shadowmapWidth, shadowmapHeight);

    return success;
}

カスケードシャドウ用のデータ作成 (ComputeDirectionalShadowMatricesAndCullingPrimitives)

カスケードシャドウに使用する各種データを計算しています。

  • viewMatrix : ビュー行列
  • projectionMatrix : プロジェクション行列
  • splitData : 分割情報 (分割球など)
bool success = cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(shadowLightIndex,
        cascadeIndex, shadowData.mainLightShadowCascadesCount, shadowData.mainLightShadowCascadesSplit, shadowResolution, shadowNearPlane, out shadowSliceData.viewMatrix, out shadowSliceData.projectionMatrix,
        out shadowSliceData.splitData);

分割球(SplitSphere)の取得

以下では、求めたカスケードシャドウの分割球(shadowSliceData.splitData.cullingSphere)を
cascadeSplitDistanceに代入しています。

com.unity.render-pipelines.universal@14.0.11/Runtime/ShadowUtils.cs # L116
cascadeSplitDistance = shadowSliceData.splitData.cullingSphere;

ここで設定する分割球は、シェーダー側で _CascadeShadowSplitSpheres0 ~ 3 として受け取ることになります。

com.unity.render-pipelines.universal@14.0.11/ShaderLibrary/Shadows.hlsl
float4      _CascadeShadowSplitSpheres0;
float4      _CascadeShadowSplitSpheres1;
float4      _CascadeShadowSplitSpheres2;
float4      _CascadeShadowSplitSpheres3;

オフセット計算

以下では、シャドウマップのオフセット offsetXoffsetYを計算しています。

shadowSliceData.offsetX = (cascadeIndex % 2) * shadowResolution;
shadowSliceData.offsetY = (cascadeIndex / 2) * shadowResolution;

左下から順番にシャドウマップが並んでいきます。

カスケードのスライス

カスケードが2個以上の場合に、ApplySliceTransformを実行します。

com.unity.render-pipelines.universal@14.0.11/Runtime/ShadowUtils.cs # L128
// If we have shadow cascades baked into the atlas we bake cascade transform
// in each shadow matrix to save shader ALU and L/S
if (shadowData.mainLightShadowCascadesCount > 1)
    ApplySliceTransform(ref shadowSliceData, shadowmapWidth, shadowmapHeight);

ここでは、計算しておいたshadowSliceDataの offsetXoffsetYresolution を利用して、
テクスチャアトラス全体を、シャドウマップ1枚の範囲で切り取るような行列演算を追加します。

com.unity.render-pipelines.universal@14.0.11/Runtime/ShadowUtils.cs
/// <summary>
/// Used for baking bake cascade transforms in each shadow matrix.
/// </summary>
/// <param name="shadowSliceData"></param>
/// <param name="atlasWidth"></param>
/// <param name="atlasHeight"></param>
public static void ApplySliceTransform(ref ShadowSliceData shadowSliceData, int atlasWidth, int atlasHeight)
{
    Matrix4x4 sliceTransform = Matrix4x4.identity;
    float oneOverAtlasWidth = 1.0f / atlasWidth;
    float oneOverAtlasHeight = 1.0f / atlasHeight;
    sliceTransform.m00 = shadowSliceData.resolution * oneOverAtlasWidth;
    sliceTransform.m11 = shadowSliceData.resolution * oneOverAtlasHeight;
    sliceTransform.m03 = shadowSliceData.offsetX * oneOverAtlasWidth;
    sliceTransform.m13 = shadowSliceData.offsetY * oneOverAtlasHeight;

    // Apply shadow slice scale and offset
    shadowSliceData.shadowTransform = sliceTransform * shadowSliceData.shadowTransform;
}

2. シャドウマップ生成

Setupメソッドにて、ShadowUtils.ShadowRTReAllocateIfNeeded を利用して、シャドウマップを生成します。

MainLightShadowCasterPass.cs # L95
public bool Setup(ref RenderingData renderingData)
{

// (省略)

    ShadowUtils.ShadowRTReAllocateIfNeeded(ref m_MainLightShadowmapTexture, renderTargetWidth, renderTargetHeight, k_ShadowmapBufferBits, name: k_MainLightShadowMapTextureName);

// (省略)

}

シャドウマップの名前は _MainLightShadowmapTextureで、深度値は 16bit float です

MainLightShadowCasterPass.cs
const int k_ShadowmapBufferBits = 16;

private const string k_MainLightShadowMapTextureName = "_MainLightShadowmapTexture";

カスケードの数が2の時は、テクスチャの縦サイズは半分になる

カスケードシャドウの数が2の時だけ、アトラスの縦サイズを半分にする処理を入れています。

MainLightShadowCasterPass.cs # L126
int shadowResolution = ShadowUtils.GetMaxTileResolutionInAtlas(renderingData.shadowData.mainLightShadowmapWidth,
    renderingData.shadowData.mainLightShadowmapHeight, m_ShadowCasterCascadesCount);
renderTargetWidth = renderingData.shadowData.mainLightShadowmapWidth;
renderTargetHeight = (m_ShadowCasterCascadesCount == 2) ?
    renderingData.shadowData.mainLightShadowmapHeight >> 1 :
    renderingData.shadowData.mainLightShadowmapHeight;

この処理によって、シャドウマップが横に2枚だけ並ぶことになります。

シャドウマップのクリア

Configureメソッドでは、シャドウマップをクリアする処理が実装されています。

MainLightShadowCasterPass.cs
/// <inheritdoc />
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
    if (m_CreateEmptyShadowmap)
        ConfigureTarget(m_EmptyMainLightShadowmapTexture);
    else
        ConfigureTarget(m_MainLightShadowmapTexture);
    ConfigureClear(ClearFlag.All, Color.black);
}


シャドウマップのクリア

3. シャドウマップの描画

Executeにて、シャドウマップの描画処理 RenderMainLightCascadeShadowmap が実行されます

/// <inheritdoc/>
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    if (m_CreateEmptyShadowmap)
    {
        SetEmptyMainLightCascadeShadowmap(ref context, ref renderingData);
        renderingData.commandBuffer.SetGlobalTexture(m_MainLightShadowmapID, m_EmptyMainLightShadowmapTexture.nameID);

        return;
    }

    RenderMainLightCascadeShadowmap(ref context, ref renderingData);
    renderingData.commandBuffer.SetGlobalTexture(m_MainLightShadowmapID, m_MainLightShadowmapTexture.nameID);
}

RenderMainLightCascadeShadowmap

カスケードごとに以下の1~4を実行しています

  1. バイアスの計算 (ShadowUtils.GetShadowBias)
    • URPAssetで設定したバイアス値、シャドウ品質などを利用して、バイアスを計算しています
  2. シャドウ計算用の定数バッファを設定(ShadowUtils.SetupShadowCasterConstantBuffer)
    • ライト向きとライト位置の設定を個なっています
  3. Panctual Light Shadow の無効化
    • ポイントライトやスポットライトの影の描画を無効にしています
  4. 影の描画 (ShadowUtils.RenderShadowSlice)
MainLightShadowCasterPass.cs # L229
VisibleLight shadowLight = lightData.visibleLights[shadowLightIndex];

var cmd = renderingData.commandBuffer;
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.MainLightShadow)))
{
    // Need to start by setting the Camera position and worldToCamera Matrix as that is not set for passes executed before normal rendering
    ShadowUtils.SetCameraPosition(cmd, renderingData.cameraData.worldSpaceCameraPos);

    // Need set the worldToCamera Matrix as that is not set for passes executed before normal rendering,
    // otherwise shadows will behave incorrectly when Scene and Game windows are open at the same time (UUM-63267).
    ShadowUtils.SetWorldToCameraMatrix(cmd, renderingData.cameraData.GetViewMatrix());

    var settings = new ShadowDrawingSettings(cullResults, shadowLightIndex, BatchCullingProjectionType.Orthographic);
    settings.useRenderingLayerMaskTest = UniversalRenderPipeline.asset.useRenderingLayers;

    for (int cascadeIndex = 0; cascadeIndex < m_ShadowCasterCascadesCount; ++cascadeIndex)
    {
        settings.splitData = m_CascadeSlices[cascadeIndex].splitData;

        Vector4 shadowBias = ShadowUtils.GetShadowBias(ref shadowLight, shadowLightIndex, ref renderingData.shadowData, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].resolution);
        ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias);
        CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.CastingPunctualLightShadow, false);
        ShadowUtils.RenderShadowSlice(cmd, ref context, ref m_CascadeSlices[cascadeIndex],
            ref settings, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].viewMatrix);
    }
RenderShadowSlice の実装

RenderShadowSliceは、シャドウアトラスに対してシャドウマップを描画する処理となっています。

  1. cmd.SetViewport でレンダリング先の矩形を指定
  2. cmd.SetViewProjectionMatrices で、カスケードに対応した行列変換を行う
  3. context.DrawShadowsで影を描画
  4. context.DisableScissorRectでシザー矩形の解除
/// <summary>
/// Renders shadows to a shadow slice.
/// </summary>
/// <param name="cmd"></param>
/// <param name="context"></param>
/// <param name="shadowSliceData"></param>
/// <param name="settings"></param>
/// <param name="proj"></param>
/// <param name="view"></param>
public static void RenderShadowSlice(CommandBuffer cmd, ref ScriptableRenderContext context,
    ref ShadowSliceData shadowSliceData, ref ShadowDrawingSettings settings,
    Matrix4x4 proj, Matrix4x4 view)
{
    cmd.SetGlobalDepthBias(1.0f, 2.5f); // these values match HDRP defaults (see https://github.com/Unity-Technologies/Graphics/blob/9544b8ed2f98c62803d285096c91b44e9d8cbc47/com.unity.render-pipelines.high-definition/Runtime/Lighting/Shadow/HDShadowAtlas.cs#L197 )

    cmd.SetViewport(new Rect(shadowSliceData.offsetX, shadowSliceData.offsetY, shadowSliceData.resolution, shadowSliceData.resolution));
    cmd.SetViewProjectionMatrices(view, proj);
    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();
    context.DrawShadows(ref settings);
    cmd.DisableScissorRect();
    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();

    cmd.SetGlobalDepthBias(0.0f, 0.0f); // Restore previous depth bias values
}

FrameDebugger上を見ると、以下のようにカスケードシャドウマップが描画されていることが確認できます

まとめ (Chapter 4)

  • 主光源の影の描画は MainLightShadowCasterPass で行われている
  • カメラの nearshadow distance で挟まれた領域が影の描画範囲となる (shadow distance > farの場合は nearfar で挟まれた領域)
  • カスケードシャドウが有効な場合、1枚のテクスチャに複数枚のシャドウマップを詰め込んでいく
    • カスケードシャドウが2つの場合、横長のテクスチャに正方形のシャドウマップ2枚詰める
    • カスケードシャドウが4つの場合、正方形のテクスチャに1/2解像度の正方形のシャドウマップ4枚詰める

参考情報 (シャドウマッピング)

床井研究室 - 第27回 シャドウマッピング
https://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20050926

参考情報 (カスケードシャドウ)

超雑訳 Single-Pass Stable Cascaded Bounding Box Shadow Maps
https://project-asura.com/blog/archives/7550

Cascade Shadow进阶之路
https://zhuanlan.zhihu.com/p/379042993

Discussion