【Unity】URP14のカスケードシャドウマッピングの実装を読んでみる
はじめに
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におけるシャドウマッピングの仕組みについて簡単に解説します。
影の表示範囲
カメラのnear
、far
や Universal Render Pipeline Assetの Shadow Distance
を元にして、
影を表示する領域を決定します。
Unity上での設定箇所
カメラのNearとFar
URPのShadow Distance
カメラNear
と Shadow Distance
で挟まれた領域が影を表示する領域となります。
シャドウディスタンス - Unity マニュアル
Shadow Distance (シャドウディスタンス) プロパティを使用して、Unity がリアルタイムの影 (シャドウ) をレンダリングするカメラからの距離制限を決定します。
現在のカメラのファークリップ面がシャドウディスタンスよりも近い場合、Unity はシャドウディスタンスではなくカメラのファークリップ面を使用します。
https://docs.unity3d.com/ja/2019.4/Manual/shadow-distance.html
シャドウマップの生成
光源から最も近いオブジェクトまでの深度値
シャドウマップは以下のような表示になります。
D3D11 | OpenGLES3 | Vulkan | Metal | |
---|---|---|---|---|
シャドウマップ | ||||
UNITY_REVERSED_Z | 1 | 0 | 1 | 1 |
シャドウマッピング - Unity マニュアル
Unity は、光線がサーフェスをヒットするまでに移動する距離の情報をシャドウマップに入力します。それから、シャドウマップをサンプリングしてライトがヒットするゲームオブジェクトのリアルタイムの影を計算します。
https://docs.unity3d.com/ja/2019.4/Manual/shadow-mapping.html
影の描画
オブジェクトを描画時、光源からの距離
影の表示
シャドウアクネとDepth Bias
影を描画する時、
特定の状況下では深度値の精度が足りず深度値の正常な大小比較ができなくなります。
この時、シャドウアクネと呼ばれる現象が発生します。
参考 : シャドウマッピングと Bias プロパティー - Unityマニュアル
シャドウアクネを回避するため、URPには2つのBiasパラメータが用意されています。
パラメータ | 効能 |
---|---|
Depth Bias | 頂点を光源方向にずらす量 |
Normal Bias | 頂点を法線方向にずらす量 |
これらのBiasパラメータは、ShadowCasterパスで影を描画する時に頂点を動かすのに使用されます。
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の実装は以下の形になっています。
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
の中身の実装は以下になります。
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
で計算されます。
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. テクスチャサンプリング
シャドウマップテクスチャから、シャドウの情報を取り出す処理は以下になります。
- シャドウマップのテクスチャサンプリング
- ソフトシャドウを使用しない場合は、
SAMPLE_TEXTURE2D_SHADOW
マクロを使用して1点でテクスチャサンプリング - ソフトシャドウを使用する場合
- Low品質 :
SampleShadowmapFilteredLowQuality
関数を使用して、4周辺サンプリング - Medium品質 :
SampleShadowmapFilteredMediumQuality
関数を使用して、9周辺サンプリング - High品質 :
SampleShadowmapFilteredHighQuality
関数を使用して、16周辺サンプリング
- Low品質 :
- ソフトシャドウを使用しない場合は、
-
LerpWhiteTo
関数を使用して、影の強弱を調整
- shadowStrengthが0に近づくほど、LerpWhiteTo
の出力は1に近づく -
BEYOND_SHADOW_FAR
マクロを利用して、shadowCoord.z が0~1の範囲外だったら1に補正
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
関数を呼び出しています。
#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
こちらは、記事の始めの方の図を思い出してみるとイメージがつかみやすいかと思います。
(以下の図の shadowCoord.z
で、シャドウマップ上に保存された深度値
です)
まとめ (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
にて実装されています。
- カスケードシャドウ用のデータ作成 (
ComputeDirectionalShadowMatricesAndCullingPrimitives
)- 行列
viewMatrix
とprojectionMatrix
- 分割球
cascadeSplitDistance
- 行列
- シャドウマッピング用の行列作成 (
GetShadowTransform
) - シャドウスライス用の行列演算 (
ApplySliceTransform
)
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
に代入しています。
cascadeSplitDistance = shadowSliceData.splitData.cullingSphere;
ここで設定する分割球は、シェーダー側で _CascadeShadowSplitSpheres0 ~ 3 として受け取ることになります。
float4 _CascadeShadowSplitSpheres0;
float4 _CascadeShadowSplitSpheres1;
float4 _CascadeShadowSplitSpheres2;
float4 _CascadeShadowSplitSpheres3;
オフセット計算
以下では、シャドウマップのオフセット offsetX
と offsetY
を計算しています。
shadowSliceData.offsetX = (cascadeIndex % 2) * shadowResolution;
shadowSliceData.offsetY = (cascadeIndex / 2) * shadowResolution;
左下から順番にシャドウマップが並んでいきます。
カスケードのスライス
カスケードが2個以上の場合に、ApplySliceTransform
を実行します。
// 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の offsetX
と offsetY
、 resolution
を利用して、
テクスチャアトラス全体を、シャドウマップ1枚の範囲で切り取るような行列演算を追加します。
/// <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
を利用して、シャドウマップを生成します。
public bool Setup(ref RenderingData renderingData)
{
// (省略)
ShadowUtils.ShadowRTReAllocateIfNeeded(ref m_MainLightShadowmapTexture, renderTargetWidth, renderTargetHeight, k_ShadowmapBufferBits, name: k_MainLightShadowMapTextureName);
// (省略)
}
シャドウマップの名前は _MainLightShadowmapTexture
で、深度値は 16bit float です
const int k_ShadowmapBufferBits = 16;
private const string k_MainLightShadowMapTextureName = "_MainLightShadowmapTexture";
カスケードの数が2の時は、テクスチャの縦サイズは半分になる
カスケードシャドウの数が2の時だけ、アトラスの縦サイズを半分にする処理を入れています。
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メソッドでは、シャドウマップをクリアする処理が実装されています。
/// <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を実行しています
- バイアスの計算 (
ShadowUtils.GetShadowBias
)- URPAssetで設定したバイアス値、シャドウ品質などを利用して、バイアスを計算しています
- シャドウ計算用の定数バッファを設定(
ShadowUtils.SetupShadowCasterConstantBuffer
)- ライト向きとライト位置の設定を個なっています
- Panctual Light Shadow の無効化
- ポイントライトやスポットライトの影の描画を無効にしています
- 影の描画 (
ShadowUtils.RenderShadowSlice
)
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は、シャドウアトラスに対してシャドウマップを描画する処理となっています。
- cmd.SetViewport でレンダリング先の矩形を指定
- cmd.SetViewProjectionMatrices で、カスケードに対応した行列変換を行う
- context.DrawShadowsで影を描画
- 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
で行われている - カメラの
near
とshadow distance
で挟まれた領域が影の描画範囲となる (shadow distance
>far
の場合はnear
とfar
で挟まれた領域) - カスケードシャドウが有効な場合、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